Sword开发手册
Sword开发手册
Sword开发手册
录
序
快速开始
环境要求
环境准备
工程导入
工程运行
技术基础
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 致力于创造新颖的开发模式,将开发中遇到的痛点、生产中所踩的坑整理归纳,并将解决方案
都融合到框架中。
说明
本手册主要讲解前端技术栈,还需要后端的同学可移步: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等细颗粒度控制方案
工程结构
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
本文档使用 看云 构建 - 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模式讲解,照顾到没有后端基础的同学
命令行运行
2. 运行完毕后浏览器会自动打开,看到如下界面则说明运行成功
本文档使用 看云 构建 - 26 -
工程运行
IDE运行
本文档使用 看云 构建 - 27 -
工程运行
本文档使用 看云 构建 - 28 -
工程运行
本文档使用 看云 构建 - 29 -
技术基础
技术基础
React
ES6
DvaJS
UmiJS
AntDesign
本文档使用 看云 构建 - 30 -
React
React
简介
学习资料
React入门 :https://segmentfault.com/a/1190000012921279
本文档使用 看云 构建 - 31 -
ES6
ES6
简介
学习资料
ECMAScript 6 入门 : http://es6.ruanyifeng.com/#README
本文档使用 看云 构建 - 32 -
DvaJS
DvaJS
简介
特性
学习资料
Dva文档:https://dvajs.com/guide/
本文档使用 看云 构建 - 33 -
UmiJS
UmiJS
简介
务好外部用户。
特性
开箱即用,内置 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文档: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. 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组件
本文档使用 看云 构建 - 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 -
自定义组件
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>
);
}
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>
);
}
改为
render() {
const { children, print } = this.props;
return (
<div>
<Button type="primary" onClick={this.handleSubmit(print)}>
{children}
</Button>
</div>
);
}
4. 打开系统点击按钮测试,发现点击并没有效果
本文档使用 看云 构建 - 55 -
自定义组件
指针会指向错误,所以需要处理一下
6. 将代码改为如下:
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,内容如下
调用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 状态机
简介
拓展ButtonX组件
1. 加入state机制,完整代码如下
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. 需要注意的有三点:
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. 系统右上角切换成因为,发现整个菜单也统一切换成了英文
增加按钮接口返回数据
本文档使用 看云 构建 - 84 -
模块准备
1. 到mock文件夹找到menu.js,加入红框内的json数据
2. 第一层code则是对应菜单模块的code,后续列表也传入这个code,就可以检索到他所属的按钮集合从而显
示在页面上了
3. 下面我们就开始编写一个基础的crud模块吧!
本文档使用 看云 构建 - 85 -
列表页
列表页
加载数据
查询功能
按钮加载原理
自定义按钮
本文档使用 看云 构建 - 86 -
列表页
加载数据
新建列表页面
1. 我们先做一个最简单的列表页面
2. 列表页面完整代码如下:
@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 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,是为了将所用到的组件、封装都引入进来进行页面的渲染使用
5. 接下来是定义整个类,进行完整的导出,用作路由发现并渲染页面。FormItem定义后可直接变为可引用的标
签 简化了代码
@Form.create()
class Demo extends PureComponent {
6. renderSearchForm方法封装了搜索模块,只需定义搜索模块的表单元素并传入到下面封装的Grid组件,即
可自动生成查询重置等功能,更复杂的功能,都可以通过这个函数进行拓展
本文档使用 看云 构建 - 89 -
列表页
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() {
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 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显示的数据必须是动态的对接接口的,所
以我们下面来学习下如何将数据对接接口,让他活起来。
对接列表接口
本文档使用 看云 构建 - 92 -
列表页
2. 打开postman调用mock接口查看返回成功
本文档使用 看云 构建 - 93 -
列表页
本文档使用 看云 构建 - 94 -
列表页
3. 我们到services文件夹下定义这个新增的接口
4. 优化列表页代码,加入接口动态对接
@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,
},
},
});
});
}
return (
<Row gutter={{ md: 8, lg: 24, xl: 48 }}>
本文档使用 看云 构建 - 95 -
列表页
render() {
const code = 'demo';
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 返回
本文档使用 看云 构建 - 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);
}
本文档使用 看云 构建 - 99 -
列表页
5. 点击重置按钮,列表又回归原样
6. 这样一来,一个简单的查询列表就做好了,未来只需将mock数据源换成真实的服务端api即可,无需再次开
发,非常方便
本文档使用 看云 构建 - 100 -
列表页
按钮加载原理
按钮加载逻辑
1. 整个列表页只有一个Grid组件,却动态加载出了按钮,我们不妨看一看这个组件是如何实现的
2. 点进Grid组件,拉到最底下查看render返回,发现封装了3个子组件
本文档使用 看云 构建 - 101 -
列表页
formValues: {},
selectedRows: [],
buttons: getButton(props.code),
};
}
}
6. 进入ToolBar查看源码,发现其实很简单,就是根据buttons数据集合动态生成了按钮。当中有一点需要注意
的是 buttons.filter(button => button.action === 1 || button.action === 3) ,他
根据action字段来判断,这个action代表按钮类型:1:只有工具栏才出现;2:只有表格行才出现;3:两者
都出现。
本文档使用 看云 构建 - 102 -
列表页
}
}
本文档使用 看云 构建 - 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 -
列表页
自定义按钮
自定义按钮
test = () => {
console.log("测试按钮生成")
}
renderLeftButton = () => (
<Button icon="tool" onClick={this.test}>
测试
</Button>
);
3. 在Grid组件中增加属性: renderLeftButton={this.renderLeftButton}
<Grid
code={code}
..............
renderLeftButton={this.renderLeftButton}
..............
/>
4. 打开系统查看,发现多了一个按钮,并且点击后也执行了自定义的方法
本文档使用 看云 构建 - 105 -
列表页
自定义按钮获取表格信息
1. 修改 state
state = {
data: {},
selectedRows: [],
};
2. 增加表格点击事件,将所选行数据加入state中
本文档使用 看云 构建 - 106 -
列表页
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 -
列表页
componentWillMount 方法,无需重复写代码。
后记
到这里整个列表页的基础讲解就结束了,相信大家多看示例模块的写法,定能熟练掌握
最后附上列表页的完整代码
@Form.create()
class Demo extends PureComponent {
state = {
data: {},
selectedRows: [],
};
getSelectKeys = () => {
const { selectedRows } = this.state;
return selectedRows.map(row => row.id);
};
// ============ 查询 ===============
本文档使用 看云 构建 - 108 -
列表页
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 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
@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>
);
}
}
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', {
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返回
const proxy = {
'GET /api/demo/list': getFakeList,
'GET /api/demo/detail': getFakeDetail,
'POST /api/demo/submit': fakeSuccess,
'POST /api/demo/remove': fakeSuccess,
};
2. 优化handelSubmit方法,增加submit方法传入表单数据values供api调用
handleSubmit = e => {
e.preventDefault();
const { form } = this.props;
本文档使用 看云 构建 - 115 -
新增页
3. 打开系统点击提交发现提交成功,network监听传递参数无误
化下提交代码
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. 最后以下附上完整代码
@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>
);
}
}
本文档使用 看云 构建 - 119 -
修改页
修改页
创建修改页
1. 修改页元素与新增差不多,不同的是修改页面需要在组件加载的时候获取数据,从而绑定在页面上
2. 将上一节的新增页代码过来稍作修改
@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 -
修改页
对接接口
1. 修改detail方法返回,模拟真实场景
本文档使用 看云 构建 - 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. 打开系统,点击修改查看数据显示成功
本文档使用 看云 构建 - 125 -
修改页
8. 点击提交查看控制台,数据也传递正确
全部代码一览
本文档使用 看云 构建 - 126 -
修改页
@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 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 -
修改页
本文档使用 看云 构建 - 129 -
详情页
详情页
创建详情页
1. 我们把修改页完成后,详情页就简单多了,只是一个查看功能。没有数据提交、数据校验等功能
2. 我们复制修改页,做如下修改,将组件都变更为span类型
@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 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>
);
}
}
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 数据,但是通常你很少会用到:
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}
Effect
Effect 被称为副作用,在我们的应用中,最常见的就是异步操作。它来自于函数编程的概念,之所以叫副作用是
因为它使得我们的函数变得不纯,同样的输入不一定获得同样的输出。
本文档使用 看云 构建 - 137 -
Dva数据流
dva 为了控制副作用的操作,底层引入了redux-sagas做异步流程控制,由于采用了generator的相关概念,所以
将异步转成同步写法,从而将effects转为纯函数。至于为什么我们这么纠结于纯函数,如果你想了解更多可以阅
读Mostly adequate guide to FP,或者它的中文译本JS函数式编程指南。
Subscription
Router
这里的路由通常指的是前端路由,由于我们的应用现在通常是单页应用,所以需要前端代码来控制路由逻辑,通
过浏览器提供的History API可以监听浏览器url的变化,从而控制路由相关操作。
dva 实例提供了 router 方法来控制路由,使用的是react-router。
Route 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文件,内容如下
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
修改handelSearch方法,使用dispatch调用数据流
删除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. 完整代码如下
getSelectKeys = () => {
const { selectedRows } = this.state;
return selectedRows.map(row => row.id);
};
// ============ 查询 ===============
handleSearch = params => {
const { dispatch } = this.props;
dispatch({
type: 'demo/fetchList',
payload: params,
});
};
return (
<Row gutter={{ md: 8, lg: 24, xl: 48 }}>
本文档使用 看云 构建 - 144 -
数据流列表页
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修改为如下代码
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
修改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. 最后提供新增页的完整代码
本文档使用 看云 构建 - 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>
);
}
}
本文档使用 看云 构建 - 151 -
数据流修改页
数据流修改页
改造修改页
1. 基于dva数据流改造需要做如下修改
model修改为如下代码
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
删掉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. 附上完整代码
本文档使用 看云 构建 - 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>
);
}
}
本文档使用 看云 构建 - 158 -
数据流详情页
数据流详情页
改造详情页
1. 基于dva数据流改造需要做如下修改
增加connect
删掉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. 附上完整代码
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>
);
}
}
4. 附上model的完整代码
本文档使用 看云 构建 - 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. 当业务开发完毕后,需要部署到服务器,这时候就需要执行打包
本文档使用 看云 构建 - 165 -
项目构建
本文档使用 看云 构建 - 166 -
项目构建
分析构建文件体积
上面的命令会自动在浏览器打开显示体积分布数据的网页。
本文档使用 看云 构建 - 167 -
项目发布
项目发布
发布
前端路由与服务端的结合
export default {
history: 'hash', // 默认是 browser
}
。如果你能控制服务端,我们推荐使用 browserHistory 。
使用 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;
}
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
本文档使用 看云 构建 - 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"
}
使用 express
express的例子
app.use(express.static(path.join(__dirname, 'build')));
使用 egg
egg的例子
// controller
exports.index = function* () {
yield this.render('App.jsx', {
context: {
user: this.session.user,
},
});
};
// router
app.get('home', '/*', 'home.index');
关于路由更多可以参看Umi 的路由文档。
本文档使用 看云 构建 - 170 -