基于dva-cli&antd的react项目实战

背景

最近因为业务需要需要尽快做一个系统并部署上线。作为前端负责人虽然时间很赶,但是也只好硬着头皮上了。考虑到项目健壮性、紧急性以及后期维护,最后的选择是用dva-cli做手脚架,antd作为UI库来做这个系统。
并且,由于考虑到按照当前情况,前期后端接口不太可能跟得上前端进度,所以此时数据mock就显得非常重要了。并且作为一个前后端彻底分离的项目,rap在这里可以充分发挥其作用——数据mock、接口协定、文档生成。


————2018.11.5日修
这篇文章到今天实际上改改还是可以用。不过考虑到rap1已经事实上无人维护频繁抛错所以数据可能会出不来。
加之现在Typescript已经非常流行好用。所以又重新整理了一篇。typescript在redux-react项目中的应用
所谓上承下启,这篇依然保留,新的代码依然还是走的这篇一样的demo,但是建议采用的新的typescript方案,不过这之前 这篇文章建议还是参考看看。
并且新的项目react升级到16 react-router和wepack都升级到4。

目标概览

dashboard
本文不是讲如何做出一个一模一样的app,而是讲如何去实现基础的逻辑

代码和预览

相关代码已经托管到github,dva-demo。同时,存在一个在线预览地址,因为rap不支持https,所以只好为gp-pages绑定了自定义域名,router因为没有配置IndexRoute,所以菜单一1需要点击才会生效,点此前往

安装和初始化项目

安装:

1
npm install -g dva-cli

初始化项目:

  • 新目录中进行初始化: dva new myApp
  • 已有目录中初始化: mkdir myApp && cd myApp && dva init

手脚架代码自动生成

1
2
3
4
$ dva g route product-list #添加router
$ dva g model products #添加model
$ dva g component title #添加模块
$ dva g component title --no-css #添加模块但是不生成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');

// Support hmr
if (env === 'development') {
webpackConfig.devtool = '#eval';
webpackConfig.babel.plugins.push('dva-hmr');
} else {
webpackConfig.babel.plugins.push('dev-expression');
}

// Don't extract common.js and common.css
webpackConfig.plugins = webpackConfig.plugins.filter(function(plugin) {
return !(plugin instanceof webpack.optimize.CommonsChunkPlugin);
});

// Support CSS Modules
// Parse all less files as css module.
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>
// ...

现在项目是这样的:
initApp

路由配置

做完这些之后我们需要开始做些路由配置了。简单点说,点击每个菜单时候,刷新右侧内容区域内容。

首先理清一下逻辑:

  1. 默认的IndexPage内容区域需要默认内容
  2. 点击菜单后仅仅局部刷新右侧内容区域
  3. 点击刷新需要做一下相关配置

针对第一和第二,我们来对右侧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}>
{/* 添加一个路由,嵌套进我们想要嵌套的 UI 里 */}
<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);

现在它是这样的:
option

数据mock

到这里就要开始联调了。毕竟这个Table不能老是假数据,而且如果老用假数据也不太好测试翻页功能。所以这里上rap了。

首先,给出一下rap的数据详情看看
rap
简单说一下这个接口的主要逻辑设计逻辑:

  1. 请求参数包括页数页每页条数
  2. 返回的json中data.info包含必须的数据分页数据,包括第几页,每页条数,和总数据数
  3. 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;
}
/**
* Requests a URL, returning a promise.
*
* @param {string} url The URL we want to request
* @param {object} [options] The options we want to pass to "fetch"
* @param {boolean} rap 是否是rap请求 true是 false 否
* @return {object} An object containing either "data" or "err"
*/
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?{ //如果为rap请求 就去掉 credentials: 'include'来允许跨域
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}>
{/* 添加一个路由,嵌套进我们想要嵌套的 UI 里 */}
<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、按需加载、公共代码抽离、打包部署,都完毕了。本文页到此也就完整了。