背景 最近因为业务需要需要尽快做一个系统并部署上线。作为前端负责人虽然时间很赶,但是也只好硬着头皮上了。考虑到项目健壮性、紧急性以及后期维护,最后的选择是用dva-cli做手脚架,antd作为UI库来做这个系统。
------------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。
# 目标概览

本文不是讲如何做出一个一模一样的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 --savenpm install babel-plugin-import --save-dev
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就集成完毕了。
这里来验证一下是否安装成功:
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"  /> "Username"  /> 			  )} 			</FormItem> 			<FormItem> 			  {getFieldDecorator('password' , { 				rules: [{ required : true , message : 'Please input your Password!'  }], 			  })( 				<Input addonBefore={<Icon  type ="lock"  /> "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 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标签
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 key="2" ><Link  to ="12" > 菜单一2</Link >          </SubMenu>         <SubMenu key="sub2"  title={<span > <Icon  type ="appstore"  /> <span > 菜单二</span > </span >            <Menu.Item key="3" ><Link  to ="21" > 菜单二1</Link >            <Menu.Item key="4" ><Link  to ="22" > 菜单二2</Link >          </SubMenu>         <SubMenu key="sub4"  title={<span > <Icon  type ="setting"  /> <span > 菜单三</span > </span >            <Menu.Item key="5" ><Link  to ="31" > 菜单三1</Link >            <Menu.Item key="6" ><Link  to ="32" > 菜单三2</Link >            <Menu.Item key="7" ><Link  to ="33" > 菜单三3</Link >            <Menu.Item key="8" ><Link  to ="34" > 菜单三4</Link >          </SubMenu>       </Menu>     );   }    
至此,我们的APP基本有那么一个样子了。
加个面包屑 作为后台系统面包屑还是必不可少,而且面包屑作为全局共用组件,需要在不同路由下显示不同的路径,所以非常适合用来讲解如何进行组件之间进行通讯——每个内容区域和面包屑都是独立的,但是内容区域内部需要向面包屑通知新的路径数据。
最简单的面包屑是一个写死的面包屑,做如下修改:
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><a  href ="" > Application List</a >        <Breadcrumb.Item>An Application</Breadcrumb.Item>     </Breadcrumb>   </Col> 
不过显然没有数据驱动的面包屑没有任何意义,现在来改一下,实现数据驱动。
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 >            </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" )); 
现在我们来修改这个文件,将它和面包屑关联起来。
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 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 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包都不需要手动安装。
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 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配置为例:
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配置改一下,让它按需加载而不是将所有路由代码都打包到一个里面去。
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  =>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的代码切片,而
这里的r11定义得看起来有些愚蠢,然而require这个函数有些特殊,无法向它内部传递变量,到目前为止,也只好这样了。
抽出公共代码其实还是很容易。dva-cli内部实际上已经留下了这个接口。
1 2 3 4 5 6 "entry": {     "index": "./src/index.js",     "common": [       "react"     ]   }, 
到此,一个基于dva-cli的react项目,从基础骨架、组件通讯、ajax异步请求数据、数据mock、按需加载、公共代码抽离、打包部署,都完毕了。本文页到此也就完整了。