React.js 集成 Spring Boot 开发 Web 应用
1. 创建工程
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
导入 IDEA 中
2. 配置数据源 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. 创建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. 实现登陆权限校验
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/**" // 不拦截静态资源
).permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
//.loginPage("/login")// url 请求路径,对应 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( // 此处为了区分我们本地系统中的 User 实体类,特意列出userdetails 的 User 类的全路径
username,
user.password,
authorities
)
}
}
5. 前端使用 React.js 开发: 目录结构
我们使用 nowa:
https://nowa-webpack.github.io/
使用文档:
https://nowa-webpack.github.io/nowa/
PC 前端组件库:
http://uxco.re/components/button/
6. 创建 React 前端工程
前端应用工程目录放到 /Users/jack/KotlinSpringBoot/reakt/src/main/resources 目录下:
前端目录结构如下:
~/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
前端工程应用单独启动:
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 使用的体验两大精华地方,
不需要学习webpack, 整个前端开发环境都集成了. react入门的小白最喜欢了, 我学webpack头大死了,到现在也没有搞明白. 用了nowa,我就不需要搞明白了.
掌握了nowa的脚手架模板, 整个开发效率提升2倍.
如果说react将组件的复用提高到极限,减少了重复代码的工作量. nowa的自定义脚手架,则把项目文件的复用便捷性提高到极限, 以前要复制一组文件,然后修改文件名/组件名..等等.
现在:
用 nowa init mod 创建一组函数组件
用nowa init rmod 创建一组react组件,
用nowa init page 创建自己个性化的一组文件,
用nwoa init api 创建api资源模块,
创建好了,直接可以写业务代码,不需要复制粘贴啥的了. 当然mod rmod page api 这几个都是按项目和自己习惯,定义过的模板.
gui版本,我也体验了一下, 管理项目方便了.不用去文件夹里面翻找了.
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">选项1</Menu.Item>
<Menu.Item key="setting:2">选项2</Menu.Item>
<Menu.Item key="setting:3">选项3</Menu.Item>
<Menu.Item key="setting:4">选项4</Menu.Item>
</SubMenu>
<Menu.Item key="alipay">
<a href="#" target="_blank">关于我们</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/