screenshot_1.png


结合了两位大神的文章,使用 springboot + react + dva + antd 快速实现 CURD 应用,包含后台数据服务,前端查询、编辑、删除、创建以及分页处理,数据 mock ,自动处理 loading 状态等。

1.新建 Spring Boot 项目

我用的是 eclipse,先在 eclipse marketplace 里面安装 Spring Tools ,然后新建 Spring Starter Project。
screenshot_2.png
选择要使用的组件,这里我选了 Mybitis 和 MySQL ,后期要添加其他组件去 pom.xml 添加即可。
screenshot_3.png

2.在 application.properties 文件中添加 Mybitis 和 MySQL 的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ############################################# MySQL 配置 ############################################
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/OMS?useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=123
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# ############################################# MySQL 配置 ############################################

# ############################################# Mybatis 配置 ##########################################
#mybatis.config-location=classpath:mybatis-config.xml

# 不写 mybatis-config.xml 这个配置文件时可以用下面的配置
mybatis.type-aliases-package=com.ethan.oms_springboot.User
mybatis.mapper-locations=classpath:mybatis-mapper/*.xml
# ############################################# Mybatis 配置 ##########################################

3.新建 Class & DAO & Controller

创建完之后项目文件大概长这样
screenshot_4.png
编辑 UserDO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.ethan.oms_springboot.User;

import java.io.Serializable;

public class UserDO implements Serializable {
private static final long serialVersionUID = -7572697046940288333L;

private String id;

private String username;

private String name;

private String permission;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getusername() {
return username;
}

public void setusername(String username) {
this.username = username;
}

public String getname() {
return name;
}

public String getpermission() {
return permission;
}

public void setname(String name) {
this.name = name;
}
}

编辑 UserMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.ethan.oms_springboot.User;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper {
UserDO selectPersonById(Integer id);

List<UserDO> selectAll();

void insert(UserDO userDO);

Long update(UserDO userDO);

Long delete(Long id);
}

编辑 Mybitis 的 UserMapper.xml ,这个文件主要就是MySQL的语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.ethan.oms_springboot.User.UserMapper">

<insert id="insert" parameterType="UserDO" useGeneratedKeys="true" keyProperty="id">
INSERT INTO users(username,name) VALUES(#{username},#{name})
</insert>

<update id="update" parameterType="UserDO">
UPDATE users SET username=#{username},name=#{name} WHERE id=#{id}
</update>

<delete id="delete" parameterType="java.lang.Long">
DELETE FROM users WHERE id=#{id}
</delete>

<select id="selectPersonById" resultType="UserDO">
select * from users where id = #{id}
</select>

<select id="selectAll" resultType="UserDO">
SELECT id, username, name, permission FROM users
</select>

</mapper>

}

编辑 UserController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.ethan.oms_springboot.User;

import com.ethan.oms_springboot.User.UserDO;
import com.ethan.oms_springboot.User.UserMapper;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
@Autowired
private UserMapper userMapper;

@RequestMapping("/save")
public String save() {
UserDO userDO = new UserDO();
userDO.setusername("test101");
userDO.setname("唐伯虎");
userMapper.insert(userDO);
return userDO.getId();
}

@RequestMapping("/update")
public Long update() {
UserDO userDO = new UserDO();
userDO.setId("3");
userDO.setusername("test101");
userDO.setname("唐伯虎");
return userMapper.update(userDO);
}

@RequestMapping("/delete")
public Long delete() {
return userMapper.delete(10L);
}

@RequestMapping("/selectById")
public UserDO selectById() {
return userMapper.selectPersonById(3);
}

@RequestMapping("/users")
public List<UserDO> selectAll() {
return userMapper.selectAll();
}
}

4.运行测试后台

写完以上应该可以通过 http://127.0.0.1:8080 访 MySQL 问数据库了

5.安装 dva-cli 并创建前端应用

先安装 dva-cli,并确保版本是 0.7.x

1
2
3
$ npm i dva-cli@0.7 -g
$ dva -v
0.7.0

然后创建应用(项目路径就是oms)

1
2
$ dva new oms
$ cd oms

6.配置 antd 和 babel-plugin-import

babel-plugin-import 用于引入 antd 的 JavaScript 和 CSS,可以实现按需加载,打包时会小一点

1
2
$ npm i antd --save
$ npm i babel-plugin-import --save-dev

修改 .roadhogrc,在 “extraBabelPlugins” 里加上:

1
["import", { "libraryName": "antd", "style": "css" }]

7.配置代理,能通过 RESTFul 的方式访问 http://localhost:8000/api/users

修改 .roadhogrc,加上 “proxy” 配置:

1
2
3
4
5
6
7
"proxy": {
"/api": {
"target": "http://127.0.0.1:8080",
"changeOrigin": true,
"pathRewrite": { "^/api" : "" }
}
},

然后启动应用:(这个命令一直开着,后面不需要重启,会自动刷新)

1
$ npm start

浏览器会自动打开 http://localhost:8000 ,访问 http://localhost:8000/api/users ,就能访问到 http://127.0.0.1:8080/users 的数据

8.生成 users 路由

用 dva-cli 生成路由:

1
$ dva g route users

然后就可以通过 http://localhost:8000/#/users 代替 http://localhost:8000/api/users

9.构造 users model 和 service

用 dva-cli 生成 Model :

1
$ dva g model users

修改 src/models/users.js :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import * as usersService from '../services/users';

export default {
namespace: 'users',
state: {
list: [],
total: null,
page: null,
},
reducers: {
save(state, { payload: { data: list, total, page } }) {
return { ...state, list, total, page };
},
},
effects: {
*fetch({ payload: { page = 1 } }, { call, put }) {
const { data, headers } = yield call(usersService.fetch, { page });
yield put({
type: 'save',
payload: {
data,
total: parseInt(headers['x-total-count'], 10),
page: parseInt(page, 10),
},
});
},
*remove({ payload: id }, { call, put }) {
yield call(usersService.remove, id);
yield put({ type: 'reload' });
},
*patch({ payload: { id, values } }, { call, put }) {
yield call(usersService.patch, id, values);
yield put({ type: 'reload' });
},
*create({ payload: values }, { call, put }) {
yield call(usersService.create, values);
yield put({ type: 'reload' });
},
*reload(action, { put, select }) {
const page = yield select(state => state.users.page);
yield put({ type: 'fetch', payload: { page } });
},
},
subscriptions: {
setup({ dispatch, history }) {
return history.listen(({ pathname, query }) => {
if (pathname === '/users') {
dispatch({ type: 'fetch', payload: query });
}
});
},
},
};

新增 src/services/users.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import request from '../utils/request';
import { PAGE_SIZE } from '../constants';

export function fetch({ page }) {
return request(`/api/users?_page=${page}&_limit=${PAGE_SIZE}`);
}

export function remove(id) {
return request(`/api/users/${id}`, {
method: 'DELETE',
});
}

export function patch(id, values) {
return request(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify(values),
});
}

export function create(values) {
return request('/api/users', {
method: 'POST',
body: JSON.stringify(values),
});
}

由于我们需要从 response headers 中获取 total users 数量,所以需要改造下 src/utils/request.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import fetch from 'dva/fetch';

function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response;
}

const error = new Error(response.statusText);
error.response = response;
throw error;
}

/**
* Requests a URL, returning a promise.
*
* @param {string} url The URL we want to request
* @param {object} [options] The options we want to pass to "fetch"
* @return {object} An object containing either "data" or "err"
*/
export default async function request(url, options) {
const response = await fetch(url, options);

checkStatus(response);

const data = await response.json();

const ret = {
data,
headers: {},
};

if (response.headers.get('x-total-count')) {
ret.headers['x-total-count'] = response.headers.get('x-total-count');
}

return ret;
}

10.添加界面,展现数据

用 dva-cli 生成 component

1
$ dva g component Users/Users

然后修改生成出来的 src/components/Users/Users.js 和 src/components/Users/Users.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import React from 'react';
import { connect } from 'dva';
import { Table, Pagination, Popconfirm, Button } from 'antd';
import { routerRedux } from 'dva/router';
import styles from './Users.css';
import { PAGE_SIZE } from '../../constants';
import UserModal from './UserModal';

function Users({ dispatch, list: dataSource, loading, total, page: current }) {
function deleteHandler(id) {
dispatch({
type: 'users/remove',
payload: id,
});
}

function pageChangeHandler(page) {
dispatch(routerRedux.push({
pathname: '/users',
query: { page },
}));
}

function editHandler(id, values) {
dispatch({
type: 'users/patch',
payload: { id, values },
});
}
function createHandler(values) {
dispatch({
type: 'users/create',
payload: values,
});
}


const columns = [
{
title: '用户名',
dataIndex: 'username',
key: 'username',
},
{
title: '姓名',
dataIndex: 'name',
key: 'name',
render: text => <a href="">{text}</a>,
},
{
title: '权限',
dataIndex: 'permission',
key: 'permission',
},
{
title: '操作',
key: 'operation',
render: (text, record) => (
<span className={styles.operation}>
<UserModal record={record} onOk={editHandler.bind(null, record.id)}>
<a>编辑</a>
</UserModal>
<Popconfirm title="确定删除?" onConfirm={deleteHandler.bind(null, record.id)}>
<a href="">删除</a>
</Popconfirm>
</span>
),
},
];

return (
<div className={styles.normal}>
<div>
<div className={styles.create}>
<UserModal record={{}} onOk={createHandler}>
<Button type="primary">Create User</Button>
</UserModal>
</div>
<Table
columns={columns}
dataSource={dataSource}
loading={loading}
rowKey={record => record.id}
pagination={false}
/>
<Pagination
className="ant-table-pagination"
total={total}
current={current}
pageSize={PAGE_SIZE}
onChange={pageChangeHandler}
/>
</div>
</div>
);
}

function mapStateToProps(state) {
const { list, total, page } = state.users;
return {
loading: state.loading.models.users,
list,
total,
page,
};
}

export default connect(mapStateToProps)(Users);

在 src/routes/Users.js 中引用他

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import { connect } from 'dva';
import styles from './Users.css';
import UsersComponent from '../components/Users/Users';
import MainLayout from '../components/MainLayout/MainLayout';

function Users({ location }) {
return (
<MainLayout location={location}>
<div className={styles.normal}>
<UsersComponent />
</div>
</MainLayout>
);
}

export default connect()(Users);

需留意两件事:
1.对 model 进行了微调,加入了 page 表示当前页
2.由于 components 和 services 中都用到了 pageSize,所以提取到 src/constants.js
改完后,切换到浏览器,应该能看到带分页的用户列表。

11.添加 layout

添加 layout 布局,使得我们可以在首页和用户列表页之间来回切换
添加布局,src/components/MainLayout/MainLayout.js 和 CSS 文件
在 src/routes 文件夹下的文件中引用这个布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react';
import styles from './MainLayout.css';
import Header from './Header';

function MainLayout({ children, location }) {
return (
<div className={styles.normal}>
<Header location={location} />
<div className={styles.content}>
<div className={styles.main}>
{children}
</div>
</div>
</div>
);
}

export default MainLayout;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.normal {
display: flex;
flex-direction: column;
height: 100%;
}

.content {
flex: 1;
display: flex;
}

.main {
padding: 0 8px;
flex: 1 0 auto;
}

这时页面的菜单会随着页面切换变化,高亮显示当前页所在的菜单项

12.通过 dva-loading 处理 loading 状态

dva 有一个管理 effects 执行的 hook,并基于此封装了 dva-loading 插件
通过这个插件,不必一遍遍地写 showLoading 和 hideLoading,当发起请求时,插件会自动设置数据里的 loading 状态为 true 或 false ,然后我们在渲染 components 时绑定并根据这个数据进行渲染
先安装 dva-loading

1
$ npm i dva-loading --save

修改 src/index.js 加载插件,在合适的地方加入下面两句

1
2
import createLoading from 'dva-loading';
app.use(createLoading());

然后在 src/components/Users/Users.js 里绑定 loading 数据:

1
loading: state.loading.models.users,

13.处理分页

只改一个文件 src/components/Users/Users.js 就好
处理分页有两个思路:
1.发 action,请求新的分页数据,保存到 model,然后自动更新页面
2.切换路由 (由于之前监听了路由变化,所以后续的事情会自动处理)
我们用的是思路 2 的方式,好处是用户可以直接访问到 page 2 或其他页面
注:第10步的文件为已修改的代码

14.处理用户删除

功能调整基本都可以按照修改以下三个地方进行
1.service
2.model
3.component
service, 修改 src/services/users.js

1
2
3
4
5
export function remove(id) {
return request(`/api/users/${id}`, {
method: 'DELETE',
});
}

model, 修改 src/models/users.js

1
2
3
4
5
*remove({ payload: id }, { call, put, select }) {
yield call(usersService.remove, id);
const page = yield select(state => state.users.page);
yield put({ type: 'fetch', payload: { page } });
},

component, 修改 src/components/Users/Users.js,替换 deleteHandler 内容

1
2
3
4
dispatch({
type: 'users/remove',
payload: id,
});

15.处理用户编辑

处理用户编辑和前面的一样,遵循三步走:
先是 service,修改 src/services/users.js

1
2
3
4
5
6
export function patch(id, values) {
return request(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify(values),
});
}

再是 model,修改 src/models/users.js

1
2
3
4
5
*patch({ payload: { id, values } }, { call, put, select }) {
yield call(usersService.patch, id, values);
const page = yield select(state => state.users.page);
yield put({ type: 'fetch', payload: { page } });
},

最后是 component,详见第10步的代码

需要注意的一点是,我们在这里如何处理 Modal 的 visible 状态,有几种选择:
1.存 dva 的 model state 里
2.存 component state 里
另外,怎么存也是个问题,可以:
1.只有一个 visible,然后根据用户点选的 user 填不同的表单数据
2.几个 user 几个 visible
此教程选的方案是 2-2,即存 component state,并且 visible 按 user 存,另外为了使用的简便,封装了一个 UserModal 的组件

到这里,一个完整的 CURD 应用就完成了,后续后需要完善处理错误、请求、超时,根据路由动态加载 JS 和 CSS 等问题