背景 最近因为业务需要需要尽快做一个系统并部署上线。作为前端负责人虽然时间很赶,但是也只好硬着头皮上了。考虑到项目健壮性、紧急性以及后期维护,最后的选择是用dva-cli做手脚架,antd作为UI库来做这个系统。 并且,由于考虑到按照当前情况,前期后端接口不太可能跟得上前端进度,所以此时数据mock就显得非常重要了。并且作为一个前后端彻底分离的项目,rap在这里可以充分发挥其作用——数据mock、接口协定、文档生成。
------------2018.11.5日修
这篇文章到今天实际上改改还是可以用。不过考虑到rap1已经事实上无人维护频繁抛错所以数据可能会出不来。
加之现在Typescript已经非常流行好用。所以又重新整理了一篇。[typescript在redux-react项目中的应用](/2018/11/05/2018-11-5-typescript-redux-starter/)。
所谓上承下启,这篇依然保留,新的代码依然还是走的这篇一样的demo,但是建议采用的新的typescript方案,不过这之前 这篇文章建议还是参考看看。
并且新的项目react升级到16 react-router和wepack都升级到4。
# 目标概览
![dashboard](/images/dashboard.png)
本文不是讲如何做出一个一模一样的app,而是讲如何去实现基础的逻辑
代码和预览 相关代码已经托管到github,dva-demo 。同时,存在一个在线预览地址,因为rap不支持https,所以只好为gp-pages绑定了自定义域名,router因为没有配置IndexRoute,所以菜单一1需要点击才会生效,点此前往
安装和初始化项目 安装:
初始化项目:
新目录中进行初始化: dva new myApp
已有目录中初始化: mkdir myApp && cd myApp && dva init
手脚架代码自动生成 1 2 3 4 $ dva g route product-list $ dva g model products $ dva g component title $ dva g component title --no-css
antd集成 dva-cli只是一个项目的宏观架构,内部默认并没有整合antd,所以需要手动整合进来。 首先: npm install antd --save
然后安装webpack插件对antd按需加载: npm install babel-plugin-import --save-dev
PS:如果使用了这个插件实际上并不需要手动安装antd,它会自动安装缺失的package 最后,修改 webpack.config.js文件(添加line40-43):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 const webpack = require ('atool-build/lib/webpack' );module .exports = function (webpackConfig, env ) { webpackConfig.babel.plugins.push('transform-runtime' ); if (env === 'development' ) { webpackConfig.devtool = '#eval' ; webpackConfig.babel.plugins.push('dva-hmr' ); } else { webpackConfig.babel.plugins.push('dev-expression' ); } webpackConfig.plugins = webpackConfig.plugins.filter(function (plugin ) { return !(plugin instanceof webpack.optimize.CommonsChunkPlugin); }); webpackConfig.module.loaders.forEach(function (loader, index ) { if (typeof loader.test === 'function' && loader.test.toString().indexOf('\\.less$' ) > -1 ) { loader.include = /node_modules/ ; loader.test = /\.less$/ ; } if (loader.test.toString() === '/\\.module\\.less$/' ) { loader.exclude = /node_modules/ ; loader.test = /\.less$/ ; } if (typeof loader.test === 'function' && loader.test.toString().indexOf('\\.css$' ) > -1 ) { loader.include = /node_modules/ ; loader.test = /\.css$/ ; } if (loader.test.toString() === '/\\.module\\.css$/' ) { loader.exclude = /node_modules/ ; loader.test = /\.css$/ ; } }); webpackConfig.babel.plugins.push(['import' , { libraryName: 'antd' , style: 'css' , }]); return webpackConfig; };
至此,antd就集成完毕了。
这里来验证一下是否安装成功: 这里看router.js也就是路由文件里面关键的几句:
1 2 3 4 5 6 7 8 import IndexPage from './routes/IndexPage' ;export default function ({ history } ) { return ( <Router history={history}> <Route path="/" component={IndexPage} /> </Router> ); };
显然,根目录指向了routes/IndexPage,我们去里面修改试试: 目标是下面组件可以显示出来:
修改IndexPage.js,主要将官网的示例代码复制替换原来的就好了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import React, { Component, PropTypes } from 'react' ;import { connect } from 'dva' ;import { Link } from 'dva/router' ;import styles from './IndexPage.css' ;import { Form, Icon, Input, Button, Checkbox } from 'antd' ;const FormItem = Form.Item;const NormalLoginForm = Form.create()(React.createClass({ handleSubmit (e ) { e.preventDefault(); this .props.form.validateFields((err, values ) => { if (!err) { console .log('Received values of form: ' , values); } }); }, render ( ) { const { getFieldDecorator } = this .props.form; return ( <div style={{width :'400px' ,margin :"0 auto" }}> <Form onSubmit={this .handleSubmit} className="login-form" > <FormItem> {getFieldDecorator('userName' , { rules: [{ required : true , message : 'Please input your username!' }], })( <Input addonBefore={<Icon type ="user" /> } placeholder="Username" /> )} </FormItem> <FormItem> {getFieldDecorator('password' , { rules: [{ required : true , message : 'Please input your Password!' }], })( <Input addonBefore={<Icon type ="lock" /> } type="password" placeholder="Password" /> )} </FormItem> <FormItem> {getFieldDecorator('remember' , { valuePropName: 'checked' , initialValue: true , })( <Checkbox>Remember me</Checkbox> )} <a className="login-form-forgot" >Forgot password</a> <Button type="primary" htmlType="submit" className="login-form-button" > Log in </Button> Or <a>register now!</a> </FormItem> </Form> </div> ); }, })); export default connect()(NormalLoginForm) ;
最后运行结果成功,图基本如上就不再发。
项目构建 接下来我们简单的按照项目图做个原型来一点点实现,本文项目中Antd组件相关代码,为了方便学习和讲解,全部直接或者轻微修改自官方文档。
栅格 删除IndexPage内部所有的测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import React, { Component, PropTypes } from 'react' ;import { connect } from 'dva' ;import { Link } from 'dva/router' ;import styles from './IndexPage.css' ;import { Row, Col } from 'antd' ;class App extends Component { constructor (props ) { super (props) } render ( ) { return ( <Row style={{width :'1000px' ,margin :'0 auto' }}> <Col span={6 }>col-12 </Col> <Col span={18 }>内容区域</Col> </Row> ) } } export default connect()(App) ;
菜单 新建components/Silder.js文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import React from 'react' ;import { Menu, Icon } from 'antd' ;const SubMenu = Menu.SubMenu;const MenuItemGroup = Menu.ItemGroup;const Sider = React.createClass({ getInitialState ( ) { return { current: '1' , }; }, handleClick (e ) { console .log('click ' , e); this .setState({ current: e.key, }); }, render ( ) { return ( <Menu onClick={this .handleClick} style={{ width : 240 }} defaultOpenKeys={['sub1' ]} selectedKeys={[this .state.current]} mode="inline" > <SubMenu key="sub1" title={<span > <Icon type ="mail" /> <span > 菜单一</span > </span > }> <Menu.Item key="3" >菜单一1 </Menu.Item> <Menu.Item key="4" >菜单一2 </Menu.Item> </SubMenu> <SubMenu key="sub2" title={<span > <Icon type ="appstore" /> <span > 菜单二</span > </span > }> <Menu.Item key="5" >菜单二1 </Menu.Item> <Menu.Item key="6" >菜单二2 </Menu.Item> </SubMenu> <SubMenu key="sub4" title={<span > <Icon type ="setting" /> <span > 菜单三</span > </span > }> <Menu.Item key="9" >菜单三1 </Menu.Item> <Menu.Item key="10" >菜单三2 </Menu.Item> <Menu.Item key="11" >菜单三3 </Menu.Item> <Menu.Item key="12" >菜单三4 </Menu.Item> </SubMenu> </Menu> ); }, }); export default Sider
修改IndexPage,import之,并在左侧的Col内部插入这个Silder
1 2 3 4 5 6 import Silder from '../components/Silder' <Col span={6 }><Silder /> </Col> <Col span={18 }>内容区域</Col>
现在项目是这样的:
路由配置 做完这些之后我们需要开始做些路由配置了。简单点说,点击每个菜单时候,刷新右侧内容区域内容。
首先理清一下逻辑:
默认的IndexPage内容区域需要默认内容
点击菜单后仅仅局部刷新右侧内容区域
点击刷新需要做一下相关配置
针对第一和第二,我们来对右侧Col做一下配置
1 2 3 <Col span={18 }> {this .props.children||'内容区域' } </Col>
然后我们新建以下文件:1-1.js、1-2.js、2-1.js、2-2.js、3-1.js、3-2.js、3-3.js、3-4.js,文件放到routes目录下,内容基本如下:
1 2 3 4 5 import React from 'react' ;const Option = (props )=> ( <div>菜单一1 </div> ) export default Option
然后修改router.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 export default function ({ history } ) { return ( <Router hjsistory={history}> <Route path="/" component={IndexPage}> {} <Route path="11" component={require ('./routes/1-1.js' )} /> <Route path="12" component={require ('./routes/1-2.js' )} /> <Route path="21" component={require ('./routes/2-1.js' )} /> <Route path="22" component={require ('./routes/2-2.js' )} /> <Route path="31" component={require ('./routes/3-1.js' )} /> <Route path="32" component={require ('./routes/3-2.js' )} /> <Route path="33" component={require ('./routes/3-3.js' )} /> <Route path="34" component={require ('./routes/3-4.js' )} /> </Route> </Router> ); };
最后给左侧菜单加点击切换路由效果,也就是Link标签 修改Silder.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import { Link } from 'dva/router' render ( ) { return ( <Menu onClick={this .handleClick} style={{ width : 240 }} defaultOpenKeys={['sub1' ]} selectedKeys={[this .state.current]} mode="inline" > <SubMenu key="sub1" title={<span > <Icon type ="mail" /> <span > 菜单一</span > </span > }> <Menu.Item key="1" ><Link to ="11" > 菜单一1</Link > </Menu.Item> <Menu.Item key="2" ><Link to ="12" > 菜单一2</Link > </Menu.Item> </SubMenu> <SubMenu key="sub2" title={<span > <Icon type ="appstore" /> <span > 菜单二</span > </span > }> <Menu.Item key="3" ><Link to ="21" > 菜单二1</Link > </Menu.Item> <Menu.Item key="4" ><Link to ="22" > 菜单二2</Link > </Menu.Item> </SubMenu> <SubMenu key="sub4" title={<span > <Icon type ="setting" /> <span > 菜单三</span > </span > }> <Menu.Item key="5" ><Link to ="31" > 菜单三1</Link > </Menu.Item> <Menu.Item key="6" ><Link to ="32" > 菜单三2</Link > </Menu.Item> <Menu.Item key="7" ><Link to ="33" > 菜单三3</Link > </Menu.Item> <Menu.Item key="8" ><Link to ="34" > 菜单三4</Link > </Menu.Item> </SubMenu> </Menu> ); }
至此,我们的APP基本有那么一个样子了。
加个面包屑 作为后台系统面包屑还是必不可少,而且面包屑作为全局共用组件,需要在不同路由下显示不同的路径,所以非常适合用来讲解如何进行组件之间进行通讯——每个内容区域和面包屑都是独立的,但是内容区域内部需要向面包屑通知新的路径数据。
最简单的面包屑是一个写死的面包屑,做如下修改: IndexPage.js
1 2 3 4 5 6 7 8 9 10 11 12 13 import { Row, Col, Breadcrumb } from 'antd' ;<Row style={{width :'1000px' ,margin :'0 auto' }}> <Col span={24 }> <Breadcrumb> <Breadcrumb.Item>Home</Breadcrumb.Item> <Breadcrumb.Item><a href ="" > Application Center</a > </Breadcrumb.Item> <Breadcrumb.Item><a href ="" > Application List</a > </Breadcrumb.Item> <Breadcrumb.Item>An Application</Breadcrumb.Item> </Breadcrumb> </Col>
不过显然没有数据驱动的面包屑没有任何意义,现在来改一下,实现数据驱动。 新建components/breadcrumb.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import React from 'react' ;import { Breadcrumb } from 'antd' ;import { Link } from 'dva/router' const breadcrumb = (props )=> { return ( <Breadcrumb> { props.data.map((v,i )=> ( <Breadcrumb.Item key={i}> {v.path?(<Link to ={v.path} > {v.name}</Link > ):v.name} </Breadcrumb.Item> )) } </Breadcrumb> ) }; export default breadcrumb;
然后修改IndexPage.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import CustomBreadcrumb from '../components/breadcrumb' const breadcrumbData = [ { name:'首页' , path:'/' },{ name:'菜单21' , path:'/21' } ]; <Col span={24 }> <CustomBreadcrumb data={breadcrumbData} /> </Col>
至此,数据驱动面包屑完工。下一步要处理组件通讯了。也就是在1-1.js这些文件中驱动面包屑动态改变,而不是写死在IndexPage.js
首先我们先建立一个model文件,执行 dva g model common
,它会在models下建立一个common.js,并注入到index.js。
1 app.model(require ("./models/common" ));
现在我们来修改这个文件,将它和面包屑关联起来。 修改models/common.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 export default { namespace: 'common' , state: { breadcrumb:[ { name:'首页' , path:'/' } ] }, reducers: { changeBreadcrumb (state,{ payload: breadcrumb } ) { return {...state, ...breadcrumb} } }, effects: {}, subscriptions: {}, }
这就是面包屑的主要数据逻辑。然后将它和面包屑关联起来:修改IndexPage.js
1 2 3 4 5 6 7 8 9 <Col span={24 }> <CustomBreadcrumb data={this .props.common.breadcrumb} /> </Col> function mapStateToProps ({ common } ) { return {common}; } export default connect(mapStateToProps)(App);
至此,我们的的关联就做好了。现在去往1-1.js这些文件(其他文件同理不在赘述),进行通讯,使其动态改变。因为需要使用到生命周期,所以之前用到stateless写法要改动一下。并且因为需要通讯,所以需要使用redux连接一下。 1-1.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import React from 'react'; import { connect } from 'dva'; class Option extends React.Component{ constructor(props){ super(props) } render(){ return (<div>菜单一1</div>) } componentDidMount(){ const breadcrumbData = { breadcrumb:[ { name:'首页', path:'/' },{ name:'菜单一1' } ] }; this.props.dispatch({ type:'common/changeBreadcrumb', payload:breadcrumbData }) } } function mapStateToProps({ common }) { return {common}; } export default connect(mapStateToProps)(Option);
到此,我们的app目前是这样子的: 虽然它仍旧非常简陋,但是如你所见,虽然他不太好看,但是一个可以作画的画板已经准备好了。它已经可以在点击菜单的时候局部更新内容区域并更新面包屑了。
做个列表页 画布既然已经铺好,接下来我们往上面做点画。这里做个最最常见的的列表。 修改1-1.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 import React from 'react'; import { connect } from 'dva'; import {Table,Icon} from 'antd'; const columns = [ { title: 'Name', dataIndex: 'name', key: 'name', render: text => <a href="#">{text}</a>, }, { title: 'Age', dataIndex: 'age', key: 'age', }, { title: 'Address', dataIndex: 'address', key: 'address', }, { title: 'Action', key: 'action', render: (text, record) => ( <span> <a href="#">Action 一 {record.name}</a> <span className="ant-divider" /> <a href="#">Delete</a> <span className="ant-divider" /> <a href="#" className="ant-dropdown-link"> More actions<Icon type="down" /> </a> </span> ), } ]; const data = [ { key: '1', name: 'John Brown', age: 32, address: 'New York No. 1 Lake Park', }, { key: '2', name: 'Jim Green', age: 42, address: 'London No. 1 Lake Park', }, { key: '3', name: 'Joe Black', age: 32, address: 'Sidney No. 1 Lake Park', } ]; class Option extends React.Component{ constructor(props){ super(props) } render(){ return (<Table columns={columns} dataSource={data} />) } componentDidMount(){ const breadcrumbData = { breadcrumb:[ { name:'首页', path:'/' },{ name:'菜单一1' } ] }; this.props.dispatch({ type:'common/changeBreadcrumb', payload:breadcrumbData }) } } function mapStateToProps({ common }) { return {common}; } export default connect(mapStateToProps)(Option);
现在它是这样的:
数据mock 到这里就要开始联调了。毕竟这个Table不能老是假数据,而且如果老用假数据也不太好测试翻页功能。所以这里上rap了。
首先,给出一下rap的数据详情看看 简单说一下这个接口的主要逻辑设计逻辑:
请求参数包括页数页每页条数
返回的json中data.info包含必须的数据分页数据,包括第几页,每页条数,和总数据数
data.results则是一个对象数组,包含需要展示的数据。
拦截请求 接下来来我们做一些配置,用来将rap相关的数据配置化,不要写死。在项目根目录建立配置文件 cd src && mkdir config && touch config/config.js
1 2 3 4 5 6 const config = { rapHost:'http://rap.taobao.org/mockjs/5889/' , rapFlag:false , onlinePath:'/api/' } export default config
其中rapHost是rap的地址,这个可以参考rap文档找出。rapFlag则用来标志是否rap的mock请求,如果不是则请求真实的地址,至于onlinePath则是加载真实地址前的前缀,例如要请求真实接口/aboutus,实际会请求/api/aboutUs——之所以这样,是为了方便配置nginx反向代理时候进行路由匹配。
dva-cli的utils目录内有个封装好的request.js用来做请求类,不过它并不支持rap,所以需要改造一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import fetch from 'dva/fetch' ;import safeeval from 'safe-eval' import Mock from 'mockjs' ;function parseText (response ) { return response.text(); } function checkStatus (response ) { if (response.status >= 200 && response.status < 300 ) { return response; } const error = new Error (response.statusText); error.response = response; throw error; } export default function request (url, options,rap ) { if (!rap){rap = false ;} return fetch(url, options) .then(checkStatus) .then(parseText) .then((data ) => { if (rap){ return Mock.mock(safeeval(data)) }else { return safeeval(data) } }) .catch((err ) => ({ err })); }
主要修改的是parseJSON函数,将它从response.json()变成了response.text().并添加了mockjs对rap返回的mock模板进行解析(虽然rap可以直接返回数据而不是模板,不过据官方旺旺群里的说法,这个后期会被废弃)。
这样request.js已经可以用了,不过鉴于fetch参数比较多,默认还不带cookie没法使用session了,这样基于session的后端权限体系基本就废了,作为管理平台这显然是无法接受的。所以在它基础上再加一层。新建utils/query.js文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import request from '../utils/request' ;import qs ,{ parse } from 'qs' ;import FormdataWrapper from 'object-to-formdata' ;import merge from 'merge-object' ;import {rapHost, onlinePath} from '../config/config' const cookieTrue = { credentials: 'include' }; const jsonConf = { headers: { 'Content-Type' : 'application/json' } } function getUrl (smarturl,flag ) { if (flag){ return rapHost + '/' + smarturl; }else { return onlinePath + smarturl; } } async function POST (url,params,rapFlag,isJson ) { if (isJson == undefined ){isJson = false }; return request( getUrl(url,rapFlag),rapFlag?{ method: 'POST' , body:isJson?JSON .stringify(params):FormdataWrapper(params), }:merge({ method: 'POST' , body:isJson?JSON .stringify(params):FormdataWrapper(params), },isJson?merge(jsonConf,cookieTrue):cookieTrue),rapFlag); } async function GET (url,params,rapFlag ) { return request( getUrl(url,rapFlag) + `?${qs.stringify(params)} ` ,rapFlag?{ method: 'GET' , }:merge({ method: 'GET' , },cookieTrue),rapFlag); } export { POST,GET }
这个封装在post请求时候可以发送formdata和json,并且发送真实请求时候(rapFlag在部署打包时候需要改成false)会带上cookie以方便后端实现基于session的权限校验,并且由于此处的async,所以可以做一些处理后,可以同步代码一样使用。另外由于用了webpack插件,这里用到的npm包都不需要手动安装。 现在,可以这样使用fetch做请求了:
1 let { code,data } = yield POST('user/1',{disable:false},false)
请求类封装完毕,现在可以使用model将模块和rap对接起来了。运行dva g model 11
注:当我写完本文再去看dva文档时候我最后发现dva-cli实际上已经提供了更好的方案选择,那就是dora-plugin-proxy,使用dora-plugin-proxy确实会让代码更加干净。但是限于当时没有时间折腾,所以当时也没有能使用这个方案。但是写这篇文章时候,我已经开始在项目中使用它来替代上面的拦截方案。下面是记录。
首先需要说明的这里撤回了上文说到的request.js文件的改动,因为这里不在需要了。
而query.js文件,现在是这样的。只是简单的去除了rapFlag。
1 2 3 4 5 6 7 8 9 10 11 12 13 async function POST (url,params,isJson ) { if (isJson == undefined ){isJson = false }; return request( url,merge({ method: 'POST' , body:isJson?JSON .stringify(params):FormdataWrapper(params), },isJson?merge(jsonConf,cookieTrue):cookieTrue),rapFlag); } async function GET (url,params ) { return request( url + `?${qs.stringify(params)} ` ,merge({ method: 'GET' , },cookieTrue)); }
接下来我们修改mock/example.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var co = require ('co' );var fetch = require ('node-fetch' );var safeeval = require ('safe-eval' );var mockjs = require ('mockjs' );var Host = 'http://rap.taobao.org/mockjs/5889' function mockMapFun (req,res ) { co(function *( ) { var response = yield fetch(Host + req.url); var mockTpl = yield response.text(); res.json( mockjs.mock(safeeval(mockTpl))['data' ] ); }); } module .exports = { 'GET /member/list' : mockMapFun };
co和fetch配合使用让它更加接近于同步的写法。然后使用res.json返回了mockjs解析模板后的数据。
当然,写了两个方案,当然是自己的更挫一些,不过因为还是有参考价值就不再删除了。如果对这里有混乱,请直接爬代码。毕竟就不到50行代码量。
连接组件 编辑这个models/11.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import { GET } from '../utils/query' import { rapFlag, onlinePath } from '../config/config'; const API = 'member/list' export default { namespace: 'test', state: { list:{ data:[], loading:true, }, pagination:{ current:1, pageSize:10, total:null } }, reducers: { fetchList(state, action) { return { ...state, ...action.payload }; }, }, effects: { *fetchRemote({ payload }, { call, put }) { let {current,pageSize} = payload; let { data } = yield call(GET,API,{ pageNum:current, pageSize:pageSize, },rapFlag); if (data) { yield put({ type: 'fetchList', payload: { list: { data:data.results, loading:false }, pagination: data.info } }); } }, }, subscriptions: {}, }
关于Effects,可以参考此处官方文档 然后将它和1-1.js对应模块对接起来,进行部分修改: 1-1.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 import React from 'react'; import { connect } from 'dva'; import {Table,Icon} from 'antd'; const columns = [ { title: 'Name', dataIndex: 'name', key: 'name', render: text => <a href="#">{text}</a>, }, { title: 'Age', dataIndex: 'age', key: 'age', }, { title: 'Address', dataIndex: 'address', key: 'address', }, { title: 'LastLogin', dataIndex: 'lastLogin', key: 'lastLogin', } ]; class Option extends React.Component{ constructor(props){ super(props) } render(){ let {data,loading} = this.props.test.list; let pagination = this.props.test.pagination; return ( <Table columns={columns} dataSource={data} pagination={pagination} onChange={this.handleTableChange.bind(this)} loading={loading} /> ) } handleTableChange(pagination, filters, sorter){ this.props.dispatch({ type:'test/changePage', payload:{ pagination:{ current:pagination.current, pageSize:pagination.pageSize, showQuickJumper: true, loading:true } } }); this.fetch(pagination.current) } fetch(current){ // 更新列表 this.props.dispatch({ type:'test/fetchRemote', payload:{ current:current, pageSize:10, loading:false, } }); } componentDidMount(){ const breadcrumbData = { breadcrumb:[ { name:'首页', path:'/' },{ name:'菜单一1' } ] }; this.props.dispatch({ type:'common/changeBreadcrumb', payload:breadcrumbData }); this.fetch(1); } } function mapStateToProps({ common,test }) { return {common,test}; } export default connect(mapStateToProps)(Option);
代码打包部署 部署 经过上文一些讲解,基本上构建简单的react项目已经没有问题了。所以接下来讲一下代码部署。
react-router本身支持两中路由,一种基于hashchange的路由,类似www.que01.top/#/11
,但是也支持传统的路由类似www.que01.top/11
虽然hashchange部署更简单,但是作为一个「有追求」的开发者,我们应该在条件允许的情况下,毫不犹豫的使用后者。
但是这需要服务器端配置。以nginx配置为例: nginx.conf,关键部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 server { listen 8888; server_name ""; root /usr/share/nginx/html/dist; gzip_static on; location / { try_files $uri $uri/ /index.html; } location /api { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://backend:800/; } } }
其中,root是项目部署的目录,try_files $uri $uri/ /index.html;
这段的意思是,如果请求资源/about,首先在服务器上查找/about资源,如果没有,继续查找/about/index.html资源,如果还是没有,最后返回index.html资源。
至于location /api
这段,是一个反向代理。将它映射到真实的后端路径即可。这里就是之前为什么要在config.js内部配置一个onlinePath的原因。她需要同这里的反向代理的路由一致。
打包 关于打包,直接运行npm run build
即可,不过默认的打包是不带文件指纹的,为了实现强缓存,我们修改package.json,如下代码,修改build,为build命令添加–hash选项。
1 2 3 4 5 "scripts": { "start": "dora --plugins \"proxy,webpack,webpack-hmr\"", "build": "atool-build --hash", "test": "atool-test-mocha ./src/**/*-test.js" }
不过这个选项仅仅是用来生成文件指纹,index.html内部的html依然引入的是没有指纹的文件,直接这样打包会导致内部引入的资源404,所以要对webpack配置文件改动一番。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const HtmlWebpackPlugin = require ('html-webpack-plugin' );if (env === 'development' ) { webpackConfig.devtool = '#eval' ; webpackConfig.babel.plugins.push(['dva-hmr' , { entries: [ './src/index.js' , ], }]); } else { webpackConfig.babel.plugins.push('dev-expression' ); webpackConfig.plugins.push( new HtmlWebpackPlugin({ inject: false , template: require ('html-webpack-template' ), title: 'Dva Demo' , appMountId: 'root' , minify: { removeComments: true , collapseWhitespace: true }, links:['//at.alicdn.com/t/font_xxxxxxxx.css' ] }) ); }
主要的改动就是html-webpack-template这个插件的引入,改动后,手动安装一下插件 npm i html-webpack-template html-webpack-plugin --save-dev
这个配置用途就是动态注入css和js,这个只依靠html-webpack-plugin就可以实现,不过html-webpack-template基于html-webpack-plugin,并且让可配置性更好了。例如可以配置links,全局引入字体图标(这个项目里面用到很多),当然,频繁修改这个修改任务文件里面的links可不是什么好主意,所以你完全可以将这个抽出来放到config/config.js里面去——随你喜欢。
按需加载 按上面的做完配置之后基本就没什么需要弄的了。但是还有个对中型和大型项目非常重要的一点,那就是按需加载——大抵你不会希望访问一下/aboutUs这个路由,就需要下载整个/allInOne的js代码;还有就是commonChunk代码抽离——你也不会希望明明可以公用的代码,在每一个路由里面都重复打包一次,这会导致项目体积虚胖和流量浪费,同时这也意味这更慢的打开速度。
首先我们将react-router配置改一下,让它按需加载而不是将所有路由代码都打包到一个里面去。 src/router.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const r11 = (location, callback ) => { require .ensure(['./routes/1-1' ], require => {callback(null , )}, '11' ) }; <Router hjsistory={history}> <Route path="/" component={IndexPage}> {} <Route path="11" getComponent={r11} /> <Route path="12" getComponent={r12} /> <Route path="21" getComponent={r21} /> <Route path="22" getComponent={r22} /> <Route path="31" getComponent={r31} /> <Route path="32" getComponent={r32} /> <Route path="33" getComponent={r33} /> <Route path="34" getComponent={r34} /> </Route> </Router>
require.ensure
这个是依赖webpack的代码切片,而
这个里面的getComponent,则是react-router内部的异步接口。结合这两个,至此,代码就可以按照路由进行按需加载了。接下来我们把react作为commonChunk抽出来,这样会显著缩小部署体积——项目越大就会越明显。
这里的r11定义得看起来有些愚蠢,然而require这个函数有些特殊,无法向它内部传递变量,到目前为止,也只好这样了。
抽出公共代码其实还是很容易。dva-cli内部实际上已经留下了这个接口。 修改package.json,添加common对象,如下:
1 2 3 4 5 6 "entry": { "index": "./src/index.js", "common": [ "react" ] },
到此,一个基于dva-cli的react项目,从基础骨架、组件通讯、ajax异步请求数据、数据mock、按需加载、公共代码抽离、打包部署,都完毕了。本文页到此也就完整了。