Sword开发手册

Download as pdf or txt
Download as pdf or txt
You are on page 1of 170



快速开始
环境要求
环境准备
工程导入
工程运行
技术基础
React
ES6
DvaJS
UmiJS
AntDesign
开发初探
第一个页面
Ant Design组件
自定义组件
Mock数据
API调用
API结合Mock加载组件
State 状态机
第一个CRUD
模块准备
列表页
加载数据
查询功能
按钮加载原理
自定义按钮
新增页
修改页
详情页
开发进阶
API反向代理
Dva数据流
基于数据流改造CRUD
数据流准备
数据流列表页
数据流新增页
数据流修改页
数据流详情页

本文档使用 看云 构建 - 2 -
构建和发布
项目构建
项目发布

本文档使用 看云 构建 - 3 -


Sword简介

Sword是SpringBlade前端UI框架,主要选型技术为React、Ant Design、Umi、Dva。
本手册主要讲解如何在Sword平台下开发业务模块,同时也是React、Ant Design入门的绝佳选择。

SpringBlade简介

SpringBlade 是由一个商业级项目升级优化而来的SpringCloud微服务架构,采用Java8 API重构了业务代


码,完全遵循阿里巴巴编码规范。
采用Spring Boot 2 、Spring Cloud Finchley 、Mybatis 等核心技术,同时提供基于React和Vue的两个前
端框架用于快速搭建企业级的微服务系统平台。

SpringBlade 致力于创造新颖的开发模式,将开发中遇到的痛点、生产中所踩的坑整理归纳,并将解决方案
都融合到框架中。

说明

本手册主要讲解前端技术栈,还需要后端的同学可移步:https://www.kancloud.cn/smallchill/blade

官网

官网地址: BladeX

项目地址

项目地址:SpringBlade
前端UI项目地址(基于React):Sword
前端UI项目地址(基于Vuet):Saber
核心框架项目地址:BladeTool
交流群: 477853168

主要特性

采用前后端分离的模式,前端开源出一个框架:Sword,主要选型技术为React、Ant Design、Umi、Dva
采用前后端分离的模式,前端开源出一个框架:Saber,主要选型技术为Vue、Vuex、Avue、Element-UI
后端采用SpringCloud全家桶,并同时对其基础组件做了高度的封装,单独开源出一个框架:BladeTool
BladeTool已推送至Maven中央库,直接引入即可,减少了工程的臃肿,也可更注重于业务开发
注册中心选型Consul
部署使用Docker或K8s + Jenkins

本文档使用 看云 构建 - 4 -

使用Traefik进行反向代理
踩了踩Kong的坑,有个基本的使用方案,但不深入,因为涉及到OpenResty。
封装了简单的Secure模块,采用JWT做Token认证,可拓展集成Redis等细颗粒度控制方案

已经稳定生产了近一年,经历了从Camden -> Finchley的技术架构,也经历了从fat jar -> docker -> k8s


+ jenkins的部署架构
项目分包明确,规范微服务的开发模式,使包与包之间的分工清晰。

工程结构

SpringBlade
├── blade-auth -- 授权服务提供
├── blade-common -- 常用工具封装包
├── blade-gateway -- Spring Cloud 网关
├── blade-ops -- 运维中心
├ ├── blade-admin -- spring-cloud后台管理
├ └── blade-develop -- 代码生成
├── blade-service -- 业务模块
├ ├── blade-desk -- 工作台模块
├ ├── blade-log -- 日志模块
├ ├── blade-system -- 系统模块
├ └── blade-user -- 用户模块
├── blade-service-api -- 业务模块api封装
├ ├── blade-desk-api -- 工作台api
├ ├── blade-dict-api -- 字典api
├ ├── blade-system-api -- 系统api
└── └── blade-user-api -- 用户api

blade-tool
├── blade-core-boot -- 业务包综合模块
├── blade-core-launch -- 基础启动模块
├── blade-core-log -- 日志封装模块
├── blade-core-mybatis -- mybatis拓展封装模块
├── blade-core-secure -- 安全模块
├── blade-core-swagger -- swagger拓展封装模块
└── blade-core-tool -- 工具包模块

界面一览

本文档使用 看云 构建 - 5 -

本文档使用 看云 构建 - 6 -

本文档使用 看云 构建 - 7 -

本文档使用 看云 构建 - 8 -

本文档使用 看云 构建 - 9 -

本文档使用 看云 构建 - 10 -

本文档使用 看云 构建 - 11 -

本文档使用 看云 构建 - 12 -

本文档使用 看云 构建 - 13 -

本文档使用 看云 构建 - 14 -

本文档使用 看云 构建 - 15 -
快速开始

快速开始
环境要求
环境准备

工程导入
工程运行

本文档使用 看云 构建 - 16 -
环境要求

环境要求
基础开发环境

NodeJs: 10.15.0+
Npm: 5.6.0+

推荐IDE

Visual Studio Code


IntelliJ WebStorm (本文将以此ide进行演示操作)

本文档使用 看云 构建 - 17 -
环境准备

环境准备
基础环境安装

本文适合有一定基础的小伙伴,所以windows和mac下的nodejs默认您已有能力安装

本文档使用 看云 构建 - 18 -
工程导入

工程导入
复制git地址

1. 进入 Sword 项目首页:https://gitee.com/smallc/Sword
2. 复制 Sword 的 git 地址

3. 进入对应目录后克隆代码(windows可以用git bash客户端)

本文档使用 看云 构建 - 19 -
工程导入

安装工程

1. 进入Sword目录并执行npm install直到结束

2. 若出现提示升级npm版本则全局执行一下

本文档使用 看云 构建 - 20 -
工程导入

3. 执行npm升级

本文档使用 看云 构建 - 21 -
工程导入

导入工程

1. 打开WebStorm,点击File选择Open

2. 找到对应目录的工程,并打开

本文档使用 看云 构建 - 22 -
工程导入

3. 看到如下界面则说明导入成功

本文档使用 看云 构建 - 23 -
工程导入

本文档使用 看云 构建 - 24 -
工程导入

本文档使用 看云 构建 - 25 -
工程运行

工程运行
前言

Sword支持mock模式与server模式,但是如果启用server模式需要同时启动后台工程,这个比较麻烦不利于我们
初学,所以会以mock模式讲解,照顾到没有后端基础的同学

命令行运行

1. 进入Sword目录,执行 npm start

2. 运行完毕后浏览器会自动打开,看到如下界面则说明运行成功

本文档使用 看云 构建 - 26 -
工程运行

IDE运行

1. 点击左下角的npm,然后双击 start ,等待片刻后,系统也会运行成功

本文档使用 看云 构建 - 27 -
工程运行

本文档使用 看云 构建 - 28 -
工程运行

本文档使用 看云 构建 - 29 -
技术基础

技术基础
React
ES6

DvaJS
UmiJS
AntDesign

本文档使用 看云 构建 - 30 -
React

React
简介

React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC 框架,都不满意,就决定自己


写一套,用来架设 Instagram 的网站。做出来以后,发现这套东西很好用,就在2013年5月开源了。由于 React
的设计思想极其独特,属于革命性创新,性能出众,代码逻辑却非常简单。所以,越来越多的人开始关注和使
用,认为它可能是将来 Web 开发的主流工具。

学习资料

React入门 :https://segmentfault.com/a/1190000012921279

本文档使用 看云 构建 - 31 -
ES6

ES6
简介

ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目


标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

学习资料

ECMAScript 6 入门 : http://es6.ruanyifeng.com/#README

本文档使用 看云 构建 - 32 -
DvaJS

DvaJS
简介

dva 首先是一个基于redux和redux-saga的数据流方案,然后为了简化开发体验,dva 还额外内置了react-


router和fetch,所以也可以理解为一个轻量级的应用框架。

特性

易学易用,仅有 6 个 api,对 redux 用户尤其友好,配合 umi 使用后更是降低为 0 API


elm 概念,通过 reducers, effects 和 subscriptions 组织 model
插件机制,比如dva-loading可以自动处理 loading 状态,不用一遍遍地写 showLoading 和 hideLoading
支持 HMR,基于babel-plugin-dva-hmr实现 components、routes 和 models 的 HMR

学习资料

Dva文档:https://dvajs.com/guide/

本文档使用 看云 构建 - 33 -
UmiJS

UmiJS
简介

umi,中文可发音为乌米,是一个可插拔的企业级 react 应用框架。umi 以路由为基础的,支持类 next.js 的约定


式路由,以及各种进阶的路由功能,并以此进行功能扩展,比如支持路由级的按需加载。然后配以完善的插件体
系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求,目前内外部加起来已有 50+ 的插
件。

umi 是蚂蚁金服的底层前端框架,已直接或间接地服务了 600+ 应用,包括 java、node、H5 无线、离线


(Hybrid)应用、纯前端 assets 应用、CMS 应用等。他已经很好地服务了我们的内部用户,同时希望他也能服

务好外部用户。

特性

开箱即用,内置 react、react-router 等
类 next.js 且功能完备的路由约定,同时支持配置的路由方式
完善的插件体系,覆盖从源码到构建产物的每个生命周期
高性能,通过插件支持 PWA、以路由为单元的 code splitting 等

支持静态页面导出,适配各种环境,比如中台业务、无线业务、egg、支付宝钱包、云凤蝶等
开发启动快,支持一键开启dll和hard-source-webpack-plugin等
一键兼容到 IE9,基于umi-plugin-polyfills
完善的 TypeScript 支持,包括 d.ts 定义和 umi test
与dva数据流的深入融合,支持 duck directory、model 的自动加载、code splitting 等等

学习资料

UmiJS文档:https://umijs.org/zh/guide/

本文档使用 看云 构建 - 34 -
AntDesign

AntDesign
简介

Ant Design 是一个服务于企业级产品的设计体系,基于确定和自然的设计价值观上的模块化解决方案,让设计


者和开发者专注于更好的用户体验。

学习资料

Ant Design文档:https://ant.design/docs/react/introduce-cn

本文档使用 看云 构建 - 35 -
开发初探

开发初探
第一个页面
Ant Design组件

自定义组件
Mock数据
API调用

API结合Mock加载组件
State 状态机

第一个CRUD

本文档使用 看云 构建 - 36 -
第一个页面

第一个页面
新建页面

1. 进入src/pages目录新建Platform/Demo文件夹以及Demo.js文件

2. Demo.js是一个最简单的页面文件

代码讲解

1. 前两行作用是从 react 和 antd 模块引入所需的 React , PureComponent , Form 。


2. @Form.create() 是一种装饰器,可以将后续讲到的Form组件所需的元素引入并调用。
3. class Demo extends PureComponent 使用了语法糖,提高代码的可读性,其目的是创建一个 类
,可供外部直接引用。
4. render() 方法是重点,返回了页面主要呈现的元素,后期我们构建页面也是在这个方法里编写。
5. export default Demo 则是将编写的Demo导出,供其他模块引入、发现。

创建路由

1. Sword工程使用了umi的配置型路由,每个页面都对应一个路由,配置好之后即可访问
2. 找到config/router.config.js文件

本文档使用 看云 构建 - 37 -
第一个页面

3. 在文件底部增加如下配置

路由讲解

1. 最外层的path代表路由访问地址,我们可以把他看成菜单的顶层
2. 最外层的routes代表此大模块下所有的路由信息
3. routes下的path代表子路由的地址
4. redirect代表访问path对应的路由后会跳转至redirect里配置的路由地址
5. component代表路由对应的文件地址,访问后则会加载出对应的页面,结尾的Demo.js可以简写成Demo

启动系统

1. 启动系统,登陆后在地址栏加上 /platform/demo,发现路由地址变成了/platform/demo/list,redirect配
置生效,页面上也看到了我们所编写的 测试页面 四个字

本文档使用 看云 构建 - 38 -
第一个页面

2. 修改文字,查看系统不用重启,页面已经自动刷新成功

本文档使用 看云 构建 - 39 -
第一个页面

后续

第一个页面我们新增成功了,那么下一章让我们来学习下如何引入Ant Design的组件并简单使用

本文档使用 看云 构建 - 40 -
Ant Design组件

Ant Design组件
Ant Design官网文档

文档地址:https://ant.design/docs/react/introduce-cn

最简单的组件引入

1. 我们找到component模块下对button,查看文档

2. 稍做修改引入我们的Demo页面

本文档使用 看云 构建 - 41 -
Ant Design组件

3. 打开系统查看效果

常见的Select组件引入

1. 我们找到常用的select,查看文档

本文档使用 看云 构建 - 42 -
Ant Design组件

2. 稍做修改引入我们的Demo页面

本文档使用 看云 构建 - 43 -
Ant Design组件

3. 发现代码较乱,可以使用下 prettier 命令,使用之后代码格式化可读性更高

本文档使用 看云 构建 - 44 -
Ant Design组件

本文档使用 看云 构建 - 45 -
Ant Design组件

4. 打开系统查看效果

5. 选中后打开console,发现点击事件生效

本文档使用 看云 构建 - 46 -
Ant Design组件

后记

Ant Design的组件质量非常高,数量也多,在这个章节只是简单的教大家如何引入组件

若已有基础可直接略过,如果是新手,则强烈推荐将Ant Design的每个组件都像刚刚这样一个一个引入并且

测试效果,这样必定会给未来打下扎实的基础,磨刀不误砍柴工,请大家谨记!~

本文档使用 看云 构建 - 47 -
自定义组件

自定义组件
前言

一般来说,Ant Design提供的都是基础组件,而在我们进行业务开发的时候,会经常有多个组件组合在一起并且
会被高频调用的情况,所以这时候就用到自定义组件,那么下面我们来看下如果制作一个最简单的自定义组件。

自定义组件制作

1. 一般自定义组件都放在components文件夹中,找到后打开目录,新建一个Demo文件夹和一个ButtonX的js
文件

本文档使用 看云 构建 - 48 -
自定义组件

本文档使用 看云 构建 - 49 -
自定义组件

2. 我们加上最简单逻辑的代码,封装一个组件 ButtonX ,使之加载后自带点击事件

3. 我们再到一开始的Demo页面引入这个自定义组件,查看是否生效

4. 打开页面发现按钮已经出现

本文档使用 看云 构建 - 50 -
自定义组件

5. 点击按钮,对应的console也出现了信息。

本文档使用 看云 构建 - 51 -
自定义组件

6. 这样一来我们的第一个自定义组件就做好了。是不是很简单?但是如果自定义组件只能写成固定的加载,那
么这个组件将毫无意义,所以下面我们来学习一些常用的拓展用法

自定义组件拓展

外层参数传递

1. 想要传递按钮显示文字,Demo模块代码做如下修改:

将代码由

本文档使用 看云 构建 - 52 -
自定义组件

<ButtonX />

更改为

<ButtonX>自定义按钮</ButtonX>

2. 同时自定义组件做如下修改:

将原先的

render() {
return (
<div>
<Button type="primary" onClick={this.handleSubmit}>
按钮点击
</Button>
</div>
);
}

改为

render() {
const { children } = this.props;
return (
<div>
<Button type="primary" onClick={this.handleSubmit}>
{children}
</Button>
</div>
);
}

其中的 children 则代表包含在组件标签内的值,而传递给组件的参数都在 this.props 中获取

3. 打开系统 查看效果,发现的确如我们传入的字符一致

本文档使用 看云 构建 - 53 -
自定义组件

方法传递参数

1. 现在我们需要点击按钮后打印出的值是Demo模块传递过去的值

2. 修改代码

将原先的

<ButtonX>自定义按钮</ButtonX>

改为

<ButtonX print='测试打印内容'>自定义按钮</ButtonX>

3. 自定义组件做如下修改:

将原先的

handleSubmit = () => {
console.log('按钮点击了');

本文档使用 看云 构建 - 54 -
自定义组件

};

render() {
const { children } = this.props;
return (
<div>
<Button type="primary" onClick={this.handleSubmit}>
{children}
</Button>
</div>
);
}

改为

handleSubmit = print => {


console.log(print);
};

render() {
const { children, print } = this.props;
return (
<div>
<Button type="primary" onClick={this.handleSubmit(print)}>
{children}
</Button>
</div>
);
}

4. 打开系统点击按钮测试,发现点击并没有效果

本文档使用 看云 构建 - 55 -
自定义组件

5. 因为 print 是从 this.props 中获取的,再通过 this.handleSubmit(print) 传入, this

指针会指向错误,所以需要处理一下
6. 将代码改为如下:

handleSubmit = print => {


console.log(print);
};

render() {
const { children, print } = this.props;
return (
<div>
<Button
type="primary"
onClick={() => {
this.handleSubmit(print);
}}
>
{children}
</Button>

本文档使用 看云 构建 - 56 -
自定义组件

</div>
);
}

7. 再次打开系统,发现参数传递成功

8. 最后附上两者截图以供参考

本文档使用 看云 构建 - 57 -
自定义组件

本文档使用 看云 构建 - 58 -
自定义组件

结尾语

看到这,相信大家已经知道如何自定义一个最简单而又功能兼备的组件,掌握其中主要知识点,再在这基础上自

行拓展,相信大家都可以写出很棒都定制型组件!

本文档使用 看云 构建 - 59 -
Mock数据

Mock数据
Mock简介

Mock是模拟对象的意思,用于进行被测组件对外依赖的模拟。
Mock 是测试驱动开发必备之利器, 只要有状态, 有依赖, 做单元测试就不能没有 Mock
在 API 或 集成测试的时候, 如果依赖于第三方的 API, 也时常使用 mock server 或 mock proxy

如何使用

Sword已经完美集成了Mock,可以很方便地模拟动静态数据,也可以模拟网络延时,达到对接服务端的真实性
与准确性。下面我们来看下如何在Sword中使用Mock

1. 我们到mock文件夹下创建demo.js

2. function则是创建一个mock函数,设定接口返回值

3. 然后将其export,定义为GET类型的接口,并且接口请求地址为 '/api/demo/detail'
4. 因为Sword默认端口为8888,所以访问的地址为 http://localhost:8888/api/demo/detail
5. 打开postman(一种很好用的接口调试工具,大家也可选型其他类型的工具),调用接口查看返回成功,一
个mock接口创建成功

本文档使用 看云 构建 - 60 -
Mock数据

Mock进阶

只是简单的返回一个固定的数据,没有网络请求延时,这样无法达到我们一些复杂业务场景的需求,所以我们需
要对其进行更深一层次的定制。

根据请求参数动态判断,返回mock数据

1. 我们给mock接口传入数据,根据数据来动态展示接口返回
2. 代码如下操作,增加请求参数的动态获取

3. 主要就是根据req.query来获取传递的参数,打开postman查看一个简单的动态接口已经诞生

本文档使用 看云 构建 - 61 -
Mock数据

4. 优化返回数据

5. 再次打开postman调用接口发现返回效果与服务器接口一致

本文档使用 看云 构建 - 62 -
Mock数据

引入roadhog-api-doc模拟网络请求延时

1. mock数据模拟完毕后,发现请求耗时非常小,此时如果想模拟真实环境的网络延时,可以引入roadhog-

api-doc模块,具体代码如下,我们将延时改为1秒

本文档使用 看云 构建 - 63 -
Mock数据

2. 打开postman发现网络延时生效

本文档使用 看云 构建 - 64 -
API调用

API调用
前言

前端开发中最重要的便是api调用,从服务端拉取数据进行业务操作,完毕之后提交数据至服务端
这个流程几乎涵盖了整个系统
下面我们来学习下标准的api调用应该如何编写
同时看下如何和我们的自定义组件结合起来

定义一个api

1. 我们到services文件夹下创建demo.js,内容如下

2. 工程封装了 request 方法,将常用的方法都封装好,方便大家直接调用

调用api

1. 我们准备让这个方法在页面初次加载的时候调用,并把获取到的数据打印出来
2. 进入我们编写的Demo页面,编写一个测试方法, componentWillMount 代表页面将要加载的时候执行
自定义方法

本文档使用 看云 构建 - 65 -
API调用

3. 刷新页面查看控制台,发现定义成功

本文档使用 看云 构建 - 66 -
API调用

4. 下面我们增加api的调用,传入自定义的数据,并将其返回打印出来

5. 打开系统查看控制台打印,发现打印成功

本文档使用 看云 构建 - 67 -
API调用

使用同步调用api

很多业务场景,经常会有同时几个接口调用共同依赖的场景,若超过3个的话,都写在.then方法里进行操作,代

码会变得非常不优雅,耦合度也高。下面我们来尝试下使用同步操作代码

1. demo增加mock接口test

本文档使用 看云 构建 - 68 -
API调用

2. 对应service增加接口定义

3. 更改代码,单独抽离出一个方法init,用于同步代码的操作。同时将init的返回类型打印出来

本文档使用 看云 构建 - 69 -
API调用

4. 打开系统查看控制台打印,可以看到两条信息都打印成功,而且是按顺序加载。这样解耦了多个接口下都操

作,代码看起来更清爽,可读性更高。

本文档使用 看云 构建 - 70 -
API调用

5. 可以看到,返回都是一个Promise对象,具体介绍,请看:https://www.imooc.com/article/20580
6. 还有一点需要注意的是,如果需要用到promise,那么方法前必须带有 async 关键字,否则将失效

本文档使用 看云 构建 - 71 -
API结合Mock加载组件

API结合Mock加载组件
前言

现在我们结合之前所学,将自定义组件、mock数据、api调用整合在一起。
拓展ButtonX组件,在外部传入参数,组件内部点击后调用api后将返回打印至控制台

开始集成

1. 我们将Demo模块中的部分代码拷贝至ButtonX组件中

本文档使用 看云 构建 - 72 -
API结合Mock加载组件

2. 可以看到,现在是每次点击按钮后,进行调用接口,并且将返回打印在控制台中
3. 打开系统点击按钮查看控制台,点击两次按钮发现调用成功

本文档使用 看云 构建 - 73 -
API结合Mock加载组件

后记

很多时候,业务组件都比较复杂,里面有很多属性相互依赖、调用。若处理不好容易造成组件内部出错,这时
候,React State(状态机)就非常需要了,我们可以将会变更的数据都交由State管理,然后将界面UI与State数

据绑定。这样一来我们只需要变更对应的State,就可以自动刷新界面了。非常方便,下面我们来学习下 React
State,看看如何使用。

本文档使用 看云 构建 - 74 -
State 状态机

State 状态机
简介

React 把组件看成是一个状态机(State Machines)。


通过与用户的交互,实现不同状态,然后渲染 UI,让用户界面和数据保持一致。
React 里,只需更新组件的 state,然后根据新的 state 重新渲染用户界面(不要操作 DOM)。
下面我们来看个简单的例子了解下state状态。

拓展ButtonX组件

1. 加入state机制,完整代码如下

import React, { PureComponent } from 'react';


import { Button } from 'antd';
import { detail } from '../../services/demo';
import DescriptionList from '../DescriptionList';

const { Description } = DescriptionList;

export default class ButtonX extends PureComponent {


state = {
data: "暂无",
};

handleSubmit = print => {


const promise = this.init(print);
console.log(promise);
};

init = async print => {


const resp = await detail({ name: print });
if (resp.success) {
console.log(resp.data);
this.setState({ data: resp.data });
} else {
console.log(resp.msg);
}
};

render() {
const { children, print } = this.props;
const { data } = this.state;
return (
<div>
<Button

本文档使用 看云 构建 - 75 -
State 状态机

type="primary"
onClick={() => {
this.handleSubmit(print);
}}
>
{children}
</Button>
<Description>返回数据为:{data}</Description>
</div>
);
}
}

2. 需要注意的有三点:

(1) . 在代码最上方定义了state,里面有一个字段 data

state = {
data: {},
};

(2) . 在接口返回中使用了setState来更新state对应的值
this.setState({ data: resp.data });
(3) . 在生成的Button旁边加了从state中获取的代码并显示
const { data } = this.state;
返回数据为:{data}
3. 将UI与state绑定之后,后续若有数据变更,只需在接口返回处使用
this.setState({ data: resp.data }); 刷新state的值,react就会自动重新渲染整个UI,无需再关系
UI的生成逻辑。

查看效果

1. 我们进入系统,发现目前按钮旁边的提示为暂无

本文档使用 看云 构建 - 76 -
State 状态机

2. 点击按钮,等待接口加载完毕,发现提示信息已经变化

本文档使用 看云 构建 - 77 -
State 状态机

3. 我们没有操作UI,只是变更了state的值,React就会自动帮我们重新渲染UI。

4. 这样一来,数据与UI可以完全解耦,可以令我们后续的开发非常高效。

结尾语

到这里,关于Sword的最基础的技术点我们就学完了

下一章节我们将直接开始编写一个简单业务模块的增删改查

本文档使用 看云 构建 - 78 -
第一个CRUD

第一个CRUD
模块准备
列表页

新增页
修改页
详情页

本文档使用 看云 构建 - 79 -
模块准备

模块准备
前言

这一节是重点内容,将向大家介绍如何在sword上进行开发一个CRUD的完整模块
在开发之前,我们需要配置一下mock数据,根据接口返回动态生成菜单按钮
当切换成服务模式的时候,则从菜单管理模块进行可视化操作即可动态生成菜单

增加菜单接口返回数据

1. 到mock文件夹中找到menu.js,加入红框内的json数据

2. 返回系统,退出系统后重新登录,发现,多了一个菜单,并且点击后就跳转到了我们先前写到Demo页面

本文档使用 看云 构建 - 80 -
模块准备

3. 但是我们发现菜单中显示到是英文,而不是中文,这是因为框架在这一块采用了国际化,所以要显示对应到
语言文字,我们需要进行菜单到国际化配置
4. 根据如下截图找到对应到三个国际化文件

本文档使用 看云 构建 - 81 -
模块准备

本文档使用 看云 构建 - 82 -
模块准备

5. 增加如下配置,对应的检索就是根据返回的code字段,子模块则用点号分割(例如demo模块就是
menu.platform.demo,而menu是前缀,每个都需要带上)

6. 改好后刷新页面,发现国际化配置成功

本文档使用 看云 构建 - 83 -
模块准备

7. 系统右上角切换成因为,发现整个菜单也统一切换成了英文

8. 更多国际化配置,请参考ant design pro官方文档:https://pro.ant.design/docs/i18n-cn

增加按钮接口返回数据

本文档使用 看云 构建 - 84 -
模块准备

1. 到mock文件夹找到menu.js,加入红框内的json数据

2. 第一层code则是对应菜单模块的code,后续列表也传入这个code,就可以检索到他所属的按钮集合从而显

示在页面上了

3. 下面我们就开始编写一个基础的crud模块吧!

本文档使用 看云 构建 - 85 -
列表页

列表页
加载数据
查询功能

按钮加载原理
自定义按钮

本文档使用 看云 构建 - 86 -
列表页

加载数据
新建列表页面

1. 我们先做一个最简单的列表页面
2. 列表页面完整代码如下:

import React, { PureComponent } from 'react';


import { Button, Col, Form, Input, Row } from 'antd';
import Grid from '../../../components/Sword/Grid';
import Panel from '../../../components/Panel';

const FormItem = Form.Item;

@Form.create()
class Demo extends PureComponent {
// ============ 查询表单 ===============
renderSearchForm = onReset => {
const { form } = this.props;
const { getFieldDecorator } = form;

return (
<Row gutter={{ md: 8, lg: 24, xl: 48 }}>
<Col md={6} sm={24}>
<FormItem label="标题">
{getFieldDecorator('title')(<Input placeholder="请输入标题" />)}
</FormItem>
</Col>
<Col>
<div style={{ float: 'right' }}>
<Button type="primary" htmlType="submit">
查询
</Button>
<Button style={{ marginLeft: 8 }} onClick={onReset}>
重置
</Button>
</div>
</Col>
</Row>
);
};

render() {
const code = 'demo';

const response = {
code: 200,

本文档使用 看云 构建 - 87 -
列表页

data: {
total: 3,
size: 10,
current: 1,
searchCount: true,
pages: 1,
records: [
{
id: '1',
title: '测试标题1',
content: '测试内容1',
date: '2018-05-08 12:00:00',
},
{
id: '2',
title: '测试标题2',
content: '测试内容2',
date: '2018-06-08 12:00:00',
},
{
id: '3',
title: '测试标题3',
content: '测试内容3',
date: '2018-07-08 12:00:00',
},
],
},
message: 'success',
success: true,
};

const data = {
list: response.data.records,
pagination: {
total: response.data.total,
current: response.data.current,
pageSize: response.data.size,
},
};

const { form, loading } = this.props;

const columns = [
{
title: '标题',
dataIndex: 'title',
},
{
title: '内容',
dataIndex: 'content',

本文档使用 看云 构建 - 88 -
列表页

},
{
title: '时间',
dataIndex: 'date',
},
];

return (
<Panel>
<Grid
code={code}
form={form}
onSearch={this.handleSearch}
renderSearchForm={this.renderSearchForm}
loading={loading}
data={data}
columns={columns}
/>
</Panel>
);
}
}
export default Demo;

3. 我们来详细分析每一个代码块的作用及目的
4. 首先第一层import,是为了将所用到的组件、封装都引入进来进行页面的渲染使用

import React, { PureComponent } from 'react';


import { Button, Col, Form, Input, Row } from 'antd';
import Grid from '../../../components/Sword/Grid';
import Panel from '../../../components/Panel';

5. 接下来是定义整个类,进行完整的导出,用作路由发现并渲染页面。FormItem定义后可直接变为可引用的标
签 简化了代码

const FormItem = Form.Item;

@Form.create()
class Demo extends PureComponent {

6. renderSearchForm方法封装了搜索模块,只需定义搜索模块的表单元素并传入到下面封装的Grid组件,即
可自动生成查询重置等功能,更复杂的功能,都可以通过这个函数进行拓展

本文档使用 看云 构建 - 89 -
列表页

renderSearchForm = onReset => {


const { form } = this.props;
const { getFieldDecorator } = form;

return (
<Row gutter={{ md: 8, lg: 24, xl: 48 }}>
<Col md={6} sm={24}>
<FormItem label="标题">
{getFieldDecorator('title')(<Input placeholder="请输入标题" />)}
</FormItem>
</Col>
<Col>
<div style={{ float: 'right' }}>
<Button type="primary" htmlType="submit">
查询
</Button>
<Button style={{ marginLeft: 8 }} onClick={onReset}>
重置
</Button>
</div>
</Col>
</Row>
);
};

7. render函数返回的就是Demo组件最终渲染的元素

render() {

8. const code = 'demo'; 定义了此模块的菜单code是demo,传入Grid组件后,便会根据设定好的


code至去查找所拥有的按钮集最后呈现在页面上。若新建了另一个模块demo1,则此时的code也改为

demo1,与菜单code保持一致
9. 再往下的response和data,则定义了Grid组件所需呈现的数据源。response模拟了接口返回的数据,而

data则是对返回数据进行了二次加工,从而变为组件可识别的数据格式

const response = {
code: 200,
data: {
total: 3,
size: 10,
current: 1,
searchCount: true,
pages: 1,

本文档使用 看云 构建 - 90 -
列表页

records: [
{
id: '1',
title: '测试标题1',
content: '测试内容1',
date: '2018-05-08 12:00:00',
},
{
id: '2',
title: '测试标题2',
content: '测试内容2',
date: '2018-06-08 12:00:00',
},
{
id: '3',
title: '测试标题3',
content: '测试内容3',
date: '2018-07-08 12:00:00',
},
],
},
message: 'success',
success: true,
};

const data = {
list: response.data.records,
pagination: {
total: response.data.total,
current: response.data.current,
pageSize: response.data.size,
},
};

10. 最后一段代码则是定义了Grid所需显示的字段,以及所有数据的汇总,都设定在了Grid组件里

const { form, loading } = this.props;

const columns = [
{
title: '标题',
dataIndex: 'title',
},
{
title: '内容',
dataIndex: 'content',
},
{
title: '时间',

本文档使用 看云 构建 - 91 -
列表页

dataIndex: 'date',
},
];

return (
<Panel>
<Grid
code={code}
form={form}
onSearch={this.handleSearch}
renderSearchForm={this.renderSearchForm}
loading={loading}
data={data}
columns={columns}
/>
</Panel>
);

11. 好了,源码分析完了,我们打开系统查看下对应的效果,发现一个列表也已然生成,数据内容与我们设置的

一样

12. 如果页面都把数据写成固定的,那么这个模块基本没有价值,Grid显示的数据必须是动态的对接接口的,所
以我们下面来学习下如何将数据对接接口,让他活起来。

对接列表接口

1. 在demo.js中加入 getFakeList ,定义数据返回(注意json格式要严格按照截图中所来),否则grid组


件加载会出问题

本文档使用 看云 构建 - 92 -
列表页

2. 打开postman调用mock接口查看返回成功

本文档使用 看云 构建 - 93 -
列表页

本文档使用 看云 构建 - 94 -
列表页

3. 我们到services文件夹下定义这个新增的接口

4. 优化列表页代码,加入接口动态对接

import React, { PureComponent } from 'react';


import { Button, Col, Form, Input, Row } from 'antd';
import Grid from '../../../components/Sword/Grid';
import Panel from '../../../components/Panel';
import { list } from '../../../services/demo';

const FormItem = Form.Item;

@Form.create()
class Demo extends PureComponent {
state = {
data: {},
};

componentWillMount() {
list().then(response => {
this.setState({
data: {
list: response.data.records,
pagination: {
total: response.data.total,
current: response.data.current,
pageSize: response.data.size,
},
},
});
});
}

// ============ 查询表单 ===============


renderSearchForm = onReset => {
const { form } = this.props;
const { getFieldDecorator } = form;

return (
<Row gutter={{ md: 8, lg: 24, xl: 48 }}>

本文档使用 看云 构建 - 95 -
列表页

<Col md={6} sm={24}>


<FormItem label="模块名">
{getFieldDecorator('title')(<Input placeholder="请输入模块名" />)}
</FormItem>
</Col>
<Col>
<div style={{ float: 'right' }}>
<Button type="primary" htmlType="submit">
查询
</Button>
<Button style={{ marginLeft: 8 }} onClick={onReset}>
重置
</Button>
</div>
</Col>
</Row>
);
};

render() {
const code = 'demo';

const { data } = this.state;

const { form, loading } = this.props;

const columns = [
{
title: '标题',
dataIndex: 'title',
},
{
title: '内容',
dataIndex: 'content',
},
{
title: '时间',
dataIndex: 'date',
},
];

return (
<Panel>
<Grid
code={code}
form={form}
onSearch={this.handleSearch}
renderSearchForm={this.renderSearchForm}
loading={loading}
data={data}

本文档使用 看云 构建 - 96 -
列表页

columns={columns}
/>
</Panel>
);
}
}
export default Demo;

5. 可以看出,我们将data放入了state,并在组件将要加载的时候调用api接口,返回的时候将数据重新写入到

state中,从而刷新渲染,显示出了数据。
6. 打开页面刷新,发现数据加载成功

本文档使用 看云 构建 - 97 -
列表页

查询功能
加入查询功能

1. 有了列表页,自然缺不了的就是查询功能,下面我们来加入这个功能
2. 为了让查询更加真实,我们稍稍改造下mock中的 getFakeList 返回

function getFakeList(req, res) {


const json = { code: 200, success: true, msg: '操作成功' };
if (req.query.title === '测试标题1') {
json.data = {
total: 1,
size: 10,
current: 1,
searchCount: true,
pages: 1,
records: [
{
id: '1',
title: '测试标题1',
content: '测试内容1',
date: '2018-05-08 12:00:00',
},
],
};
} else if (req.query.title === '测试标题2') {
json.data = {
total: 1,
size: 10,
current: 1,
searchCount: true,
pages: 1,
records: [
{
id: '2',
title: '测试标题2',
content: '测试内容2',
date: '2018-06-08 12:00:00',
},
],
};
} else {
json.data = {
total: 3,
size: 10,
current: 1,
searchCount: true,

本文档使用 看云 构建 - 98 -
列表页

pages: 1,
records: [
{
id: '1',
title: '测试标题1',
content: '测试内容1',
date: '2018-05-08 12:00:00',
},
{
id: '2',
title: '测试标题2',
content: '测试内容2',
date: '2018-06-08 12:00:00',
},
{
id: '3',
title: '测试标题3',
content: '测试内容3',
date: '2018-07-08 12:00:00',
},
],
};
}
return res.json(json);
}

3. 在 componentWillMount 方法下增加 handleSearch ,传入参数重新调用接口进行查询返回

handleSearch = params => {


list(params).then(response => {
this.setState({
data: {
list: response.data.records,
pagination: {
total: response.data.total,
current: response.data.current,
pageSize: response.data.size,
},
},
});
});
};

4. 打开系统,输入 测试标题1 后点击查询,发现列表刷新成功

本文档使用 看云 构建 - 99 -
列表页

5. 点击重置按钮,列表又回归原样

6. 这样一来,一个简单的查询列表就做好了,未来只需将mock数据源换成真实的服务端api即可,无需再次开
发,非常方便

本文档使用 看云 构建 - 100 -
列表页

按钮加载原理
按钮加载逻辑

1. 整个列表页只有一个Grid组件,却动态加载出了按钮,我们不妨看一看这个组件是如何实现的
2. 点进Grid组件,拉到最底下查看render返回,发现封装了3个子组件

<Card bordered={false} {...cardProps}>


<div className={styles.swordPage}>
<SearchBox onSubmit={this.handleSearch} onReset={this.handleFormReset}>
{renderSearchForm(this.handleFormReset)}
</SearchBox>
<ToolBar
buttons={buttons}
renderLeftButton={renderLeftButton}
renderRightButton={renderRightButton}
onClick={this.handelToolBarClick}
/>
<StandardTable
rowKey={rowKey || 'id'}
selectedRows={selectedRows}
loading={loading}
columns={columns}
data={data}
onSelectRow={this.handleSelectRows}
onChange={this.handleStandardTableChange}
scroll={scroll}
tblProps={tblProps}
size="middle"
/>
</div>
</Card>

3. 这3个组件分别为 SearchBox 、 ToolBar 、 StandardTable ,我们主要来看 ToolBar


4. ToolBar 传入了4个参数: buttons 、 renderLeftButton 、 renderRightButton 、
onClick
5. buttons就是根据列表页传入的菜单code,通过执行getButton方法从而获取到的按钮集合

export default class Grid extends PureComponent {


constructor(props) {
super(props);
this.state = {
current: 1,
size: 10,

本文档使用 看云 构建 - 101 -
列表页

formValues: {},
selectedRows: [],
buttons: getButton(props.code),
};
}
}

6. 进入ToolBar查看源码,发现其实很简单,就是根据buttons数据集合动态生成了按钮。当中有一点需要注意
的是 buttons.filter(button => button.action === 1 || button.action === 3) ,他
根据action字段来判断,这个action代表按钮类型:1:只有工具栏才出现;2:只有表格行才出现;3:两者
都出现。

export default class ToolBar extends PureComponent {


render() {
const { buttons, renderLeftButton, renderRightButton, onClick } = this.props
;
return (
<div className={styles.operator}>
<div>
{buttons
.filter(button => button.action === 1 || button.action === 3)
.map(button => (
<Button
key={button.code}
icon={button.source}
type={
button.alias === 'delete'
? 'danger'
: button.alias === 'add'
? 'primary'
: 'default'
}
onClick={() => {
onClick(button);
}}
>
<FormattedMessage id={`button.${button.alias}.name`} />
</Button>
))}
{renderLeftButton ? renderLeftButton() : null}
{renderRightButton ? (
<div style={{ float: 'right', marginRight: '20px' }}>{renderRightBu
tton()}</div>
) : null}
</div>
</div>
);

本文档使用 看云 构建 - 102 -
列表页

}
}

7. 其中都 renderLeftButton 、 renderRightButton 则是提供大家这列表页自定义的按钮,传入组


件,从而生成,不受限于通过菜单接口返回的数据
8. onClick 则是组件定义的一些默认点击事件,大家也可以自行拓展,下面是封装的方法。可以看到方法
根据菜单中定义的 按钮别名 来判断执行不同的操作。若一个code为 notice_add ,一个code为
news_add,两个按钮的别名同为 add,则这两个按钮点击后都会进入第一个判断并执行。若后续有通用的
按钮事件,大家也可以在这里自定义拓展。

handelClick = (btn, keys = []) => {


const { path, alias } = btn;
const { btnCallBack } = this.props;
const refresh = (temp = true) => this.refreshTable(temp);
if (alias === 'add') {
if (keys.length > 1) {
message.warn('父记录只能选择一条!');
return;
}
if (keys.length === 1) {
router.push(`${path}/${keys[0]}`);
return;
}
router.push(path);
return;
}
if (alias === 'edit') {
if (keys.length <= 0) {
message.warn('请先选择一条数据!');
return;
}
if (keys.length > 1) {
message.warn('只能选择一条数据!');
return;
}
router.push(`${path}/${keys[0]}`);
return;
}
if (alias === 'view') {
if (keys.length <= 0) {
message.warn('请先选择一条数据!');
return;
}
if (keys.length > 1) {
message.warn('只能选择一条数据!');
return;

本文档使用 看云 构建 - 103 -
列表页

}
router.push(`${path}/${keys[0]}`);
return;
}
if (alias === 'delete') {
if (keys.length <= 0) {
message.warn('请先选择要删除的记录!');
return;
}

Modal.confirm({
title: '删除确认',
content: '确定删除选中记录?',
okText: '确定',
okType: 'danger',
cancelText: '取消',
async onOk() {
const response = await requestApi(path, { ids: keys.join(',') });
if (response.success) {
message.success(response.msg);
refresh();
} else {
message.error(response.msg || '删除失败');
}
},
onCancel() {},
});
return;
}
if (btnCallBack) {
btnCallBack({ btn, keys, refresh });
}
};

9. 看到这里,相信大家对封装对列表组件应该有个大致的认识了,下面我们来看一下如果进行自定义按钮的配
置。

本文档使用 看云 构建 - 104 -
列表页

自定义按钮
自定义按钮

1. 自定义按钮提供两种入口: renderLeftButton 、 renderRightButton ,我们来尝试下是否可以


生成
2. 我们加入如下代码

test = () => {
console.log("测试按钮生成")
}

renderLeftButton = () => (
<Button icon="tool" onClick={this.test}>
测试
</Button>
);

3. 在Grid组件中增加属性: renderLeftButton={this.renderLeftButton}

<Grid
code={code}
..............
renderLeftButton={this.renderLeftButton}
..............
/>

4. 打开系统查看,发现多了一个按钮,并且点击后也执行了自定义的方法

本文档使用 看云 构建 - 105 -
列表页

5. 将其修改为 renderRightButton ,打开系统发现同样的效果,只不过按钮到了最右边

自定义按钮获取表格信息

1. 修改 state

state = {
data: {},
selectedRows: [],
};

2. 增加表格点击事件,将所选行数据加入state中

本文档使用 看云 构建 - 106 -
列表页

onSelectRow = rows => {


this.setState({
selectedRows: rows,
});
};

getSelectKeys = () => {
const { selectedRows } = this.state;
return selectedRows.map(row => row.id);
};

3. 强化按钮点击事件

test = () => {
const keys = this.getSelectKeys();
if (keys.length === 0) {
message.warn('请先选择一条数据!');
} else {
console.log(`已选择数据id:${keys}`);
}
}

4. Grid组件增加参数 onSelectRow={this.onSelectRow}

<Grid
code={code}
..............
onSelectRow={this.onSelectRow}
..............
/>

5. 未选中数据的时候,点击按钮发现提示先选择一条数据

6. 选中数据后点击按钮查看控制台发现已经将对应的id打印了出来

本文档使用 看云 构建 - 107 -
列表页

7. Grid组件默认每次加载列表页会默认执行一次 handelSearch 方法,所以我们可以删掉

componentWillMount 方法,无需重复写代码。

后记

到这里整个列表页的基础讲解就结束了,相信大家多看示例模块的写法,定能熟练掌握
最后附上列表页的完整代码

import React, { PureComponent } from 'react';


import { Button, Col, Form, Input, message, Row } from 'antd';
import Grid from '../../../components/Sword/Grid';
import Panel from '../../../components/Panel';
import { list } from '../../../services/demo';

const FormItem = Form.Item;

@Form.create()
class Demo extends PureComponent {
state = {
data: {},
selectedRows: [],
};

onSelectRow = rows => {


this.setState({
selectedRows: rows,
});
};

getSelectKeys = () => {
const { selectedRows } = this.state;
return selectedRows.map(row => row.id);
};

// ============ 查询 ===============

本文档使用 看云 构建 - 108 -
列表页

handleSearch = params => {


list(params).then(response => {
this.setState({
data: {
list: response.data.records,
pagination: {
total: response.data.total,
current: response.data.current,
pageSize: response.data.size,
},
},
});
});
};

// ============ 查询表单 ===============


renderSearchForm = onReset => {
const { form } = this.props;
const { getFieldDecorator } = form;

return (
<Row gutter={{ md: 8, lg: 24, xl: 48 }}>
<Col md={6} sm={24}>
<FormItem label="标题">
{getFieldDecorator('title')(<Input placeholder="请输入标题" />)}
</FormItem>
</Col>
<Col>
<div style={{ float: 'right' }}>
<Button type="primary" htmlType="submit">
查询
</Button>
<Button style={{ marginLeft: 8 }} onClick={onReset}>
重置
</Button>
</div>
</Col>
</Row>
);
};

test = () => {
const keys = this.getSelectKeys();
if (keys.length === 0) {
message.warn('请先选择一条数据!');
} else {
console.log(`已选择数据id:${keys}`);
}
}

本文档使用 看云 构建 - 109 -
列表页

renderLeftButton = () => (
<Button icon="tool" onClick={this.test}>
测试
</Button>
);

render() {
const code = 'demo';

const { data } = this.state;

const { form, loading } = this.props;

const columns = [
{
title: '标题',
dataIndex: 'title',
},
{
title: '内容',
dataIndex: 'content',
},
{
title: '时间',
dataIndex: 'date',
},
];

return (
<Panel>
<Grid
code={code}
form={form}
onSearch={this.handleSearch}
onSelectRow={this.onSelectRow}
renderSearchForm={this.renderSearchForm}
renderLeftButton={this.renderLeftButton}
loading={loading}
data={data}
columns={columns}
/>
</Panel>
);
}
}
export default Demo;

本文档使用 看云 构建 - 110 -
新增页

新增页
新增页面

1. 我们到Demo文件夹下新建文件DemoAdd.js

import React, { PureComponent } from 'react';


import { Form, Input, Card, Button, DatePicker } from 'antd';
import moment from 'moment';
import Panel from '../../../components/Panel';
import styles from '../../../layouts/Sword.less';

const FormItem = Form.Item;


const { TextArea } = Input;

@Form.create()
class DemoAdd extends PureComponent {

render() {
const {
form: { getFieldDecorator },
} = this.props;

const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 7 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 },
md: { span: 10 },
},
};

const action = (
<Button type="primary">
提交
</Button>
);

return (
<Panel title="新增" back="platform/demo" action={action}>
<Form hideRequiredMark style={{ marginTop: 8 }}>
<Card title="基本信息" className={styles.card} bordered={false}>
<FormItem {...formItemLayout} label="标题">
{getFieldDecorator('title')(<Input placeholder="请输入标题" />)}

本文档使用 看云 构建 - 111 -
新增页

</FormItem>
<FormItem {...formItemLayout} label="时间">
{getFieldDecorator('date')(
<DatePicker
style={{ width: '100%' }}
format="YYYY-MM-DD HH:mm:ss"
showTime={{ defaultValue: moment('00:00:00', 'HH:mm:ss') }}
/>
)}
</FormItem>
<FormItem {...formItemLayout} label="内容">
{getFieldDecorator('content')(
<TextArea style={{ minHeight: 32 }} placeholder="请输入内容" rows
={10} />
)}
</FormItem>
</Card>
</Form>
</Panel>
);
}
}

export default DemoAdd;

2. 主要需要注意的是引入了form中的 getFieldDecorator ,用于进行表单操作、数据绑定、数据获取等


功能
3. 当写入组件的时候,需要被 getFieldDecorator 包装起来,具体语法如下

{getFieldDecorator('title')(<Input placeholder="请输入标题" />)}

4. 这样生成了组件后,他的值就会和对于设定的字段进行绑定,从而对其进行操作
5. 页面写好后,我们到路由配置文件 router.config.js 增加路径

本文档使用 看云 构建 - 112 -
新增页

6. 打开系统点击新增按钮后发现页面跳转成功

表单提交

1. 页面构建成功,接下来我们需要的就是提交表单的数据

2. 增加按钮点击事件

handleSubmit = e => {
e.preventDefault();
const { form } = this.props;
form.validateFieldsAndScroll((err, values) => {
if (!err) {
console.log(values)
}
});
};

本文档使用 看云 构建 - 113 -
新增页

const action = (
<Button type="primary" onClick={this.handleSubmit}>
提交
</Button>
);

3. 打开系统控制台,输入一些数据提交,查看打印正确

表单校验

1. 为了保证业务数据的准确性,表单提交前需要做数据校验

2. 我们先做一个最简单的非空校验,修改如下代码
将原先的

{getFieldDecorator('title')(<Input placeholder="请输入标题" />)}

修改为

{getFieldDecorator('title', {
rules: [
{
required: true,
message: '请输入标题',
},
],
})(<Input placeholder="请输入标题" />)}

3. 可以看到,在getFieldDecorator方法的入参中加入了一个json对象,值是rules,对应着校验规则(更多内

容请看官方文档:https://ant.design/components/form-cn/)

本文档使用 看云 构建 - 114 -
新增页

{
rules: [
{
required: true,
message: '请输入标题',
},
],
}

4. 打开系统,不输入日期,点击提交发现提示,校验成功

对接接口

1. 到mockjs中新建api提交的接口,增加fakeSuccess返回

function fakeSuccess(req, res) {


const json = { code: 200, success: true, msg: '操作成功' };
return res.json(json);
}

const proxy = {
'GET /api/demo/list': getFakeList,
'GET /api/demo/detail': getFakeDetail,
'POST /api/demo/submit': fakeSuccess,
'POST /api/demo/remove': fakeSuccess,
};

export default delay(proxy, 1000);

2. 优化handelSubmit方法,增加submit方法传入表单数据values供api调用

handleSubmit = e => {
e.preventDefault();
const { form } = this.props;

本文档使用 看云 构建 - 115 -
新增页

form.validateFieldsAndScroll((err, values) => {


if (!err) {
submit(values).then(resp => {
if (resp.success) {
message.success('提交成功');
router.push('/platform/demo');
} else {
message.warn(resp.msg);
}
});
}
});
};

3. 打开系统点击提交发现提交成功,network监听传递参数无误

4. 但是有一点需要注意的是,传递过去的日期类型并非标准的 YYYY-MM-DD HH:mm:ss ,所以需要再次优

化下提交代码

handleSubmit = e => {
e.preventDefault();
const { form } = this.props;
form.validateFieldsAndScroll((err, values) => {
if (!err) {
const params = {
...values,
date: func.format(values.date),
};
submit(params).then(resp => {

本文档使用 看云 构建 - 116 -
新增页

if (resp.success) {
message.success('提交成功');
router.push('/platform/demo');
} else {
message.warn(resp.msg);
}
});
}
});
};

5. 再次提交查看参数传递给接口的格式已经正确

6. 最后以下附上完整代码

import React, { PureComponent } from 'react';


import router from 'umi/router';
import { Form, Input, Card, Button, message, DatePicker } from 'antd';
import moment from 'moment';
import Panel from '../../../components/Panel';
import styles from '../../../layouts/Sword.less';
import { submit } from '../../../services/demo';
import func from '../../../utils/Func';

const FormItem = Form.Item;


const { TextArea } = Input;

@Form.create()
class DemoAdd extends PureComponent {

handleSubmit = e => {
e.preventDefault();
const { form } = this.props;
form.validateFieldsAndScroll((err, values) => {
if (!err) {
const params = {
...values,
date: func.format(values.date),
};
submit(params).then(resp => {
if (resp.success) {
message.success('提交成功');
router.push('/platform/demo');

本文档使用 看云 构建 - 117 -
新增页

} else {
message.warn(resp.msg);
}
});
}
});
};

render() {
const {
form: { getFieldDecorator },
} = this.props;

const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 7 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 },
md: { span: 10 },
},
};

const action = (
<Button type="primary" onClick={this.handleSubmit}>
提交
</Button>
);

return (
<Panel title="新增" back="/platform/demo" action={action}>
<Form hideRequiredMark style={{ marginTop: 8 }}>
<Card title="基本信息" className={styles.card} bordered={false}>
<FormItem {...formItemLayout} label="标题">
{getFieldDecorator('title', {
rules: [
{
required: true,
message: '请输入标题',
},
],
})(<Input placeholder="请输入标题" />)}
</FormItem>
<FormItem {...formItemLayout} label="日期">
{getFieldDecorator('date', {
rules: [
{
required: true,

本文档使用 看云 构建 - 118 -
新增页

message: '请输入日期',
},
],
})(
<DatePicker
style={{ width: '100%' }}
format="YYYY-MM-DD HH:mm:ss"
showTime={{ defaultValue: moment('00:00:00', 'HH:mm:ss') }}
/>
)}
</FormItem>
<FormItem {...formItemLayout} label="内容">
{getFieldDecorator('content')(
<TextArea style={{ minHeight: 32 }} placeholder="请输入内容" rows
={10} />
)}
</FormItem>
</Card>
</Form>
</Panel>
);
}
}

export default DemoAdd;

本文档使用 看云 构建 - 119 -
修改页

修改页
创建修改页

1. 修改页元素与新增差不多,不同的是修改页面需要在组件加载的时候获取数据,从而绑定在页面上
2. 将上一节的新增页代码过来稍作修改

import React, { PureComponent } from 'react';


import router from 'umi/router';
import { Form, Input, Card, Button, message, DatePicker } from 'antd';
import moment from 'moment';
import Panel from '../../../components/Panel';
import styles from '../../../layouts/Sword.less';
import { submit } from '../../../services/demo';
import func from '../../../utils/Func';

const FormItem = Form.Item;


const { TextArea } = Input;

@Form.create()
class DemoEdit extends PureComponent {

handleSubmit = e => {
e.preventDefault();
const { form } = this.props;
form.validateFieldsAndScroll((err, values) => {
if (!err) {
const params = {
...values,
date: func.format(values.date),
};
submit(params).then(resp => {
if (resp.success) {
message.success('提交成功');
router.push('/platform/demo');
} else {
message.warn(resp.msg);
}
});
}
});
};

render() {
const {
form: { getFieldDecorator },
} = this.props;

本文档使用 看云 构建 - 120 -
修改页

const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 7 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 },
md: { span: 10 },
},
};

const action = (
<Button type="primary" onClick={this.handleSubmit}>
提交
</Button>
);

return (
<Panel title="修改" back="/platform/demo" action={action}>
<Form hideRequiredMark style={{ marginTop: 8 }}>
<Card title="基本信息" className={styles.card} bordered={false}>
<FormItem {...formItemLayout} label="标题">
{getFieldDecorator('title', {
rules: [
{
required: true,
message: '请输入标题',
},
],
})(<Input placeholder="请输入标题" />)}
</FormItem>
<FormItem {...formItemLayout} label="日期">
{getFieldDecorator('date', {
rules: [
{
required: true,
message: '请输入日期',
},
],
})(
<DatePicker
style={{ width: '100%' }}
format="YYYY-MM-DD HH:mm:ss"
disabledDate={this.disabledDate}
showTime={{ defaultValue: moment('00:00:00', 'HH:mm:ss') }}
/>
)}
</FormItem>

本文档使用 看云 构建 - 121 -
修改页

<FormItem {...formItemLayout} label="内容">


{getFieldDecorator('content')(
<TextArea style={{ minHeight: 32 }} placeholder="请输入内容" rows
={10} />
)}
</FormItem>
</Card>
</Form>
</Panel>
);
}
}

export default DemoEdit;

3. 到路由配置文件 router.config.js 增加路径

对接接口

1. 修改detail方法返回,模拟真实场景

function getFakeDetail(req, res) {


const json = { code: 200, success: true, msg: '操作成功' };
if (req.query.id === '1') {
json.data = {
id: '1',
title: '测试标题1',
content: '测试内容1',
date: '2018-05-08 12:00:00',
};
} else if (req.query.id === '2') {
json.data = {

本文档使用 看云 构建 - 122 -
修改页

id: '2',
title: '测试标题2',
content: '测试内容2',
date: '2018-06-08 12:00:00',
};
} else {
json.data = {
id: '3',
title: '测试标题3',
content: '测试内容3',
date: '2018-07-08 12:00:00',
};
}
return res.json(json);
}

2. 增加接口对接,因为涉及到UI数据绑定,所以需要用到state

state = {
data: {},
};

componentWillMount() {
const {
match: {
params: { id },
},
} = this.props;
detail({ id }).then(resp => {
if (resp.success) {
this.setState({ data: resp.data });
}
});
}

3. 页面跳转的时候可以通过props中的match获取url参数,主要获取方式为

const {
match: {
params: { id },
},
} = this.props;

4. 为三个组件增加数据绑定,需要注意的是,日期类型需要做下格式化才能初始化成功
initialValue: moment(data.date, 'YYYY-MM-DD HH:mm:ss')

本文档使用 看云 构建 - 123 -
修改页

{getFieldDecorator('title', {
rules: [
{
required: true,
message: '请输入标题',
},
],
initialValue: data.title,
})(<Input placeholder="请输入标题" />)}

{getFieldDecorator('date', {
rules: [
{
required: true,
message: '请输入日期',
},
],
initialValue: moment(data.date, 'YYYY-MM-DD HH:mm:ss'),
})(
<DatePicker
style={{ width: '100%' }}
format="YYYY-MM-DD HH:mm:ss"
showTime={{ defaultValue: moment('00:00:00', 'HH:mm:ss') }}
/>
)}

{getFieldDecorator('content', {
initialValue: data.content,
})(<TextArea style={{ minHeight: 32 }} placeholder="请输入内容" rows={10} />)}

5. 因为修改的时候,需要同时传递id,所以提交方法也得做如下修改

handleSubmit = e => {
e.preventDefault();
const {
form,
match: {
params: { id },
},
} = this.props;
form.validateFieldsAndScroll((err, values) => {
if (!err) {
const params = {
id,
...values,
date: func.format(values.date),

本文档使用 看云 构建 - 124 -
修改页

};
submit(params).then(resp => {
if (resp.success) {
message.success('提交成功');
router.push('/platform/demo');
} else {
message.warn(resp.msg);
}
});
}
});
};

6. 打开系统,点击修改查看数据显示成功

7. 点击返回再次点击 测试标题2 的修改,发现数据也同步变化

本文档使用 看云 构建 - 125 -
修改页

8. 点击提交查看控制台,数据也传递正确

全部代码一览

import React, { PureComponent } from 'react';


import router from 'umi/router';
import { Form, Input, Card, Button, message, DatePicker } from 'antd';
import moment from 'moment';
import Panel from '../../../components/Panel';
import styles from '../../../layouts/Sword.less';

本文档使用 看云 构建 - 126 -
修改页

import { submit, detail } from '../../../services/demo';


import func from '../../../utils/Func';

const FormItem = Form.Item;


const { TextArea } = Input;

@Form.create()
class DemoEdit extends PureComponent {
state = {
data: {},
};

componentWillMount() {
const {
match: {
params: { id },
},
} = this.props;
detail({ id }).then(resp => {
if (resp.success) {
this.setState({ data: resp.data });
}
});
}

handleSubmit = e => {
e.preventDefault();
const {
form,
match: {
params: { id },
},
} = this.props;
form.validateFieldsAndScroll((err, values) => {
if (!err) {
const params = {
id,
...values,
date: func.format(values.date),
};
submit(params).then(resp => {
if (resp.success) {
message.success('提交成功');
router.push('/platform/demo');
} else {
message.warn(resp.msg);
}
});
}
});

本文档使用 看云 构建 - 127 -
修改页

};

render() {
const {
form: { getFieldDecorator },
} = this.props;

const { data } = this.state;

const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 7 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 },
md: { span: 10 },
},
};

const action = (
<Button type="primary" onClick={this.handleSubmit}>
提交
</Button>
);

return (
<Panel title="修改" back="/platform/demo" action={action}>
<Form hideRequiredMark style={{ marginTop: 8 }}>
<Card title="基本信息" className={styles.card} bordered={false}>
<FormItem {...formItemLayout} label="标题">
{getFieldDecorator('title', {
rules: [
{
required: true,
message: '请输入标题',
},
],
initialValue: data.title,
})(<Input placeholder="请输入标题" />)}
</FormItem>
<FormItem {...formItemLayout} label="日期">
{getFieldDecorator('date', {
rules: [
{
required: true,
message: '请输入日期',
},
],

本文档使用 看云 构建 - 128 -
修改页

initialValue: moment(data.date, 'YYYY-MM-DD HH:mm:ss'),


})(
<DatePicker
style={{ width: '100%' }}
format="YYYY-MM-DD HH:mm:ss"
showTime={{ defaultValue: moment('00:00:00', 'HH:mm:ss') }}
/>
)}
</FormItem>
<FormItem {...formItemLayout} label="内容">
{getFieldDecorator('content', {
initialValue: data.content,
})(<TextArea style={{ minHeight: 32 }} placeholder="请输入内容" ro
ws={10} />)}
</FormItem>
</Card>
</Form>
</Panel>
);
}
}

export default DemoEdit;

本文档使用 看云 构建 - 129 -
详情页

详情页
创建详情页

1. 我们把修改页完成后,详情页就简单多了,只是一个查看功能。没有数据提交、数据校验等功能
2. 我们复制修改页,做如下修改,将组件都变更为span类型

import React, { PureComponent } from 'react';


import { Form, Card } from 'antd';
import Panel from '../../../components/Panel';
import styles from '../../../layouts/Sword.less';
import { detail } from '../../../services/demo';

const FormItem = Form.Item;

@Form.create()
class DemoEdit extends PureComponent {
state = {
data: {},
};

componentWillMount() {
const {
match: {
params: { id },
},
} = this.props;
detail({ id }).then(resp => {
if (resp.success) {
this.setState({ data: resp.data });
}
});
}

render() {

const { data } = this.state;

const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 7 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 },
md: { span: 10 },

本文档使用 看云 构建 - 130 -
详情页

},
};

return (
<Panel title="查看" back="/platform/demo">
<Form hideRequiredMark style={{ marginTop: 8 }}>
<Card title="基本信息" className={styles.card} bordered={false}>
<FormItem {...formItemLayout} label="标题">
<span>{data.title}</span>
</FormItem>
<FormItem {...formItemLayout} label="日期">
<span>{data.date}</span>
</FormItem>
<FormItem {...formItemLayout} label="内容">
<span>{data.content}</span>
</FormItem>
</Card>
</Form>
</Panel>
);
}
}

export default DemoEdit;

3. 到路由配置文件 router.config.js 增加路径

4. 打开系统点击查看发现数据加载成功

本文档使用 看云 构建 - 131 -
详情页

5. 附上完整的路由配置

{
path: '/platform',
routes: [
{
path: '/platform/demo',
routes: [
{ path: '/platform/demo', redirect: '/platform/demo/list' },
{ path: '/platform/demo/list', component: './Platform/Demo/Demo' },
{ path: '/platform/demo/add', component: './Platform/Demo/DemoAdd' },
{ path: '/platform/demo/edit/:id', component: './Platform/Demo/DemoEdit'
},
{ path: '/platform/demo/view/:id', component: './Platform/Demo/DemoView'
},
],
},
],
},

结束语

经过一整个章节的讲解,相信大家能对React开发有个初步的认识,下面我们来学习下进阶的知识并且对我们做

的CRUD模块再次优化,符合正式的开发需求。

本文档使用 看云 构建 - 132 -
开发进阶

开发进阶
API反向代理
Dva数据流

基于数据流改造CRUD

本文档使用 看云 构建 - 133 -
API反向代理

API反向代理
什么是反向代理

反向代理是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务
器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。

为什么要用反向代理

前端与后端接口对接的时候,若只使用完整的api链接,或者后端api不做处理的话,会造成跨域。从而无法正常
调用到接口。这时候就需要将接口代理到本地以此来消除跨域生成的条件,这样一来就可以顺利调用api了。

如何使用反向代理

1. 找到config.js,修改proxy下的配置

2. 第一个"/api"则为反向代理后的前缀
3. target则代表需要反向代理的地址
4. 如图所示, http://localhost:8800/token ,代理后会变成 /api/token 。
5. 同时因为pathRewrite的作用,会把/api替换成空,则 http://localhost:8800/api/token ,代理
后地址也会变成 /api/token 。
6. 如果需要多个,则只需像下面配置即可

proxy: {
'/api': {
target: 'http://localhost:8800',
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
'/app': {
target: 'http://localhost:8801',
changeOrigin: true,
pathRewrite: { '^/app': '' },

本文档使用 看云 构建 - 134 -
API反向代理

},
},

本文档使用 看云 构建 - 135 -
Dva数据流

Dva数据流
数据流向

数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时
候可以通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是
异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State ,所以在 dva 中,数据流
向非常清晰简明,并且思路基本跟开源社区保持一致(也是来自于开源社区)。

Models

State
type State = any
State 表示 Model 的状态数据,通常表现为一个 javascript 对象(当然它可以是任何值);操作的时候每次都要
当作不可变数据(immutable data)来对待,保证每次都是全新对象,没有引用关系,这样才能保证 State 的
独立性,便于测试和追踪变化。
在 dva 中你可以通过 dva 的实例属性 _store 看到顶部的 state 数据,但是通常你很少会用到:

const app = dva();


console.log(app._store); // 顶部的 state 数据

Action
type AsyncAction = any
Action 是一个普通 javascript 对象,它是改变 State 的唯一途径。无论是从 UI 事件、网络回调,还是
WebSocket 等数据源所获得的数据,最终都会通过 dispatch 函数调用一个 action,从而改变对应的数据。
action 必须带有 type 属性指明具体的行为,其它字段可以自定义,如果要发起一个 action 需要使用
dispatch 函数;需要注意的是 dispatch 是在组件 connect Models以后,通过 props 传入的。

本文档使用 看云 构建 - 136 -
Dva数据流

dispatch({
type: 'add',
});

dispatch 函数
type dispatch = (a: Action) => Action
dispatching function 是一个用于触发 action 的函数,action 是改变 State 的唯一途径,但是它只描述了一个
行为,而 dipatch 可以看作是触发这个行为的方式,而 Reducer 则是描述如何改变数据的。
在 dva 中,connect Model 的组件通过 props 可以访问到 dispatch,可以调用 Model 中的 Reducer 或者
Effects,常见的形式如:

dispatch({
type: 'user/add', // 如果在 model 外调用,需要添加 namespace
payload: {}, // 需要传递的信息
});

Reducer
type Reducer = (state: S, action: A) => S
Reducer(也称为 reducing function)函数接受两个参数:之前已经累积运算的结果和当前要被累积的值,返
回的是一个新的累积结果。该函数把一个集合归并成一个单值。
Reducer 的概念来自于是函数式编程,很多语言中都有 reduce API。如在 javascript 中:

[{x:1},{y:2},{z:3}].reduce(function(prev, next){
return Object.assign(prev, next);
})
//return {x:1, y:2, z:3}

在 dva 中,reducers 聚合积累的结果是当前 model 的 state 对象。通过 actions 中传入的值,与当前


reducers 中的值进行运算获得新的值(也就是新的 state)。需要注意的是 Reducer 必须是纯函数,所以同样的
输入必然得到同样的输出,它们不应该产生任何副作用。并且,每一次的计算都应该使用immutable data,这
种特性简单理解就是每次操作都是返回一个全新的数据(独立,纯净),所以热重载和时间旅行这些功能才能够
使用。

Effect

Effect 被称为副作用,在我们的应用中,最常见的就是异步操作。它来自于函数编程的概念,之所以叫副作用是
因为它使得我们的函数变得不纯,同样的输入不一定获得同样的输出。

本文档使用 看云 构建 - 137 -
Dva数据流

dva 为了控制副作用的操作,底层引入了redux-sagas做异步流程控制,由于采用了generator的相关概念,所以
将异步转成同步写法,从而将effects转为纯函数。至于为什么我们这么纠结于纯函数,如果你想了解更多可以阅
读Mostly adequate guide to FP,或者它的中文译本JS函数式编程指南。

Subscription

Subscriptions 是一种从源获取数据的方法,它来自于 elm。


Subscription 语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。数据源可以是当前的
时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。

import key from 'keymaster';


...
app.model({
namespace: 'count',
subscriptions: {
keyEvent({dispatch}) {
key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
},
}
});

Router

这里的路由通常指的是前端路由,由于我们的应用现在通常是单页应用,所以需要前端代码来控制路由逻辑,通
过浏览器提供的History API可以监听浏览器url的变化,从而控制路由相关操作。
dva 实例提供了 router 方法来控制路由,使用的是react-router。

import { Router, Route } from 'dva/router';


app.router(({history}) =>
<Router history={history}>
<Route path="/" component={HomePage} />
</Router>
);

Route Components

在组件设计方法中,我们提到过 Container Components,在 dva 中我们通常将其约束为 Route

Components,因为在 dva 中我们通常以页面维度来设计 Container Components。


所以在 dva 中,通常需要 connect Model的组件都是 Route Components,组织在 /routes/ 目录下,而

/components/ 目录下则是纯组件(Presentational Components)。

本文档使用 看云 构建 - 138 -
Dva数据流

参考

redux docs

redux docs 中文
Mostly adequate guide to FP

JS函数式编程指南
choo docs

elm

原文出处

地址:Dva 概念

本文档使用 看云 构建 - 139 -
基于数据流改造CRUD

基于数据流改造CRUD
数据流准备
数据流列表页

数据流新增页
数据流修改页
数据流详情页

本文档使用 看云 构建 - 140 -
数据流准备

数据流准备
数据流准备

1. 了解了dva数据流(基于redux二次封装)之后,我们来实战改造下之前写的模块
2. 到src/models下新建demo.js文件,内容如下

import { list } from '../services/demo';

export default {
namespace: 'demo',
state: {
data: {
list: [],
pagination: {},
},
detail: {},
},
effects: {
*fetchList({ payload }, { call, put }) {
const response = yield call(list, payload);
if (response.success) {
yield put({
type: 'saveList',
payload: {
list: response.data.records,
pagination: {
total: response.data.total,
current: response.data.current,
pageSize: response.data.size,
},
},
});
}
},
},
reducers: {
saveList(state, action) {
return {
...state,
data: action.payload,
};
},
},
};

本文档使用 看云 构建 - 141 -
数据流准备

3. 新建到model文件由四个部分组成,分别是:namespace、state、effects、reducers
4. namespace定义了model的命名空间,外部可通过对应的值检索到;
5. state是model内的状态,供外部调用和内部组织结构;

6. effects主要提供了异步操作,被dispatch函数调用,请求api并将返回数据交由reduceers处理并更新state;
7. reducers主要用于接受effects传递过来的数据并进行加工操作,最后更新到state中

effects中用到了es6的生成器函数,更多概念请移步:https://www.jianshu.com/p/e0778b004596

本文档使用 看云 构建 - 142 -
数据流列表页

数据流列表页
改造列表页

1. 基于dva数据流改造需要做如下修改

增加connect

@connect(({ demo, loading }) => ({


demo,
loading: loading.models.demo,
}))
@Form.create()
class Demo extends PureComponent {

修改handelSearch方法,使用dispatch调用数据流

handleSearch = params => {


const { dispatch } = this.props;
dispatch({
type: 'demo/fetchList',
payload: params,
});
};

删除Demo中的state的data字段并且将render方法内从state获取date的代码替换为从props获取,其中的

demo则是从connect连接到model内的namespace名,demo.data则对应model内state的data字段。

state = {
selectedRows: [],
};

render() {
const code = 'demo';

const {
form,
loading,
demo: { data },
} = this.props;
...............

本文档使用 看云 构建 - 143 -
数据流列表页

2. 完整代码如下

import React, { PureComponent } from 'react';


import { Button, Col, Form, Input, message, Row } from 'antd';
import { connect } from 'dva';
import Grid from '../../../components/Sword/Grid';
import Panel from '../../../components/Panel';

const FormItem = Form.Item;

@connect(({ demo, loading }) => ({


demo,
loading: loading.models.demo,
}))
@Form.create()
class Demo extends PureComponent {
state = {
selectedRows: [],
};

onSelectRow = rows => {


this.setState({
selectedRows: rows,
});
};

getSelectKeys = () => {
const { selectedRows } = this.state;
return selectedRows.map(row => row.id);
};

// ============ 查询 ===============
handleSearch = params => {
const { dispatch } = this.props;
dispatch({
type: 'demo/fetchList',
payload: params,
});
};

// ============ 查询表单 ===============


renderSearchForm = onReset => {
const { form } = this.props;
const { getFieldDecorator } = form;

return (
<Row gutter={{ md: 8, lg: 24, xl: 48 }}>

本文档使用 看云 构建 - 144 -
数据流列表页

<Col md={6} sm={24}>


<FormItem label="标题">
{getFieldDecorator('title')(<Input placeholder="请输入标题" />)}
</FormItem>
</Col>
<Col>
<div style={{ float: 'right' }}>
<Button type="primary" htmlType="submit">
查询
</Button>
<Button style={{ marginLeft: 8 }} onClick={onReset}>
重置
</Button>
</div>
</Col>
</Row>
);
};

test = () => {
const keys = this.getSelectKeys();
if (keys.length === 0) {
message.warn('请先选择一条数据!');
} else {
console.log(`已选择数据id:${keys}`);
}
};

renderLeftButton = () => (
<Button icon="tool" onClick={this.test}>
测试
</Button>
);

render() {
const code = 'demo';

const {
form,
loading,
demo: { data },
} = this.props;

const columns = [
{
title: '标题',
dataIndex: 'title',
},
{
title: '内容',

本文档使用 看云 构建 - 145 -
数据流列表页

dataIndex: 'content',
},
{
title: '时间',
dataIndex: 'date',
},
];

return (
<Panel>
<Grid
code={code}
form={form}
onSearch={this.handleSearch}
onSelectRow={this.onSelectRow}
renderSearchForm={this.renderSearchForm}
renderLeftButton={this.renderLeftButton}
loading={loading}
data={data}
columns={columns}
/>
</Panel>
);
}
}
export default Demo;

3. 打开系统,查看查询之后调用的接口及传递参数都正确。

4. 如此一来就已经将api调用、数据处理彻底解耦到了model 中。Demo页面只需关注UI与数据的绑定。

本文档使用 看云 构建 - 146 -
数据流新增页

数据流新增页
改造新增页

1. 基于dva数据流改造需要做如下修改

model修改为如下代码

import { message } from 'antd';


import router from 'umi/router';
import { list, submit } from '../services/demo';

export default {
namespace: 'demo',
state: {
data: {
list: [],
pagination: {},
},
detail: {},
},
effects: {
*fetchList({ payload }, { call, put }) {
const response = yield call(list, payload);
if (response.success) {
yield put({
type: 'saveList',
payload: {
list: response.data.records,
pagination: {
total: response.data.total,
current: response.data.current,
pageSize: response.data.size,
},
},
});
}
},
*submit({ payload }, { call }) {
const response = yield call(submit, payload);
if (response.success) {
message.success('提交成功');
router.push('/platform/demo');
}
},
},
reducers: {

本文档使用 看云 构建 - 147 -
数据流新增页

saveList(state, action) {
return {
...state,
data: action.payload,
};
},
},
};

增加connect

@connect(({ demo, loading }) => ({


demo,
loading: loading.models.demo,
}))
@Form.create()
class DemoAdd extends PureComponent {

修改handelSubmit,使用dispatch调用数据流进行表单提交

handleSubmit = e => {
e.preventDefault();
const { form, dispatch } = this.props;
form.validateFieldsAndScroll((err, values) => {
if (!err) {
const params = {
...values,
date: func.format(values.date),
};
dispatch({
type: 'demo/submit',
payload: params,
});
}
});
};

2. 打开系统提交数据,发现接口传参调用完全正确

本文档使用 看云 构建 - 148 -
数据流新增页

3. 最后提供新增页的完整代码

import React, { PureComponent } from 'react';


import { Form, Input, Card, Button, DatePicker } from 'antd';
import { connect } from 'dva';
import moment from 'moment';
import Panel from '../../../components/Panel';
import styles from '../../../layouts/Sword.less';
import func from '../../../utils/Func';

const FormItem = Form.Item;


const { TextArea } = Input;

@connect(({ demo, loading }) => ({


demo,
loading: loading.models.demo,
}))
@Form.create()
class DemoAdd extends PureComponent {
handleSubmit = e => {
e.preventDefault();
const { form, dispatch } = this.props;
form.validateFieldsAndScroll((err, values) => {
if (!err) {
const params = {
...values,
date: func.format(values.date),
};
dispatch({

本文档使用 看云 构建 - 149 -
数据流新增页

type: 'demo/submit',
payload: params,
});
}
});
};

render() {
const {
form: { getFieldDecorator },
} = this.props;

const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 7 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 },
md: { span: 10 },
},
};

const action = (
<Button type="primary" onClick={this.handleSubmit}>
提交
</Button>
);

return (
<Panel title="新增" back="/platform/demo" action={action}>
<Form hideRequiredMark style={{ marginTop: 8 }}>
<Card title="基本信息" className={styles.card} bordered={false}>
<FormItem {...formItemLayout} label="标题">
{getFieldDecorator('title', {
rules: [
{
required: true,
message: '请输入标题',
},
],
})(<Input placeholder="请输入标题" />)}
</FormItem>
<FormItem {...formItemLayout} label="日期">
{getFieldDecorator('date', {
rules: [
{
required: true,
message: '请输入日期',

本文档使用 看云 构建 - 150 -
数据流新增页

},
],
})(
<DatePicker
style={{ width: '100%' }}
format="YYYY-MM-DD HH:mm:ss"
disabledDate={this.disabledDate}
showTime={{ defaultValue: moment('00:00:00', 'HH:mm:ss') }}
/>
)}
</FormItem>
<FormItem {...formItemLayout} label="内容">
{getFieldDecorator('content')(
<TextArea style={{ minHeight: 32 }} placeholder="请输入内容" rows
={10} />
)}
</FormItem>
</Card>
</Form>
</Panel>
);
}
}

export default DemoAdd;

本文档使用 看云 构建 - 151 -
数据流修改页

数据流修改页
改造修改页

1. 基于dva数据流改造需要做如下修改

model修改为如下代码

import { message } from 'antd';


import router from 'umi/router';
import { list, detail, submit } from '../services/demo';

export default {
namespace: 'demo',
state: {
data: {
list: [],
pagination: {},
},
detail: {},
},
effects: {
*fetchList({ payload }, { call, put }) {
const response = yield call(list, payload);
if (response.success) {
yield put({
type: 'saveList',
payload: {
list: response.data.records,
pagination: {
total: response.data.total,
current: response.data.current,
pageSize: response.data.size,
},
},
});
}
},
*fetchDetail({ payload }, { call, put }) {
const response = yield call(detail, payload);
if (response.success) {
yield put({
type: 'saveDetail',
payload: {
detail: response.data,
},
});

本文档使用 看云 构建 - 152 -
数据流修改页

}
},
*submit({ payload }, { call }) {
const response = yield call(submit, payload);
if (response.success) {
message.success('提交成功');
router.push('/platform/demo');
}
},
},
reducers: {
saveList(state, action) {
return {
...state,
data: action.payload,
};
},
saveDetail(state, action) {
return {
...state,
detail: action.payload.detail,
};
},
},
};

增加connect

@connect(({ demo, loading }) => ({


demo,
loading: loading.models.demo,
}))
@Form.create()
class DemoEdit extends PureComponent {

删掉state,修改componentWillMount,使用dispatch调用数据流进行数据加载

componentWillMount() {
const {
dispatch,
match: {
params: { id },
},
} = this.props;

本文档使用 看云 构建 - 153 -
数据流修改页

dispatch({
type: 'demo/fetchDetail',
payload: { id },
});
}

修改handelSubmit,使用dispatch调用数据流进行表单提交

handleSubmit = e => {
e.preventDefault();
const {
form,
dispatch,
match: {
params: { id },
},
} = this.props;
form.validateFieldsAndScroll((err, values) => {
if (!err) {
const params = {
id,
...values,
date: func.format(values.date),
};
dispatch({
type: 'demo/submit',
payload: params,
});
}
});
};

修改render下原先state中data的获取,改为从props获取并将data改为detail

const {
form: { getFieldDecorator },
demo: { detail },
} = this.props;

2. 打开系统,修改页数据加载成功

本文档使用 看云 构建 - 154 -
数据流修改页

3. 点击提交,接口调用与传参页正确

4. 附上完整代码

import React, { PureComponent } from 'react';


import { Form, Input, Card, Button, DatePicker } from 'antd';
import { connect } from 'dva';
import moment from 'moment';
import Panel from '../../../components/Panel';
import styles from '../../../layouts/Sword.less';
import func from '../../../utils/Func';

const FormItem = Form.Item;


const { TextArea } = Input;

@connect(({ demo, loading }) => ({


demo,
loading: loading.models.demo,
}))

本文档使用 看云 构建 - 155 -
数据流修改页

@Form.create()
class DemoEdit extends PureComponent {

componentWillMount() {
const {
dispatch,
match: {
params: { id },
},
} = this.props;
dispatch({
type: 'demo/fetchDetail',
payload: { id },
});
}

handleSubmit = e => {
e.preventDefault();
const {
form,
dispatch,
match: {
params: { id },
},
} = this.props;
form.validateFieldsAndScroll((err, values) => {
if (!err) {
const params = {
id,
...values,
date: func.format(values.date),
};
dispatch({
type: 'demo/submit',
payload: params,
});
}
});
};

render() {
const {
form: { getFieldDecorator },
demo: { detail },
} = this.props;

const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 7 },

本文档使用 看云 构建 - 156 -
数据流修改页

},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 },
md: { span: 10 },
},
};

const action = (
<Button type="primary" onClick={this.handleSubmit}>
提交
</Button>
);

return (
<Panel title="修改" back="/platform/demo" action={action}>
<Form hideRequiredMark style={{ marginTop: 8 }}>
<Card title="基本信息" className={styles.card} bordered={false}>
<FormItem {...formItemLayout} label="标题">
{getFieldDecorator('title', {
rules: [
{
required: true,
message: '请输入标题',
},
],
initialValue: detail.title,
})(<Input placeholder="请输入标题" />)}
</FormItem>
<FormItem {...formItemLayout} label="日期">
{getFieldDecorator('date', {
rules: [
{
required: true,
message: '请输入日期',
},
],
initialValue: moment(detail.date, 'YYYY-MM-DD HH:mm:ss'),
})(
<DatePicker
style={{ width: '100%' }}
format="YYYY-MM-DD HH:mm:ss"
showTime={{ defaultValue: moment('00:00:00', 'HH:mm:ss') }}
/>
)}
</FormItem>
<FormItem {...formItemLayout} label="内容">
{getFieldDecorator('content', {
initialValue: detail.content,
})(<TextArea style={{ minHeight: 32 }} placeholder="请输入内容" ro

本文档使用 看云 构建 - 157 -
数据流修改页

ws={10} />)}
</FormItem>
</Card>
</Form>
</Panel>
);
}
}

export default DemoEdit;

本文档使用 看云 构建 - 158 -
数据流详情页

数据流详情页
改造详情页

1. 基于dva数据流改造需要做如下修改

增加connect

@connect(({ demo, loading }) => ({


demo,
loading: loading.models.demo,
}))
@Form.create()
class DemoEdit extends PureComponent {

删掉state,修改componentWillMount,使用dispatch调用数据流进行数据加载

componentWillMount() {
const {
dispatch,
match: {
params: { id },
},
} = this.props;
dispatch({
type: 'demo/fetchDetail',
payload: { id },
});
}

修改render下原先state中data的获取,改为从props获取并将data改为detail

const {
demo: { detail },
} = this.props;

2. 打开系统,详情页数据加载成功

本文档使用 看云 构建 - 159 -
数据流详情页

3. 附上完整代码

import { connect } from 'dva';


import React, { PureComponent } from 'react';
import { Form, Card } from 'antd';
import Panel from '../../../components/Panel';
import styles from '../../../layouts/Sword.less';

const FormItem = Form.Item;

@connect(({ demo, loading }) => ({


demo,
loading: loading.models.demo,
}))
@Form.create()
class DemoEdit extends PureComponent {

componentWillMount() {
const {
dispatch,
match: {
params: { id },
},
} = this.props;
dispatch({
type: 'demo/fetchDetail',
payload: { id },

本文档使用 看云 构建 - 160 -
数据流详情页

});
}

render() {
const {
demo: { detail },
} = this.props;

const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 7 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 },
md: { span: 10 },
},
};

return (
<Panel title="查看" back="/platform/demo">
<Form hideRequiredMark style={{ marginTop: 8 }}>
<Card title="基本信息" className={styles.card} bordered={false}>
<FormItem {...formItemLayout} label="标题">
<span>{detail.title}</span>
</FormItem>
<FormItem {...formItemLayout} label="日期">
<span>{detail.date}</span>
</FormItem>
<FormItem {...formItemLayout} label="内容">
<span>{detail.content}</span>
</FormItem>
</Card>
</Form>
</Panel>
);
}
}

export default DemoEdit;

4. 附上model的完整代码

import { message } from 'antd';


import router from 'umi/router';
import { list, detail, submit } from '../services/demo';

本文档使用 看云 构建 - 161 -
数据流详情页

export default {
namespace: 'demo',
state: {
data: {
list: [],
pagination: {},
},
detail: {},
},
effects: {
*fetchList({ payload }, { call, put }) {
const response = yield call(list, payload);
if (response.success) {
yield put({
type: 'saveList',
payload: {
list: response.data.records,
pagination: {
total: response.data.total,
current: response.data.current,
pageSize: response.data.size,
},
},
});
}
},
*fetchDetail({ payload }, { call, put }) {
const response = yield call(detail, payload);
if (response.success) {
yield put({
type: 'saveDetail',
payload: {
detail: response.data,
},
});
}
},
*submit({ payload }, { call }) {
const response = yield call(submit, payload);
if (response.success) {
message.success('提交成功');
router.push('/platform/demo');
}
},
},
reducers: {
saveList(state, action) {
return {
...state,
data: action.payload,

本文档使用 看云 构建 - 162 -
数据流详情页

};
},
saveDetail(state, action) {
return {
...state,
detail: action.payload.detail,
};
},
},
};

结束语

经过整本手册的学习,带领大家由浅入深,见证了一个简单组件到复杂的诞生,也见证了一个增删改查模块
经过多次修改越来越精简清晰的过程

知识的海洋是无限的,大家若能掌握学习方法,相信以后会对React更加熟悉,写起系统来更加顺畅

本文档使用 看云 构建 - 163 -
构建和发布

构建和发布
项目构建
项目发布

本文档使用 看云 构建 - 164 -
项目构建

项目构建
如何构建

打包

1. 当业务开发完毕后,需要部署到服务器,这时候就需要执行打包

2. 进入Sword根目录,执行 npm run build ,等待打包完毕,我们会发现目录下多出了一个dist文件夹,


这个文件夹下的内容就是我们后续需要部署到web服务器的代码了

本文档使用 看云 构建 - 165 -
项目构建

本文档使用 看云 构建 - 166 -
项目构建

3. 由于 Ant Design Pro 使用的工具Umi已经将复杂的流程封装完毕,构建打包文件只需要一个命令


umi build ,构建打包成功之后,会在根目录生成 dist 文件夹,里面就是构建打包好的文件,通常
是 *.js 、 *.css 、 index.html 等静态文件。。
如果需要自定义构建,比如指定 dist 目录等,可以通过 config/config.js 进行配置,详情参看:
Umi 配置。

分析构建文件体积

如果你的构建文件很大,你可以通过 analyze 命令构建并分析依赖模块的体积分布,从而优化你的代码。

$ npm run analyze

上面的命令会自动在浏览器打开显示体积分布数据的网页。

本文档使用 看云 构建 - 167 -
项目发布

项目发布
发布

对于发布来讲,只需要将最终生成的静态文件,也就是通常情况下 dist 文件夹的静态文件发布到你的 cdn 或


者静态服务器即可,需要注意的是其中的 index.html 通常会是你后台服务的入口页面,在确定了 js 和 css
的静态之后可能需要改变页面的引入路径。

前端路由与服务端的结合

Ant Design Pro 使用的 Umi 支持两种路由方式: browserHistory 和 hashHistory 。

可以在 config/config.js 中进行配置选择用哪个方式:

export default {
history: 'hash', // 默认是 browser
}

hashHistory 使用如 https://cdn.com/#/users/123 这样的 URL,取井号后面的字符作为路径。


browserHistory 则直接使用 https://cdn.com/users/123 这样的 URL。使用 hashHistory 时
浏览器访问到的始终都是根目录下 index.html 。使用 browserHistory 则需要服务器做好处理 URL 的
准备,处理应用启动最初的 / 这样的请求应该没问题,但当用户来回跳转并在 /users/123 刷新时,服务
器就会收到来自 /users/123 的请求,这时你需要配置服务器能处理这个 URL 返回正确的 index.html

。如果你能控制服务端,我们推荐使用 browserHistory 。

使用 nginx

nginx 作为最流行的 web 容器之一,配置和使用相当简单,只要简单的配置就能拥有高性能和高可用。推荐使用


nginx 托管。示例配置如下:

server {
listen 80;
# gzip config
gzip on;
gzip_min_length 1k;
gzip_comp_level 9;
gzip_types text/plain application/javascript application/x-javascript text/
css application/xml text/javascript application/x-httpd-php image/jpeg image/gi
f image/png;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";

root /usr/share/nginx/html;

本文档使用 看云 构建 - 168 -
项目发布

location / {
# 用于配合 browserHistory使用
try_files $uri $uri/ /index.html;

# 如果有资源,建议使用 https + http2,配合按需加载可以获得更好的体验


# rewrite ^/(.*)$ https://preview.pro.ant.design/$1 permanent;

}
location /api {
proxy_pass https://preview.pro.ant.design;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
}
}
server {
# 如果有资源,建议使用 https + http2,配合按需加载可以获得更好的体验
listen 443 ssl http2 default_server;

# 证书的公私钥
ssl_certificate /path/to/public.crt;
ssl_certificate_key /path/to/private.key;

location / {
# 用于配合 browserHistory使用
try_files $uri $uri/ /index.html;

}
location /api {
proxy_pass https://preview.pro.ant.design;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
}
}

使用 spring boot

Spring Boot 是使用最多的 java 框架,只需要简单的几步就可以与 Ant Design Pro 进行整合。


首先运行 build
$ npm run build
然后将编译之后的文件复制到 spring boot 项目的 /src/main/resources/static 目录下。
重新启动项目,访问 http://localhost:8080/ 就可以看到效果。

为了方便做整合,最好使用 hash 路由。如果你想使用 browserHistory ,你创建一个 controller ,并添加如下


代码:

本文档使用 看云 构建 - 169 -
项目发布

@RequestMapping("/api/**")
public ApiResult api(HttpServletRequest request, HttpServletResponse response){
return apiProxy.proxy(request, reponse);
}

@RequestMapping(value="/**", method=HTTPMethod.GET)
public String index(){
return "index"
}

注意 pro 并没有提供 java 的 api 接口实现,如果只是为了预览 demo,可以使用反向代理到


https://preview.pro.ant.design 。

使用 express

express的例子

app.use(express.static(path.join(__dirname, 'build')));

app.get('/*', function (req, res) {


res.sendFile(path.join(__dirname, 'build', 'index.html'));
});

使用 egg

egg的例子

// controller
exports.index = function* () {
yield this.render('App.jsx', {
context: {
user: this.session.user,
},
});
};

// router
app.get('home', '/*', 'home.index');

关于路由更多可以参看Umi 的路由文档。

本文档使用 看云 构建 - 170 -

You might also like