0%

Dva最不佳实践

前言

由于之前写了几个dva的项目,近期没怎么用有些遗忘了,写个小结记录一下。

dva是基于react、react-router、redux封装的一个轻框架。详细的介绍在 dva官网,这里仅仅摘录部分。项目托管在GitHub上,点击这里

特性

  • 易学易用:仅有 6 个 api,对 redux 用户尤其友好
  • elm 概念:通过 reducers, effectssubscriptions 组织 model
  • 支持 mobile 和 react-native:跨平台 (react-native 例子)
  • 支持 HMR:目前基于 babel-plugin-dva-hmr 支持 components、routes 和 models 的 HMR
  • 动态加载 Model 和路由:按需加载加快访问速度 (例子)
  • 插件机制:比如 dva-loading 可以自动处理 loading 状态,不用一遍遍地写 showLoading 和 hideLoading
  • 完善的语法分析库 dva-astdva-cli 基于此实现了智能创建 model, router 等
  • 支持 TypeScript:通过 d.ts (例子)

准备工作

  • 确保 node 版本是 6.5 +
  • cnpmyarn 能节约你安装依赖的时间

Step1. 安装 dva-cli 并创建应用

先安装 dva-cli,并确保版本是 0.7.x。

1
2
3
$ npm i dva-cli@0.7 -g
$ dva -v
0.7.0

然后创建应用:

1
$ dva new dva-learning

创建成功后进入该文件夹:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ cd dva-learning 
`项目的目录结构`
dva_learning
|-----mock # => 前端模拟数据
|-----node_modules # => 项目依赖采用npm管理,所有包均在此目录。
|-----public # => 存放index.html
|-----src
|------assets # => 项目静态资源文件夹(图片等)
|------components # => 无状态组件文件夹
|------models # => 状态model文件夹
|------routes # => 路由配置文件夹,页面存放在该文件夹下
|------services # => 服务层文件夹
|------utils # => 工具函数文件夹
|------index.css # => 全局通用样式
|------index.js # => 单页引用入口js,dva项目初始化
|------router.js # => 全局路由状态管理文件
|-----package.json # => npm包管理文件
|-----.eslintrc # => 代码规范配置文件
|-----.roadhogrc # => 打包配置文件
|-----.roadhogrc.mock.js # => 模拟数据配置文件
|-----README.md
`项目的目录结构`
`运行项目`
$ npm start

如果运行成功的话,浏览器会自动弹出并访问8000端口,看到如下画面:

Step2. 配置 antdbabel-plugin-import

antd是由蚂蚁金服开发的一套UI组件,具有学习成本低、上手速度快、实现效果好的特点。十分适合初学者并且与dva无缝接入。如需了解更多请查看 ANT DESIGN

babel-plugin-import 用于按需引入 antd 的 JavaScript 和 CSS,这样打包出来的文件不至于太大。

1
2
$ npm i antd --save
$ npm i babel-plugin-import --save-dev

修改 .roadhogrc,在 "extraBabelPlugins" 里加上:

1
["import", { "libraryName": "antd", "style": "css" }]

Step3. 添加新页面

我们的目标是写一个登录的界面,成功之后显示dva默认的首页。所以在src/routes文件夹下,新建Login.js和Login.css文件。js文件用来写组件布局,css文件用来写样式,默认为js文件和css文件一一对应。

在Login.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
`Login.js`
import React from 'react';
import { connect } from 'dva';
import { Input, Icon, Button } from 'antd';
import styles from './Login.css';

function Login() {
return (
<div className={styles.inputDiv}>
<div>
<Input
placeholder="用户名"
prefix={<Icon type="user" />}
size="large"
className={styles.inputUser}
/>
</div>
<div>
<Input
placeholder="密码"
prefix={<Icon type="lock" />}
size="large"
className={styles.inputPass}
/>
</div>
<Button type="primary" className={styles.button}>
登录
</Button>
</div>
);
}

Login.propTypes = {
};

export default connect()(Login);

为这个页面添加样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
`Login.css`
.inputDiv {
position: absolute;
top: 50%;
left: 50%;
width: 200px;
height: 220px;
margin: -110px 0 0 -100px;
}
.inputUser {
width: 200px;
}
.inputPass {
width: 200px;
margin-top: 20px;
}
.button {
width: 200px;
margin-top: 20px;
}

然后修改router.js页面,将新写的登录页面,放到默认显示页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
`router.js`
import React from 'react';
import { Router, Route } from 'dva/router';
import IndexPage from './routes/IndexPage';
import Login from './routes/Login';

function RouterConfig({ history }) {
return (
<Router history={history}>
<Route path="/" component={Login} />
<Route path="/home" component={IndexPage} />
</Router>
);
}

export default RouterConfig;

然后运行,会看到你完成的页面

Step4. 添加事件

之前完成的页面还没有添加点击事件,接下来添加几行代码,让它可以编辑,可以输入和点击

页面中使用的Input, Icon, Button均是Antd中基本的组件,在 ANT DESIGN有对它们详细的介绍。

为Button添加一个单击事件

1
2
3
4
5
6
7
8
`点击button时触发`
const submit = () => {
alert('here')
};
`在Button中添加这个方法`
<Button type="primary" className={styles.button} onClick={submit}>
登录
</Button>

再次运行项目,点击登录按钮会显示

Step.5 处理逻辑

如果需要这个登录界面更加真实的话,需要处理一些登录中的逻辑,比如点击登录按钮的时候,判断输入框中是否输入了数据等。

dva中有专门的文件夹存放这些处理页面内逻辑的代码。查看src/models/example.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
export default {

namespace: 'example', # => 唯一标识,应用中唯一

state: {}, # => 需要存储的值,每次修改会刷新界面

subscriptions: { # => 订阅
setup({ dispatch, history }) {
},
},

effects: { # => 副作用,一般用来发起请求
*fetch({ payload }, { call, put }) {
yield put({ type: 'save' });
},
},

reducers: { # => 只有在这里才能修改state的值
save(state, action) {
return { ...state, ...action.payload };
},
},

};

更多关于dva的api,请查看dva APIs

在src/models中新建login.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
`login.js`
export default {

namespace: 'login',

state: {
loading: false,
},

subscriptions: {
},

effects: {
},

reducers: {
save(state, action) {
return { ...state, ...action.payload };
}
},
};

然后在Login.js中调用save方法

1
2
3
4
5
6
7
8
9
`Login.js`
const userName = (e) => {
dispatch({
type: 'login/save',
payload: {
user: e.target.value,
},
});
};

这个方法的意思是:当输入用户名的时候,调用save方法,并将输入的值,保存在state.user中。

Step.6 发送请求

输入完用户名以及密码之后,单击登录按钮,将输入的值发送至后台校验,校验通过之后跳转到下一个页面。

现在已经有了输入的用户名和密码,分别是login.user和login.password,现在需要将这两个数据发送到后台。由于现在并没有后台服务支持,dva支持mock数据,所以先在前台模拟一个后台服务。

在项目根目录下新建.roadhogrc.mock.js并添加:

1
2
3
4
5
6
`.roadhogrc.mock.js`
const mock = {}
require('fs').readdirSync(require('path').join(__dirname + '/mock')).forEach(function(file) {
Object.assign(mock, require('./mock/' + file))
})
module.exports = mock

之后在mock文件夹中新建login.js并添加:

1
2
3
4
5
6
7
8
9
10
`login.js`
const qs = require('qs');

module.exports = {
'POST /login' (req, res) {
console.log(req.body);
console.log('接受到请求');
setTimeout(()=>res.json({code:'200',message:'从mock/example.js请求成功'}),2000)
},
};

这个的意思是说,监听本地的8000端口,当访问http://localhost:8000/login的时候,会延迟2秒并返回数据。

模拟的后台服务已经完成,现在要在button中添加点击事件,去请求这个接口。

首先改造一下fetch请求,在utils/request.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
export default function request(url, method, params) {
if (method === 'POST') {
const formData = new FormData();

for (const key in params) {
if (params[key] != null) {
const value = params[key];
formData.append(key, value);
}
}
return fetch(url, {
method: method,
body: formData,
}).then(checkStatus)
.then(parseJSON)
.then(data => ({ data }))
.catch(err => ({ err }));
}
if (method === 'GET') {
return fetch(url)
.then(checkStatus)
.then(parseJSON)
.then(data => ({ data }))
.catch(err => ({ err }));
}
}

然后在services中新建login.js,这里可以理解为转发,从点击事件中传递到request里请求:

1
2
3
4
5
6
import request from '../utils/request';

export function login(params) {
console.log('services处理');
return request('/login', 'POST', params);
}

之后在models/login.js,发起这个请求:

1
2
3
4
5
6
7
8
9
effects: {
*fetch({ payload }, { call, put }) { // eslint-disable-line
if(!payload.userName || !payload.passWord) {
message.error('请输入账号密码');
return;
}
const { data } = yield call(service.login, payload);
},
},

最后在界面的button的点击事件中,调用models里的事件:

1
2
3
4
5
6
7
8
9
const submit = () => {
dispatch({
type: 'login/fetch',
payload: {
userName: login.user,
passWord: login.password,
},
});
};

所有的都完成之后,重启项目,点击button之后可以在命令行中看到:

undefined的原因是,roadhog的版本问题,获取不到从前台传递过来的参数。

Step.7 完善细节

整体的流程已经完成,现在要为它添加一些细节,让它看起来更加的真实

添加一个加载等待的圈圈,在点击button的同时显示,后台反馈结果后消失并跳转到下一个界面。

1
2
3
<Spin spinning={login.loading}>
...
</Spin>

用Spin标签将其他的标签包起来,当它显示的时候,会出现在被包裹的标签之上。

通过控制login.loading来控制Spin的显示与消失。

1
2
3
4
5
6
7
8
9
 reducers: {
...
loadingShow(state) {
return { ...state, loading: true};
},
loadingHide(state) {
return { ...state, loading: false};
}
},

最后在请求的时候完成这个流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
effects: {
*fetch({ payload }, { call, put }) {
if(!payload.userName || !payload.passWord) {
message.error('请输入账号密码');
return;
}
yield put({ type: 'loadingShow' });
const { data } = yield call(service.login, payload);
if(data.code === '200') {
yield put({ type: 'loadingHide' });
browserHistory.push('/home');
}else {
yield put({ type: 'loadingHide' });
message.error('登录失败');
}
},
},

如果需要在显示登录页面之前执行某些操作,可以在subscriptions中订阅:

1
2
3
4
5
6
7
8
9
subscriptions: {
setup({ dispatch, history }) {
history.listen((location) => {
if (location.pathname === '/') {
message.info('进入了登录页');
}
});
},
},

总结

这个登录页面,展示了从页面到请求的整个过程,虽然看起来有点绕,涉及了很多的页面。但是当文件多了,就会体现出dva这样分层的好处:各个文件夹各司其职,功能单一。

最后实现的效果: