Ant Design Pro 是一个企业级中后台前端/设计解决方案。本地环境需要安装 node 和 git,技术栈基于 ES2015+、React、dva、g2 和 antd。
https://github.com/ant-design/ant-design-pro/blob/master/README.zh-CN.md
https://pro.ant.design/docs/getting-started-cn
1、预备知识
1)Redux 是 JavaScript 状态容器,提供可预测化的状态管理;Redux 除了和 React 一起用外,还支持其它界面库。
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]):
连接 React 组件与 Redux store。
[mapStateToProps(state, [ownProps]): stateProps
] (Function): 如果定义该参数,组件将会监听 Redux store 的变化。任何时候,只要 Redux store 发生改变,mapStateToProps
函数就会被调用。该回调函数必须返回一个纯对象,这个对象会与组件的 props 合并。
- 函数将被调用两次。第一次是设置参数,第二次是组件与 Redux store 连接:
connect(mapStateToProps, mapDispatchToProps, mergeProps)(MyComponent)
。 - connect 函数不会修改传入的 React 组件,返回的是一个新的已与 Redux store 连接的组件,而且你应该使用这个新组件。
mapStateToProps
函数接收整个 Redux store 的 state 作为 props,然后返回一个传入到组件 props 的对象。
注入 dispatch
和 todos
function mapStateToProps(state) {
return { todos: state.todos }
}
export default connect(mapStateToProps)(TodoApp)
// 注入 dispatch 和全局 state
export default connect(state => state)(TodoApp)
// 不要这样做!这会导致每次 action 都触发整个 TodoApp 重新渲染
// 最好在多个组件上使用 connect(),每个组件只监听它所关联的部分 state。
Action 是把数据从应用(这里之所以不叫 view 是因为这些数据有可能是服务器响应,用户输入或其它非 view 的数据 )传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch()
将 action 传到 store。
Action 本质上是 JavaScript 普通对象。我们约定,action 内必须使用一个字符串类型的 type
字段来表示将要执行的动作。
2)redux-saga
是一个 redux 中间件,意味着这个线程可以通过正常的 redux action 从主应用程序启动,暂停和取消,它能访问完整的 redux state,也可以 dispatch redux action。
redux-saga 使用了 ES6 的 Generator 功能,让异步的流程更易于读取,写入和测试。通过这样的方式,这些异步的流程看起来就像是标准同步的 Javascript 代码。
effects: {
*create({ payload: values }, { call, put }) {
yield call(usersService.create, values);
yield put({ type: 'reload' });
},
*reload(action, { put, select }) {
const page = yield select(state => state.users.page);
yield put({ type: 'fetch', payload: { page } });
},
}
call(fn, ...args)
创建一个 Effect 描述信息,用来命令 middleware 以参数 args
调用函数 fn
。
-
fn: Function
- 一个 Generator 函数, 也可以是一个返回 Promise 或任意其它值的普通函数。 -
args: Array<any>
- 传递给fn
的参数数组。
put(action)
创建一个 Effect 描述信息,用来命令 middleware 向 Store 发起一个 action。 这个 effect 是非阻塞型的,并且所有向下游抛出的错误(例如在 reducer 中),都不会冒泡回到 saga 当中。
select(selector, ...args)
创建一个 Effect,用来命令 middleware 在当前 Store 的 state 上调用指定的选择器。
selector: Function
- 一个(state, ...args) => args
的函数。它接受当前 state 和一些可选参数,并返回当前 Store state 上的一部分数据。
2、dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。
dva 是基于现有应用架构 (redux + react-router + redux-saga 等)的一层轻量封装,没有引入任何新概念。dva 帮你自动化了Redux 架构一些繁琐的步骤,比如redux store 的创建,中间件的配置,路由的初始化等等,只需写几行代码就可以实现上述步骤。
1)使用 antd
通过 npm 安装 antd
和 babel-plugin-import
,babel-plugin-import
是用来按需加载 antd 的脚本和样式的;编辑 .webpackrc
,使 babel-plugin-import
插件生效。
// .webpackrc.js
extraBabelPlugins: [['import', { libraryName: 'antd', libraryDirectory: 'es', style: true }]]
2)dva应用
// src/index.js 入口js
import dva from 'dva';
import browserHistory from 'history/createBrowserHistory';
import createLoading from 'dva-loading';
// 1. Initialize
const app = dva({
history: browserHistory(),
});
// 2. Plugins
app.use(createLoading());
// 3. Model
app.model(require('./models/global').default);
app.model(require('./models/menu').default);
// 4. Router
app.router(require('./router').default);
// 5. Start
app.start('#root'); // 启动应用
app = dva(opts)-》
创建应用,返回 dva 实例。(注:dva 支持多实例)
opts
包含:
-
history
:指定给路由用的 history,默认是hashHistory
2)定义路由
app.router(({ history, app }) => RouterConfig)
注册路由表,推荐把路由信息抽成一个单独的文件,这样结合 babel-plugin-dva-hmr 可实现路由和组件的热加载(只更新页面修改的部分,不会刷新整个页面)。
// .webpackrc.js
env: {
development: {
extraBabelPlugins: ['dva-hmr'],
},
},
3)定义 Model(处理数据和逻辑)
dva 通过 model 的概念把一个领域的模型管理起来,包含同步更新 state 的 reducers,处理异步逻辑的 effects,订阅数据源的 subscriptions 。
import * as usersService from '../services/users';
export default {
namespace: 'users',
state: {
list: [],
total: null,
page: null,
},
reducers: {
save(state, { payload: { data: list, total, page } }) {
return { ...state, list, total, page };
},
},
effects: {
*fetch({ payload: { page = 1 } }, { call, put }) {
const { data, headers } = yield call(usersService.fetch, { page });
yield put({
type: 'save',
payload: {
data,
total: parseInt(headers['x-total-count'], 10),
page: parseInt(page, 10),
},
});
},
*remove({ payload: id }, { call, put }) {
yield call(usersService.remove, id);
yield put({ type: 'reload' });
},*reload(action, { put, select }) {
const page = yield select(state => state.users.page);
yield put({ type: 'fetch', payload: { page } });
},
},
subscriptions: {
setup({ dispatch, history }) {
return history.listen(({ pathname, query }) => {
if (pathname === '/users') {
dispatch({ type: 'fetch', payload: query });
}
});
},
},
};
namespace:model 的命名空间,同时也是他在全局 state 上的属性
state:初始值
reducers:以 key/value 格式定义 reducer。用于处理同步操作,唯一可以修改 state
的地方。由 action
触发
effects:以 key/value 格式定义 effect。用于处理异步操作和业务逻辑,不直接修改 state
。由 action
触发,可以触发 action
,可以和服务器交互,可以获取全局 state
的数据等等。
subscriptions:以 key/value 格式定义 subscription。subscription 是订阅,用于订阅一个数据源,然后根据需要 dispatch 相应的 action。在 app.start()
时被执行,数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。
app.model(model)-》
注册 model
4)编写UI Component并connect起来
import React from 'react';
import { connect } from 'dva';
import { Table, Pagination, Popconfirm, Button } from 'antd';
import { routerRedux } from 'dva/router';
import styles from './Users.css';
import { PAGE_SIZE } from '../../../../constants';
import UserModal from './UserModal';
function Users({ dispatch, list: dataSource, loading, total, page: current }) {
function deleteHandler(id) {
dispatch({
type: 'users/remove',
payload: id,
});
}
function pageChangeHandler(page) {
dispatch(
routerRedux.push({
pathname: '/users',
query: { page },
})
);
}
const columns = [
{
title: 'Username',
dataIndex: 'username',
key: 'username',
render: text => <a href="">{text}</a>,
},
{
title: 'Street',
dataIndex: 'address.street',
key: 'street',
},
{
title: 'Website',
dataIndex: 'website',
key: 'website',
},
{
title: 'Operation',
key: 'operation',
render: (text, record) => (
<span className={styles.operation}>
<Popconfirm title="Confirm to delete?" onConfirm={deleteHandler.bind(null, record.id)}>
<a href="">Delete</a>
</Popconfirm>
</span>
),
},
];
return (
<div className={styles.normal}>
<div>
<Table
columns={columns}
dataSource={dataSource}
loading={loading}
rowKey={record => record.id}
pagination={false}
/>
<Pagination
className="ant-table-pagination"
total={total}
current={current}
pageSize={PAGE_SIZE}
onChange={pageChangeHandler}
/>
</div>
</div>
);
}
function mapStateToProps(state) {
const { list, total, page } = state.users;
return {
loading: state.loading.models.users,
list,
total,
page,
};
}
export default connect(mapStateToProps)(Users);
5)相关概念
dva 提供了 connect 方法,这个 connect 就是 react-redux 的 connect 。 connect 方法返回的也是一个 React 组件,通常称为容器组件。因为它是原始 UI 组件的容器,即在外面包了一层 State。connect 方法传入的第一个参数是 mapStateToProps 函数,mapStateToProps 函数会返回一个对象,用于建立 State 到 Props 的映射关系。
数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch
发起一个 action,如果是同步行为会直接通过 Reducers
改变 State
,如果是异步行为(副作用)会先触发 Effects
然后流向 Reducers
最终改变 State。
Model 对象的属性
- namespace: 当前 Model 的名称。整个应用的 State,由多个小的 Model 的 State 以 namespace 为 key 合成
- state: 该 Model 当前的状态。数据保存在这里,直接决定了视图层的输出
- reducers: Action 处理器,处理同步动作,用来算出最新的 State
- effects:Action 处理器,处理异步动作
Action 是一个普通 javascript 对象,它是改变 State 的唯一途径。无论是从 UI 事件、网络回调,还是 WebSocket 等数据源所获得的数据,最终都会通过 dispatch 函数调用一个 action,从而改变对应的数据。action 必须带有 type
属性指明具体的行为,其它字段可以自定义,如果要发起一个 action 需要使用 dispatch
函数;需要注意的是 dispatch
是在组件 connect Models以后,通过 props 传入的。在 dva 中,connect Model 的组件通过 props 可以访问到 dispatch,可以调用 Model 中的 Reducer 或者 Effects
dispatch({
type: 'user/add', // 如果在 model 外调用,需要添加 namespace
payload: {}, // 需要传递的信息
});
Reducer函数接受两个参数:之前已经累积运算的结果和当前要被累积的值,返回的是一个新的累积结果。在 dva 中,reducers 聚合积累的结果是当前 model 的 state 对象。通过 actions 中传入的值,与当前 reducers 中的值进行运算获得新的值(也就是新的 state)。
state: {
list: [],
total: null,
page: null,
},
reducers: {
save(state, { payload: { data: list, total, page } }) {
return { ...state, list, total, page };
},
}
Effect:Action 处理器,处理异步动作,基于 Redux-saga 实现。Effect 指的是副作用。根据函数式编程,计算以外的操作都属于 Effect,典型的就是 I/O 操作、数据库读写。
dva 提供多个 effect 函数内部的处理函数,比较常用的是 call
和 put
。
- call:执行异步函数
- put:发出一个 Action,类似于 dispatch
effects: {
*create({ payload: values }, { call, put }) {
yield call(usersService.create, values);
yield put({ type: 'reload' });
},
*reload(action, { put, select }) {
const page = yield select(state => state.users.page);
yield put({ type: 'fetch', payload: { page } });
},
}
Router:这里的路由通常指的是前端路由,由于我们的应用现在通常是单页应用,所以需要前端代码来控制路由逻辑,通过浏览器提供的 History API 可以监听浏览器url的变化,从而控制路由相关操作。
dva 实例提供了 router 方法来控制路由,使用的是react-router。
在组件设计方法中,我们提到过 Container Components,在 dva 中我们通常将其约束为 Route Components,因为在 dva 中我们通常以页面维度来设计 Container Components。
所以在 dva 中,通常需要 connect Model的组件都是 Route Components,组织在/routes/
目录下,而/components/
目录下则是纯组件。
组件设计
React 应用是由一个个独立的 Component 组成的,我们在拆分 Component 的过程中要尽量让每个 Component 专注做自己的事。
一般来说,我们的组件有两种设计:Container Component、Presentational Component
- Container Component
Container Component 一般指的是具有监听数据行为
的组件,一般来说它们的职责是绑定相关联的 model 数据
,以数据容器的角色包含其它子组件。
- Presentational Component
它不会关联订阅 model 上的数据,而所需数据的传递则是通过 props 传递到组件内部。
对组件分类,主要有两个好处:让项目的数据处理更加集中;让组件高内聚低耦合,更加聚焦;
试想如果每个组件都去订阅数据 model,那么一方面组件本身跟 model 耦合太多,另一方面代码过于零散,到处都在操作数据,会带来后期维护的烦恼。
除了写法上订阅数据的区别以外,在设计思路上两个组件也有很大不同。 Presentational Component
是独立的纯粹的,可以参考 ant.design UI组件的React实现 ,每个组件跟业务数据并没有耦合关系,只是完成自己独立的任务,需要的数据通过 props
传递进来,需要操作的行为通过接口暴露出去。 而 Container Component
更像是状态管理器,它表现为一个容器,订阅子组件需要的数据,组织子组件的交互逻辑和展示。
3、其它
1)roadhog-》和 webpack 相似的库,起的是 webpack 自动打包和热更替的作用
roadhog 是一个 cli 工具,提供 dev、 build
和 test
三个命令,分别用于本地调试、构建和测试,并且提供了特别易用的 mock 功能。在体验上,保持了和 create-react-app一致(如 redbox 显示出错信息、HMR、ESLint 出错提示等等),并且提供了 JSON 格式的配置方式。如果 create-react-app 的默认配置不能满足需求,而他又不提供定制的功能,于是基于他实现了一个可配置版。所以如果既要 create-react-app 的优雅体验,又想定制配置,那么可以试试 roadhog
## Install globally or locally
$ npm i roadhog -g
## Local development
$ roadhog dev
## Build
$ roadhog build
## Test
$ roadhog test
roadhog dev支持mock, 在.roadhogrc.mock.js里配置
export default {
// Support type as Object and Array
'GET /api/users': { users: [1,2] },
// Method like GET or POST can be omitted(省略)
'/api/users/1': { id: 1 },
// Support for custom functions, the API is the same as express@4
'POST /api/users/create': (req, res) => { res.end('OK'); },
};
roadhog的webpack部分是基于af-webpack的实现。在项目根目录创建 .webpackrc进行配置,格式是
JSON。
2)react-router-redux和dva
redux 是状态管理的库,router 是(唯一)控制页面跳转的库。两者都很美好,但是不美好的是两者无法协同工作。换句话说,当路由变化以后,store 无法感知到。于是便有了 react-router-redux
。
react-router-redux
是 redux 的一个中间件,主要作用是:加强了React Router库中history这个实例,以允许将history中接受到的变化反应到state中去。
从代码上讲,主要是监听了 history 的变化。dva 在此基础上又进行了一层代理,把代理后的对象当作初始值传递给了 dva-core,方便其在 model 的 subscriptions 中监听 router 变化。
3)dva/fetch-》异步请求库,输出 isomorphic-fetch 的接口。
4)dva-loading
dva 有一个管理 effects 执行的 hook,并基于此封装了 dva-loading 插件。通过这个插件,我们可以不必一遍遍地写 showLoading 和 hideLoading,当发起请求时,插件会自动设置数据里的 loading 状态为 true 或 false 。然后我们在渲染 components 时绑定并根据这个数据进行渲染。
// 1、注册 dva-loading 插件
import dva from 'dva';
import createLoading from 'dva-loading';
const app = dva();
app.use(createLoading());
// 2、从store中获取loading状态
import React from 'react';
import { connect } from 'dva';
import { Table } from 'antd';
function Users({ dispatch, list: dataSource, loading }) {
const columns = [
{
title: 'Username',
dataIndex: 'username',
key: 'username',
render: text => <a href="">{text}</a>,
},
{
title: 'Street',
dataIndex: 'address.street',
key: 'street',
},
{
title: 'Website',
dataIndex: 'website',
key: 'website',
}
];
return (
<div className={styles.normal}>
<Table
columns={columns}
dataSource={dataSource}
loading={loading}
rowKey={record => record.id}
pagination={false}
/>
</div>
);
}
function mapStateToProps(state) {
const { list } = state.users;
return {
loading: state.loading.models.users,
list,
};
}
export default connect(mapStateToProps)(Users);
2、项目积累
1)React 中常见模式是为一个组件返回多个元素。为了包裹多个元素我们写过很多的 div 和 span,进行不必要的嵌套,无形中增加了浏览器的渲染压力。
react15版以前,render 函数的返回必须有一个根节点,否则报错,为满足这一原则我会使用一个没有任何样式的 div 包裹一下。
import React from 'react';
export default function () {
return (
<div>
<div>一步 01</div>
<div>一步 02</div>
<div>一步 03</div>
</div>
);
}
react 16版开始, render支持返回数组,这一特性已经可以减少不必要节点嵌套。
import React from 'react';
export default function () {
return [
<div>一步 01</div>,
<div>一步 02</div>,
<div>一步 03</div>
];
}
而且,React 16为我们提供了Fragment。Fragment与Vue.js的<template>
功能类似,可做不可见的包裹元素。
import React from 'react';
export default function () {
return (
<React.Fragment>
<div>一步 01</div>
<div>一步 02</div>
<div>一步 03</div>
</React.Fragment>
);
}
参考:
附录:es6
1)Generator 函数
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function
关键字与函数名之间有一个星号;二是,函数体内部使用yield
表达式,定义不同的内部状态。
Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
上面代码定义了一个 Generator 函数helloWorldGenerator
,它内部有两个yield
表达式(hello
和world
),即该函数有三个状态:hello,world 和 return 语句(结束执行)。
然后,Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象—遍历器对象。
下一步,必须调用遍历器对象的next
方法,使得指针移向下一个状态。也就是说,每次调用next
方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield
表达式(或return
语句)为止。换言之,Generator 函数是分段执行的,yield
表达式是暂停执行的标记,而next
方法可以恢复执行。
hw.next() // { value: 'hello', done: false }
hw.next() // { value: 'world', done: false }
hw.next() // { value: 'ending', done: true }
hw.next() // { value: undefined, done: true }
遍历器对象的next
方法的运行逻辑如下。
(1)遇到yield
表达式,就暂停执行后面的操作,并将紧跟在yield
后面的那个表达式的值,作为返回的对象的value
属性值。
(2)下一次调用next
方法时,再继续往下执行,直到遇到下一个yield
表达式。
(3)如果没有再遇到新的yield
表达式,就一直运行到函数结束,直到return
语句为止,并将return
语句后面的表达式的值,作为返回的对象的value
属性值。
(4)如果该函数没有return
语句,则返回的对象的value
属性值为undefined
。
总结一下,调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next
方法,就会返回一个有着value
和done
两个属性的对象。value
属性表示当前的内部状态的值,是yield
表达式后面那个表达式的值;done
属性是一个布尔值,表示是否遍历结束。另外需要注意,yield
表达式只能用在 Generator 函数里面,用在其他地方都会报错。
2)Generator 函数的异步应用
ES6 诞生以前,异步编程的方法,大概有四种:回调函数、事件监听、发布/订阅、Promise 对象。Generator 函数将 JavaScript 异步编程带入了一个全新的阶段。