React.js 集成 Spring Boot 開發(fā) Web 應(yīng)用
1. 創(chuàng)建工程
reakt$ tree .
.
├── build.gradle
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── src
├── main
│ ├── kotlin
│ │ └── com
│ │ └── reaktboot
│ │ └── reakt
│ │ └── ReaktApplication.kt
│ └── resources
│ ├── application.properties
│ ├── static
│ └── templates
└── test
└── kotlin
└── com
└── reaktboot
└── reakt
└── ReaktApplicationTests.kt
16 directories, 8 files
導(dǎo)入 IDEA 中
2. 配置數(shù)據(jù)源 application-dev.properties
#mysql
spring.datasource.url=jdbc:mysql://localhost:3306/reakt?useUnicode=true&characterEncoding=UTF8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driverClassName=com.mysql.jdbc.Driver
# Specify the DBMS
spring.jpa.database=MYSQL
# Show or not log for each sql query
spring.jpa.show-sql=true
# Hibernate ddl auto (create, create-drop, update)
spring.jpa.hibernate.ddl-auto=create-drop
#spring.jpa.hibernate.ddl-auto=update
# stripped before adding them to the entity manager)
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
3. 創(chuàng)建User,Role 表
package com.reaktboot.reakt.entity
import javax.persistence.*
/**
* Created by jack on 2017/4/29.
*/
@Entity
class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = -1
@Column(length = 50, unique = true)
var username: String = ""
var password: String = ""
@ManyToMany(targetEntity = Role::class, fetch = FetchType.EAGER)
lateinit var roles: Set<Role>
override fun toString(): String {
return "User(id=$id, username='$username', password='$password', roles=$roles)"
}
}
package com.reaktboot.reakt.entity
import javax.persistence.*
/**
* Created by jack on 2017/4/29.
*/
@Entity
class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = -1
@Column(length = 50, unique = true)
var role: String = "ROLE_USER"
}
package com.reaktboot.reakt.dao
import com.reaktboot.reakt.entity.User
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
interface UserDao : JpaRepository<User, Long> {
@Query("""
select a from #{#entityName} a where a.username = :username
""")
fun findByUsername(@Param("username") username: String): User?
}
package com.reaktboot.reakt.dao
import com.reaktboot.reakt.entity.Role
import org.springframework.data.jpa.repository.JpaRepository
interface RoleDao : JpaRepository<Role, Long> {
}
4. 實(shí)現(xiàn)登陸權(quán)限校驗(yàn)
WebSecurityConfig
package com.reaktboot.reakt
import com.reaktboot.reakt.handler.MyAccessDeniedHandler
import com.reaktboot.reaktservice.MyUserDetailService
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.web.access.AccessDeniedHandler
/**
prePostEnabled :決定Spring Security的前注解是否可用 [@PreAuthorize,@PostAuthorize,..]
secureEnabled : 決定是否Spring Security的保障注解 [@Secured] 是否可用
jsr250Enabled :決定 JSR-250 annotations 注解[@RolesAllowed..] 是否可用.
*/
@Configuration
@EnableWebSecurity
// 開啟 Spring Security 方法級安全
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
class WebSecurityConfig : WebSecurityConfigurerAdapter() {
@Bean
fun myAccessDeniedHandler(): AccessDeniedHandler {
return MyAccessDeniedHandler("/403")
}
@Bean
override fun userDetailsService(): UserDetailsService {
return MyUserDetailService()
}
@Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http.csrf().disable()
http.authorizeRequests()
.antMatchers("/", // 首頁不攔截
"/css/**",
"/fonts/**",
"/js/**",
"/images/**" // 不攔截靜態(tài)資源
).permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
//.loginPage("/login")// url 請求路徑,對應(yīng) LoginController 里面的 @GetMapping("/login")
.usernameParameter("username")
.passwordParameter("password")
.defaultSuccessUrl("/main").permitAll()
.and()
.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler())
// .exceptionHandling().accessDeniedPage("/403")
.and()
.logout().permitAll()
http.logout().logoutSuccessUrl("/")
}
@Throws(Exception::class)
override fun configure(auth: AuthenticationManagerBuilder) {
//AuthenticationManager 使用我們的 lightSwordUserDetailService 來獲取用戶信息
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder())
}
/**
* 密碼加密算法
*
* @return
*/
@Bean
fun passwordEncoder(): BCryptPasswordEncoder {
return BCryptPasswordEncoder();
}
}
MyUserDetailService
package com.reaktboot.reaktservice
import com.reaktboot.reakt.dao.UserDao
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.stereotype.Service
@Service
class MyUserDetailService : UserDetailsService {
val logger = LoggerFactory.getLogger(MyUserDetailService::class.java)
@Autowired lateinit var userDao: UserDao
override fun loadUserByUsername(username: String): UserDetails {
val user = userDao.findByUsername(username) ?: throw UsernameNotFoundException(username + " not found")
logger.info("user = {}", user)
val roles = user.roles
val authorities = mutableSetOf<SimpleGrantedAuthority>()
roles.forEach {
authorities.add(SimpleGrantedAuthority(it.role))
}
return org.springframework.security.core.userdetails.User( // 此處為了區(qū)分我們本地系統(tǒng)中的 User 實(shí)體類硝逢,特意列出userdetails 的 User 類的全路徑
username,
user.password,
authorities
)
}
}
5. 前端使用 React.js 開發(fā): 目錄結(jié)構(gòu)
我們使用 nowa:
https://nowa-webpack.github.io/
使用文檔:
https://nowa-webpack.github.io/nowa/
PC 前端組件庫:
http://uxco.re/components/button/
6. 創(chuàng)建 React 前端工程
前端應(yīng)用工程目錄放到 /Users/jack/KotlinSpringBoot/reakt/src/main/resources 目錄下:
前端目錄結(jié)構(gòu)如下:
~/KotlinSpringBoot/reakt/src/main/resources/reakt$ ls
abc.json mock package-lock.json src
html node_modules package.json webpack.config.js
~/KotlinSpringBoot/reakt/src/main/resources/reakt$ tree src/
src/
├── app
│ ├── app.js
│ ├── app.less
│ ├── db.js
│ ├── util.js
│ └── variables.js
├── components
│ ├── search-data
│ │ ├── SearchData.jsx
│ │ └── index.js
│ └── search-word
│ ├── SearchWord.jsx
│ └── index.js
├── images
│ └── README.md
└── pages
├── demo
│ ├── PageDemo.jsx
│ ├── PageDemo.less
│ ├── index.js
│ └── logic.js
└── home
├── PageHome.jsx
├── PageHome.less
├── index.js
└── logic.js
8 directories, 18 files
前端工程應(yīng)用單獨(dú)啟動:
jack@jacks-MacBook-Air:~/KotlinSpringBoot/reakt/src/main/resources/reakt$ nowa server
Listening at http://192.168.0.104:3000
瀏覽器訪問: http://192.168.0.104:3000
可以看到 nowa 集成的 uxcore 的樣板示例工程:
nowa 使用參考: https://segmentfault.com/a/1190000009088343
nowa 使用的體驗(yàn)兩大精華地方,
不需要學(xué)習(xí)webpack, 整個前端開發(fā)環(huán)境都集成了. react入門的小白最喜歡了, 我學(xué)webpack頭大死了,到現(xiàn)在也沒有搞明白. 用了nowa,我就不需要搞明白了.
掌握了nowa的腳手架模板, 整個開發(fā)效率提升2倍.
如果說react將組件的復(fù)用提高到極限,減少了重復(fù)代碼的工作量. nowa的自定義腳手架,則把項(xiàng)目文件的復(fù)用便捷性提高到極限, 以前要復(fù)制一組文件,然后修改文件名/組件名..等等.
現(xiàn)在:
用 nowa init mod 創(chuàng)建一組函數(shù)組件
用nowa init rmod 創(chuàng)建一組react組件,
用nowa init page 創(chuàng)建自己個性化的一組文件,
用nwoa init api 創(chuàng)建api資源模塊,
創(chuàng)建好了,直接可以寫業(yè)務(wù)代碼,不需要復(fù)制粘貼啥的了. 當(dāng)然mod rmod page api 這幾個都是按項(xiàng)目和自己習(xí)慣,定義過的模板.
gui版本,我也體驗(yàn)了一下, 管理項(xiàng)目方便了.不用去文件夾里面翻找了.
7. 前后端目錄集成
Navbar.jsx
import {Component} from 'react';
import './Navbar.less';
const Menu = require('uxcore-menu')
const SubMenu = Menu.SubMenu
const MenuItem = Menu.Item
export default class Navbar extends Component {
static defaultProps = {}
static propTypes = {}
constructor(props) {
super(props);
this.state = {
current: '1'
}
}
handleClick(e) {
console.log('click ', e);
this.setState({
current: e.key,
});
}
render() {
return (
<div>
<Menu onClick={this.handleClick.bind(this)} selectedKeys={[this.state.current]} mode="horizontal">
<Menu.Item key="brand" className = 'brand-style'>
<h3>Reakt</h3>
</Menu.Item>
<Menu.Item key="mail">
<i className="kuma-icon kuma-icon-email"/>首頁
</Menu.Item>
<Menu.Item key="app">
<i className="kuma-icon kuma-icon-wangwang"/>快速開始
</Menu.Item>
<SubMenu title={<span><i className="kuma-icon kuma-icon-setting"/>博客文章</span>}>
<Menu.Item key="setting:1">選項(xiàng)1</Menu.Item>
<Menu.Item key="setting:2">選項(xiàng)2</Menu.Item>
<Menu.Item key="setting:3">選項(xiàng)3</Menu.Item>
<Menu.Item key="setting:4">選項(xiàng)4</Menu.Item>
</SubMenu>
<Menu.Item key="alipay">
<a href="#" target="_blank">關(guān)于我們</a>
</Menu.Item>
</Menu>
<Menu
className="kuma-menu-none-border menu-style"
defaultOpenKeys={['sub1']}
selectedKeys={[this.state.current]}
mode="inline">
<SubMenu key={"sub1"} title={<span><i className="kuma-icon kuma-icon-email"/><span>Kotlin</span></span>}>
<MenuItem key={11}>Java</MenuItem>
<MenuItem key={12}>Scala </MenuItem>
<MenuItem key={13}>Groovy</MenuItem>
</SubMenu>
<SubMenu key={"sub2"}
title={<span><i className="kuma-icon kuma-icon-wangwang"/><span>Spring Boot</span></span>}>
<MenuItem key={21}>Spring MVC</MenuItem>
<MenuItem key={22}>WebFlux</MenuItem>
<MenuItem key={23}>Security</MenuItem>
<MenuItem key={23}>JPA</MenuItem>
</SubMenu>
<SubMenu key={"sub3"} title={<span><i className="kuma-icon kuma-icon-wangwang"/><span>React </span></span>}>
<MenuItem key={31}>Node.js</MenuItem>
<MenuItem key={32}>Reflux</MenuItem>
<MenuItem key={33}>ES6</MenuItem>
</SubMenu>
</Menu>
</div>
);
}
}
參考文檔: https://spring.io/guides/tutorials/react-and-spring-data-rest/