Ant Design Pro 实战
概述
Ant Design Pro 是蚂蚁金服出口的一个企业级中后台前端/设计解决方案,基于Ant Design React组件和umi框架。用于快速开发后台管理的前端。
为了不增加阅读难度,本文不讲umi、dva的原理,只讲简单的东西,基本操作,先做什么,再做什么,然后做什么,最后做什么。脚手架已经为我们生成了整项目的架构,我们只关心我们业务相关的内容就行了,先按步骤快速上手,等上手后,如果想知道更多原理,再去查看相关的文档。
此次实战环境:
- node v10.16.0
- npm 6.14.4
- Google Chrome 81.0.4044.122(正式版本) (64 位)
关键组件版本
- umi 3.1.1
- antd ^4.0.0
- ant design pro 4.x
代码已经提交到github,https://github.com/garrett12138/todo-list,参考说明档使用代码。
本文通过一个todo list的小项目进行演示,只用三个功能:
- 列表功能,列出所有todo项
- 添加新项,添加新的todo项
- 更新项,更新现有的todo项状态,修改成待办、已完成或者取消
数据结构:
{
id:1, //唯一标识
title:"完成Ant Design Pro 实战",//待办事项标题
status:0 //状态:0=待办,1=已完成,2=已取消
}
创建新项目
本地环境需要安装 yarn、node 和 git 环境,如果不懂得安装网上很多文章有介绍,自行百度。
新建项目目录
mkdir todo-list
cd todo-list
执行
yarn create umi
或
npm create umi
? Select the boilerplate type (Use arrow keys)
> ant-design-pro - Create project with a layout-only ant-design-pro boilerplate, use together with umi block.
app - Create project with a simple boilerplate, support typescript.
block - Create a umi block.
library - Create a library with umi.
plugin - Create a umi plugin.
选择 ant-design-pro 回车
? Which language do you want to use? (Use arrow keys)
TypeScript
>JavaScript
选择javascript回车
? Do you need all the blocks or a simple scaffold? (Use arrow keys)
> simple
complete
选择simple回车
? Time to use better, faster and latest antd@4! (Y/n)
我们要使用最新版的antd组件,直接回车, 脚手架将会自动安装需要的文件。
目录结构
├── config # umi 配置,包含路由,构建等配置
├── mock # 本地模拟数据
├── public # 一些公共静态资源
├── src
│ ├── assets # 本地静态资源
│ ├── components # 业务通用组件
│ ├── e2e # 集成测试用例
│ ├── layouts # 通用布局
│ ├── models # 全局 dva model
│ ├── pages # 业务页面入口和常用模板
│ ├── services # 后台接口服务
│ ├── utils # 工具库
│ ├── locales # 国际化资源
│ ├── global.less # 全局样式
│ └── global.jsx # 全局 JS
├── tests # 测试工具
├── README.md
└── package.json
期中,mock、models、pages、services将是我们的主要工作目录。
本地开发
安装依赖
npm install
安装完后运行
npm start
App running at:
- Local: http://localhost:8000 (copied to clipboard)
- Network: http://192.168.56.101:8000
浏览器打开http://localhost:8000看到如下界面
这就是脚手架为我们建好的框架,一个欢迎页面,一个空白的二级页面和一个查询表格,实际开发中我们要删除掉这些页面,然后建自己的业务界面,这里我们就不删了,在上面添加我们需要的页面就行。
添加新功能
添加新功能我们主要的工作就是上面提到的mock、models、pages、services四个目录,其它虽然也有涉及,但频率相对低很多,这里就不讲了,看官方文档即可。
本项目只有三个功能:
- 列表功能,列出所有todo项
- 添加新项,添加新的todo项
- 更新项,更新现有的todo项状态,修改成待办、已完成或者取消
只要一查询表格页面就行,添加和修改都在同一页面完成。
步骤如下(实际上步骤可以不按这个顺序):
配置路由
...
routes: [
{
path: '/',
component: '../layouts/BasicLayout',
authority: ['admin', 'user'],
routes: [
{
path: '/',
redirect: '/welcome',
},
{
path: '/todo',//url中path部分,http://localhost:8000/todo状态跌幅到这个页面
name: 'todo', //名称,国际化菜单配置根据这个名称来配置,如果不配置将直菜单将直接显示这个名称
icon: 'unordered-list',//菜单图标名称
component:'./todo'// 组件(页面)相对于src/pages的路径
},
...
],
...
保存,这时会报错,因为src/todo相关的页面,我之所以习惯按这个顺序来操作是因为,不管我做了什么都可以立即得到反馈,出错了马上报错,不出错可以马上看到结果,我喜欢这种即时反馈。
添加空白页面
在src/pages下新建目录todo,在todo目录下新建空白index.jsx文件,代码如下:
import React, { Component } from 'react';
import { PageHeaderWrapper } from '@ant-design/pro-layout';
class TodoPage extends Component {
componentWillMount() { }
render() {
return (
<PageHeaderWrapper>
<div>空白面</div>
</PageHeaderWrapper>);
}
}
export default TodoPage
我习惯也建议使用组件开发的方式,这样代码结构比较清晰。
项目中文件的修改、增删都会触发重新编译,浏览器上就可以马上看到效果,如果因为出错冲断了自动重新编译,请重新npm start.
此时,左边菜单多出了一个todo的菜单,点击可以打开我们刚建好的空白页面,如下图
修改国际化资源
打开src/locales/zh-CN/menu.js,添加 'menu.'+路由对象中的name为键,值为'待办事项'的键值对,如下
export default {
...
'menu.todo': '待办事项',
};
保存,重新编译后,菜单名称,面包屑、页面标题都更新了,如下图:
本项目中只用到中文,如果使用到其它语言,按照这个方法分别修改src/locales下的其它文件夹下的menu.js。
修改空白页面
修改刚才建好的空白页面,在本页面添加模拟数据,和查询表格,事件处理等。组件的使用参考官司例子和组件文档。
import React, { Component } from 'react';
import { PlusOutlined } from '@ant-design/icons';
import ProTable from '@ant-design/pro-table';
import { PageHeaderWrapper } from '@ant-design/pro-layout';
import { Button, Divider, Alert, Modal } from 'antd';
const status = [
<Alert message="待办" type="info" showIcon={false} />,
<Alert message="已完成" type="success" showIcon />,
<Alert message="已取消" type="error" showIcon />];
class TodoPage extends Component {
state = {
modalVisible: false,
todoList: [
{
"id": 8,
"title": "完成Antd-Pro-Generator手动添加接口并生成代码",
"status": 0
},
{
"id": 7,
"title": "修改Antd-Pro-Generator UI",
"status": 0
},
{
"id": 6,
"title": "完善Antd-Pro-Generator数据类型定义",
"status": 0
},
{
"id": 5,
"title": "文章使用Antd-Pro-Generator生成代码",
"status": 1
},
{
"id": 4,
"title": "Antd-Pro-Generator支持TypeScript",
"status": 2
},
{
"id": 3,
"title": "发布Antd-Pro-Generator vscode 插件",
"status": 1
},
{
"id": 2,
"title": "文章Ant Design Pro 快速入门",
"status": 1
},
{
"id": 1,
"title": "文章React快速入门",
"status": 1
}
]
}
componentDidMount() {
}
handelSubmit = async (values) => {
const { todoList } = this.state;
const item = { id: todoList.length + 1, title: values.title, status: 0 };
this.setState({ todoList: [item, ...todoList] })
}
handleModalVisible(visible) {
this.setState({ modalVisible: visible });
}
updateStatus(item, _status) {
const { todoList } = this.state;
for (let i = 0; i < todoList.length; i += 1) {
if (todoList[i].id === item.id) {
todoList[i].status = _status;
break;
}
}
this.setState({ todoList: [...todoList] });
}
render() {
const { todoList, modalVisible } = this.state;
const columns = [
{
title: 'id',
dataIndex: 'id',
hideInForm: true,
},
{
title: '标题',
dataIndex: 'title',
rules: [
{
required: true,
message: '待办事项标题不能为空',
},
]
},
{
title: '状态',
dataIndex: 'status',
hideInForm: true,
render: val => status[val]
},
{
title: '修改状态',
hideInForm: true,
render: (_, record) => {
const operations = [];
if (record.status !== 0) {
operations.push(<a key='normal'
onClick={() => this.updateStatus(record, 0)} > 待办</a>);
}
if (record.status !== 1) {
if (operations.length > 0) {
operations.push(<Divider key='done-divider' type="vertical" />);
}
operations.push(<a key='done'
onClick={() => this.updateStatus(record, 1)} > 完成</a>);
}
if (record.status !== 2) {
if (operations.length > 0) {
operations.push(<Divider key='canceled-divider' type="vertical" />);
}
operations.push(<a key='canceled'
onClick={() => this.updateStatus(record, 2)} > 取消</a>);
}
return (<>{operations}</>)
},
},
]
return (
<PageHeaderWrapper>
<ProTable
headerTitle="待办事项列表"
rowKey="id"
toolBarRender={() => [
<Button type="primary" onClick={() => this.handleModalVisible(true)}>
<PlusOutlined /> 新建
</Button>,
]}
search={false}
dataSource={todoList}
columns={columns}
pagination={false}
rowSelection={false}
expandable={false}
/>
<Modal
destroyOnClose
title="新建待办事项"
visible={modalVisible}
onCancel={() => this.handleModalVisible(false)}
footer={null}
>
<ProTable
onSubmit={async values => {
await this.handelSubmit(values);
this.handleModalVisible(false);
}}
rowKey="key"
type="form"
columns={columns}
/>
</Modal>
</PageHeaderWrapper>);
}
}
export default TodoPage
添加后端接口访问函数
在src/services目录下新建todo.js文件,添加更新、添加、获取列表函数代码:
/**
* api client for todo}
*/
import request from '@/utils/request';
/**
* @summary 更新项
* @description 更新item项,如果找不到相应id的项则返回错误
* @param {*} item 参数 {id,title,status}
*/
export async function updateItem(item) {
const options = {
method: 'PUT'
};
const url = '/item';
options.data = item;
return request(url, options);
}
/**
* @summary 添加新项
* @description 添加新的项目到todo列表中
* @param {*} item 参数 {id,title,status}
*/
export async function addItem(item) {
const options = {
method: 'POST'
};
const url = '/item';
options.data = item;
return request(url, options);
}
/**
* @summary 获取所有todo项
* @description 获取所有todo项
*/
export async function getAll() {
const options = {
method: 'GET'
};
const url = '/items';
return request(url, options);
}
添加mock代码
使用mock模拟后端服务器,前后端分离同步开发时在没有后端服务器的情况下,可以本地开发和调试。
在mock文件下新建todo.js,添加代码
export default {
'PUT /item': (req, res) => {
// PUT 是http的method,GET,POST,PUT,DELETE等
// /item是接口相对于服务器地址的url
// req 是request对象,http请求的信息包括头部,参数什么的,req.body可以取到传到服务端json,
// 同样通过req.query,req.params获取其它参数
// res 是response对象,http请求返回信息包括头部cookie,json数据什么的
// res.send()是返回客户端的数据,这里直接返回 {code: 0, message: '操作成功', body: true, }
// res.send(200) 返回200的状态码,不返回其它数据
const result = {
code: 0,
message: '操作成功',
body: true,
};
res.send(result);
},
'POST /item': (req, res) => {
const result = {
code: 0,
message: '操作成功',
body: true,
};
res.send(result);
},
'GET /items': (req, res) => {
const result = {
code: 0,
message: '操作成功',
body: [
{
id: 8,
title: '完成Antd-Pro-Generator手动添加接口并生成代码',
status: 0,
},
{
id: 7,
title: '修改Antd-Pro-Generator UI',
status: 0,
},
{
id: 6,
title: '完善Antd-Pro-Generator数据类型定义',
status: 0,
},
{
id: 5,
title: '文章使用Antd-Pro-Generator生成代码',
status: 1,
},
{
id: 4,
title: 'Antd-Pro-Generator支持TypeScript',
status: 2,
},
{
id: 3,
title: '发布Antd-Pro-Generator vscode 插件',
status: 1,
},
{
id: 2,
title: '文章Ant Design Pro 快速入门',
status: 1,
},
{
id: 1,
title: '文章React快速入门',
status: 1,
},
],
};
res.send(result);
},
};
直接返回数据,没有逻辑,如果想模拟得逼真一点,适当加点逻辑。
添加model
react组件、页面中的state它只能在本组件、页面的范围内使用,其它页面是用不到的,虽然可以使用props的方式传子组件,但是有些组件,页面并不存在父子关系,所以数据在各组件、页面中共享很麻烦。model就是为这个而生的。本示例中,todoList 在todo页面用到了,全局顶部的头像下拉菜单也用到了,这种情况适合用model。
在src/models目录下新建todo.js文件
添加代码
import { getAll } from '@/services/todo';
import { message } from 'antd';
export default {
namespace: 'todo',
state: {
todoList: [],
},
effects: {
*fetchTodoList({ payload }, { call, put }) {
const response = yield call(getAll, payload);
if (response.code === 0) {
yield put({
type: 'setTodoList',
payload: response.body,
});
} else {
message.error(response.message);
yield put({
type: 'setTodoList',
payload: [],
});
}
},
},
reducers: {
setTodoList(state, action) {
return {
...state,
todoList: action.payload,
};
},
},
};
model的结构是这样的
export default {
namespace: 'todo',//model中的effect和reducer要通过命名空间调用,不要和其他model同名
state: { }, //state,跟react组件中的state差不多一个意思
effects: { },//effect通常是调用服务端接口,后调用reducer更新state
reducers: { } //state通过reducer来更新
}
修改页面
去掉页面中state的模拟数据todoList,使用 todo model 中的todoList,修改各事件处理调用api刷新列表。
import React, { Component } from 'react';
import { PlusOutlined } from '@ant-design/icons';
import ProTable from '@ant-design/pro-table';
import { PageHeaderWrapper } from '@ant-design/pro-layout';
import { Button, Divider, Alert, Modal, message } from 'antd';
import { connect } from 'umi';
import { addItem, updateItem } from '@/services/todo'
const status = [
<Alert message="待办" type="info" showIcon={false} />,
<Alert message="已完成" type="success" showIcon />,
<Alert message="已取消" type="error" showIcon />];
class TodoPage extends Component {
state = {
modalVisible: false,
}
componentDidMount() {
this.loadData();
}
handelSubmit = async (values) => {
const item = { title: values.title, status: 0 };
//调用todo service的addItem添加待办事项
const rsp = await addItem(item);
if (rsp.code === 0) {
message.success('添加成功!');
this.loadData();
} else {
message.error(rsp.message);
}
}
updateStatus = async (item, _status) => {
//调用todo service的updateItem更新待办事项
const rsp = await updateItem({ ...item, status: _status });
if (rsp.code === 0) {
message.success('修改成功!');
this.loadData();
} else {
message.error(rsp.message);
}
}
//获取待办事项列表
loadData() {
//使用connect后,dispatch通过props传给了组件
const { dispatch } = this.props;
dispatch({ type: 'todo/fetchTodoList', payload: null });
}
handleModalVisible(visible) {
this.setState({ modalVisible: visible });
}
render() {
const { todo } = this.props;
const { todoList } = todo;
const { modalVisible } = this.state;
const columns = [
{
title: 'id',
dataIndex: 'id',
hideInForm: true,
},
{
title: '标题',
dataIndex: 'title',
rules: [
{
required: true,
message: '待办事项标题不能为空',
},
]
},
{
title: '状态',
dataIndex: 'status',
hideInForm: true,
render: val => status[val]
},
{
title: '修改状态',
hideInForm: true,
render: (_, record) => {
const operations = [];
if (record.status !== 0) {
operations.push(<a key='normal'
onClick={() => this.updateStatus(record, 0)} > 待办</a>);
}
if (record.status !== 1) {
if (operations.length > 0) {
operations.push(<Divider key='done-divider' type="vertical" />);
}
operations.push(<a key='done'
onClick={() => this.updateStatus(record, 1)} > 完成</a>);
}
if (record.status !== 2) {
if (operations.length > 0) {
operations.push(<Divider key='canceled-divider' type="vertical" />);
}
operations.push(<a key='canceled'
onClick={() => this.updateStatus(record, 2)} > 取消</a>);
}
return (
<>
{operations}
</>
)
},
},
]
return (
<PageHeaderWrapper>
<ProTable
headerTitle="待办事项列表"
rowKey="id"
toolBarRender={() => [
<Button type="primary" onClick={() => this.handleModalVisible(true)}>
<PlusOutlined /> 新建
</Button>,
]}
search={false}
dataSource={todoList}
columns={columns}
pagination={false}
rowSelection={false}
expandable={false}
/>
<Modal
destroyOnClose
title="新建待办事项"
visible={modalVisible}
onCancel={() => this.handleModalVisible(false)}
footer={null}
>
<ProTable
onSubmit={async values => {
await this.handelSubmit(values);
this.handleModalVisible(false);
}}
rowKey="key"
type="form"
columns={columns}
/>
</Modal>
</PageHeaderWrapper>);
}
}
//使用umi的connect方法把命名空间为todo的model的数据通过props传给页面
export default connect(({ todo }) => ({ todo }))(TodoPage);
这时,虽然修改状态提示成功和添加成功,但列表数据不会有任何变化,这是因为我们的mock没有逻辑,获取列表什么时候返回的都是同样的数据,所以不所怎么操作,返回的数据都是一样不变的。其实,开发过程中客户端中没有什么逻辑,只要获取、提交数据的参数对了,对结果的判断处理正确了就行了,逻辑都是后端完成的。如果想模拟得真的一点,给mock加上逻辑。
修改mock逻辑
let list = [
{
id: 8,
title: '完成Antd-Pro-Generator手动添加接口并生成代码',
status: 0,
},
{
id: 7,
title: '修改Antd-Pro-Generator UI',
status: 0,
},
{
id: 6,
title: '完善Antd-Pro-Generator数据类型定义',
status: 0,
},
{
id: 5,
title: '文章使用Antd-Pro-Generator生成代码',
status: 1,
},
{
id: 4,
title: 'Antd-Pro-Generator支持TypeScript',
status: 2,
},
{
id: 3,
title: '发布Antd-Pro-Generator vscode 插件',
status: 1,
},
{
id: 2,
title: '文章Ant Design Pro 快速入门',
status: 1,
},
{
id: 1,
title: '文章React快速入门',
status: 1,
},
];
export default {
'PUT /item': (req, res) => {
let result;
const item = req.body;
for (let i = 0; i < list.length; i += 1) {
if (item.id === list[i].id) {
list[i] = item;
result = {
code: 0,
message: '操作成功',
body: true,
};
res.send(result);
return;
}
}
result = {
code: 404,
message: '待办事项不存在',
body: true,
};
res.send(result);
},
'POST /item': (req, res) => {
const item = { ...req.body, id: list.length + 1 };
list = [item, ...item];
const result = {
code: 0,
message: '操作成功',
body: true,
};
res.send(result);
},
'GET /items': (req, res) => {
const result = {
code: 0,
message: '操作成功',
body: list,
};
res.send(result);
},
};
修改顶部头像下拉菜单
修改头像下拉菜单
打开src/components/GlobalHeader/AvatarDropdown.jsx,修改代码,头像下拉菜单增加一个待办事项菜单,并加上徽标Badge当有待办事项时显示数量,同时用记名称也加上徽标,当有待办事项上显示红点,代码如下:
import { LogoutOutlined, SettingOutlined, UserOutlined, UnorderedListOutlined } from '@ant-design/icons';
import { Avatar, Menu, Spin, Badge } from 'antd';
import React from 'react';
import { history, connect } from 'umi';
import HeaderDropdown from '../HeaderDropdown';
import styles from './index.less';
class AvatarDropdown extends React.Component {
onMenuClick = (event) => {
const { key } = event;
if (key === 'logout') {
const { dispatch } = this.props;
if (dispatch) {
dispatch({
type: 'login/logout',
});
}
return;
}
//--------------------------------------
//点击待办事项菜单时跳转到todo页面
if (key === 'todo') {
history.push(`/todo`);
return;
}
//--------------------------------------
history.push(`/account/${key}`);
};
render() {
const {
currentUser = {
avatar: '',
name: '',
},
menu,
todo: { todoList }
} = this.props;
//----------------------------------------------------------------
//计算待办事项数量
const todoNum = todoList.filter(item => item.status === 0).length;
//----------------------------------------------------------------
const menuHeaderDropdown = (
<Menu className={styles.menu} selectedKeys={[]} onClick={this.onMenuClick}>
{menu && (
<Menu.Item key="center">
<UserOutlined />
个人中心
</Menu.Item>
)}
{menu && (
<Menu.Item key="settings">
<SettingOutlined />
个人设置
</Menu.Item>
)}
{menu && <Menu.Divider />}
<!---增加待办事项菜单------------------------------------------>
<Menu.Item key="todo">
<UnorderedListOutlined />
<Badge offset={[13, 8]} count={todoNum}>待办事项</Badge>
</Menu.Item>
<!---------------------------------------------------------->
<Menu.Item key="logout">
<LogoutOutlined />
退出登录
</Menu.Item>
</Menu>
);
return currentUser && currentUser.name ? (
<HeaderDropdown overlay={menuHeaderDropdown}>
<span className={`${styles.action} ${styles.account}`}>
<Avatar size="small" className={styles.avatar} src={currentUser.avatar} alt="avatar" />
<span className={styles.name}>
<!---用户名增加徽标---------------------------------------->
<Badge dot={true} count={todoNum}>{currentUser.name}</Badge>
</span>
</span>
</HeaderDropdown>
) : (
<span className={`${styles.action} ${styles.account}`}>
<Spin
size="small"
style={{
marginLeft: 8,
marginRight: 8,
}}
/>
</span>
);
}
}
//----------------------------------------
//将命名空间为todo的model 通过props传给组件
export default connect(({ user, todo }) => ({
currentUser: user.currentUser,
todo,
}))(AvatarDropdown);
这个时,如果刚加载整个页面,显示欢迎页面,待办事项都是0,因为我们还没有打开todo页面,还没有拉取待办事项列表,要在页面加载时就拉取待办事项列表,要在基本布局组件加载时拉取数据。
修改布局组件,加载时获取待办列表
打开srclayoutsBasicLayout.jsx文件,增加拉取待办事项代码:
...
useEffect(() => {
if (dispatch) {
dispatch({
type: 'user/fetchCurrent',
});
//---------------------------------
//调用todo model 的fetchTodoList effect
dispatch({
type: 'todo/fetchTodoList',
});
//---------------------------------
}
}, []);
...
数据流程
此时todo model的数据connect两个地方,一个是AvatarDropdown,一个是TodoPage,打开todo页面,更新状态或者添加新的待办事项时,代码中都重新加载待办事项列表,数据流程如下:
- 调用加载列表函数 -->this.loadData
- loadData 调用todo model的effect fetchTodoList--> dispatch(type:'todo/fetchTodoList',payload:null)
- todo model的effect fetchTodoList 调用todo service的getAll函数 得到返回数据 response ---> const response = yield call(getAll, payload),再调用reducer setTotoList 更新model的state--> yield put({ type: 'setTodoList', payload: response.body})
- AvatarDropdown和TodoPage同时得知model的state有更新,各自调用render刷新自身,这样待力事项列表和头像下拉菜单同时显示变化。
结束
todo list这个项目到此就完成了,ant design pro 使用了umi,dva(model),还有一些es6的特性(effect 中的星 * ,yield),这些刚开始如果看不懂,可以先照着做,上手后再慢慢去查看相关的文档吧。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。