Ant Design Pro 是一个企业级中后台前端/设计解决方案。本地环境须要安装 node 和 git,技术栈基于 ES2015+、React、dva、g2 和 antd。javascript
参考:https://dvajs.com/css
https://github.com/ant-design/ant-design-pro/blob/master/README.zh-CN.md前端
https://pro.ant.design/docs/getting-started-cnjava
一、预备知识node
1)Redux 是 JavaScript 状态容器,提供可预测化的状态管理;Redux 除了和 React 一块儿用外,还支持其它界面库。react
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]):
链接 React 组件与 Redux store。webpack
[mapStateToProps(state, [ownProps]): stateProps
] (Function): 若是定义该参数,组件将会监听 Redux store 的变化。任什么时候候,只要 Redux store 发生改变,mapStateToProps
函数就会被调用。该回调函数必须返回一个纯对象,这个对象会与组件的 props 合并。git
函数将被调用两次。第一次是设置参数,第二次是组件与 Redux store 链接:connect(mapStateToProps, mapDispatchToProps, mergeProps)(MyComponent)
。es6
connect 函数不会修改传入的 React 组件,返回的是一个新的已与 Redux store 链接的组件,并且你应该使用这个新组件。github
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 上的一部分数据。
二、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 对象的属性
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
。
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 通常指的是具备监听数据行为
的组件,通常来讲它们的职责是绑定相关联的 model 数据
,以数据容器的角色包含其它子组件。
它不会关联订阅 model 上的数据,而所需数据的传递则是经过 props 传递到组件内部。
对组件分类,主要有两个好处:让项目的数据处理更加集中;让组件高内聚低耦合,更加聚焦;
试想若是每一个组件都去订阅数据 model,那么一方面组件自己跟 model 耦合太多,另外一方面代码过于零散,处处都在操做数据,会带来后期维护的烦恼。
除了写法上订阅数据的区别之外,在设计思路上两个组件也有很大不一样。 Presentational Component
是独立的纯粹的,能够参考 ant.design UI组件的React实现 ,每一个组件跟业务数据并无耦合关系,只是完成本身独立的任务,须要的数据经过 props
传递进来,须要操做的行为经过接口暴露出去。 而 Container Component
更像是状态管理器,它表现为一个容器,订阅子组件须要的数据,组织子组件的交互逻辑和展现。
三、其它
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 时绑定并根据这个数据进行渲染。
// 一、注册 dva-loading 插件 import dva from 'dva'; import createLoading from 'dva-loading'; const app = dva(); app.use(createLoading()); // 二、从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);
二、项目积累
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> ); }
参考:http://www.javashuo.com/article/p-tpcakqgb-dt.html
附录: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 异步编程带入了一个全新的阶段。