React.js 集成 Spring Boot 开发 Web 应用

React.js 集成 Spring Boot 开发 Web 应用

1. 创建工程

image.png
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 中

image.png

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 表

image.png
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/

image.png

6. 创建 React 前端工程

前端应用工程目录放到 /Users/jack/KotlinSpringBoot/reakt/src/main/resources 目录下:

image.png

前端目录结构如下:

image.png
~/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 的样板示例工程:

image.png

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. 前后端目录集成

image.png
image.png

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/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,468评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,620评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,427评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,160评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,197评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,334评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,775评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,444评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,628评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,459评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,508评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,210评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,767评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,850评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,076评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,627评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,196评论 2 341

推荐阅读更多精彩内容