dva 实践笔记一
作者: dkvirus 发表于: 2018-06-28 09:28:00 最近更新: 2018-07-05 17:55:41

一、dva 中 axios 使用说明

dva 官网使用的网络请求库是 dva/fetch,个人比较喜欢 axios,因为可以跨域,各种拦截使用起来也很舒服。项目中经常需要对错误的请求进行统一拦截,统一友好的输出错误提示。本例为实践摸索出来的一套方案,无论是在 react 还是 vue 中都可以进行参考。

1. 代码示例

先贴代码,下面有各个部分详细解释。

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// request.js
import axios from 'axios';
import NProgress from 'nprogress';
import { notification, message } from 'antd';
import { routerRedux } from 'dva/router';
import store from '../index';

/**
* 一、功能:
* 1. 统一拦截http错误请求码;
* 2. 统一拦截业务错误代码;
* 3. 统一设置请求前缀
* |-- 每个 http 加前缀 baseURL = /api/v1,从配置文件中获取 apiPrefix
* 4. 配置异步请求过渡状态:显示蓝色加载条表示正在请求中,避免给用户页面假死的不好体验。
* |-- 使用 NProgress 工具库。
*
* 二、引包:
* |-- axios:http 请求工具库
* |-- NProgress:异步请求过度条,在浏览器主体部分顶部显示蓝色小条
* |-- notification:Antd组件 > 处理错误响应码提示信息
* |-- routerRedux:dva/router对象,用于路由跳转,错误响应码跳转相应页面
* |-- store:dva中对象,使用里面的 dispatch 对象,用于触发路由跳转
*/

// 设置全局参数,如响应超市时间,请求前缀等。
axios.defaults.timeout = 5000
axios.defaults.baseURL = '/api/v1';
axios.defaults.withCredentials = true;

// 状态码错误信息
const codeMessage = {
200: '服务器成功返回请求的数据。',
201: '新建或修改数据成功。',
202: '一个请求已经进入后台排队(异步任务)。',
204: '删除数据成功。',
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
401: '用户没有权限(令牌、用户名、密码错误)。',
403: '用户得到授权,但是访问是被禁止的。',
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
406: '请求的格式不可得。',
410: '请求的资源被永久删除,且不会再得到的。',
422: '当创建一个对象时,发生一个验证错误。',
500: '服务器发生错误,请检查服务器。',
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。',
};

// 添加一个请求拦截器,用于设置请求过渡状态
axios.interceptors.request.use((config) => {
// 请求开始,蓝色过渡滚动条开始出现
NProgress.start();
return config;
}, (error) => {
return Promise.reject(error);
});

// 添加一个返回拦截器
axios.interceptors.response.use((response) => {
// 请求结束,蓝色过渡滚动条消失
NProgress.done();
return response;
}, (error) => {
// 请求结束,蓝色过渡滚动条消失
// 即使出现异常,也要调用关闭方法,否则一直处于加载状态很奇怪
NProgress.done();
return Promise.reject(error);
});

export default function request (opt) {
// 调用 axios api,统一拦截
return axios(opt)
.then((response) =>
// >>>>>>>>>>>>>> 请求成功 <<<<<<<<<<<<<<
console.log(`【${opt.method} ${opt.url}】请求成功,响应数据:%o`, response);

// 打印业务错误提示
if (response.data && response.data.code != '0000') {
message.error(response.data.message);
}

return { ...response.data };
})
.catch((error) => {
// >>>>>>>>>>>>>> 请求失败 <<<<<<<<<<<<<<
// 请求配置发生的错误
if (!error.response) {
return console.log('Error', error.message);
}

// 响应时状态码处理
const status = error.response.status;
const errortext = codeMessage[status] || error.response.statusText;

notification.error({
message: `请求错误 ${status}`,
description: errortext,
});

// 存在请求,但是服务器的返回一个状态码,它们都在2xx之外
const { dispatch } = store;

if (status === 401) {
dispatch(routerRedux.push('/user/login'));
} else if (status === 403) {
dispatch(routerRedux.push('/exception/403'));
} else if (status <= 504 && status >= 500) {
dispatch(routerRedux.push('/exception/500'));
} else if (status >= 404 && status < 422) {
dispatch(routerRedux.push('/exception/404'));
}

// 开发时使用,上线时删除
console.log(`【${opt.method} ${opt.url}】请求失败,响应数据:%o`, error.response);

return { code: status, message: errortext };
});
}

2. 明确响应体数据结构

以微信小程序为例,请求响应数据分为两部分:

  • 网络请求是否成功;
  • 业务场景值。即便网络请求成功了,业务处理上可能有时也会出错,比如校验不通过。

我们在拦截响应时要分别对这两部分进行处理。

1
2
3
4
5
6
7
8
9
response = {
status: 200, // 网络请求状态。
statusText: 'xxx',
data: {
code: '1001', // 业务请求状态。这里 '0000' 表示业务没问题,其它都有问题
message: 'yyy',
data: { },
}
}

3. 依赖包说明

1
2
3
4
5
import axios from 'axios';
import NProgress from 'nprogress';
import { notification, message } from 'antd';
import { routerRedux } from 'dva/router';
import store from '../index';

上面几个都是比较常见的包,最后一个 import store from '../index'; 可能有些同学会比较陌生,这是 dva 中导出的对象。即下面代码最终导出的 app._store,引入它是因为 dispatch 对象在里面,我们需要 dispatch 对象进行路由跳转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// index.js
import dva from 'dva';
import { message } from 'antd';
import { createBrowserHistory as createHistory } from 'history';

// 1. Initialize
const app = dva({
history: createHistory(),
});

// 2. Plugins
app.use(createLoading());

// 3. Model
app.model(require('./models/app/global').default);

// 4. Router
app.router(require('./router').default);

// 5. Start
app.start('#root');

export default app._store;

4. axios 全局配置

1
2
3
4
// 设置全局参数,如响应超时时间,请求前缀等。
axios.defaults.timeout = 5000
axios.defaults.baseURL = '/api/v1';
axios.defaults.withCredentials = true;

axios 可以设置很多全局配置,具体可参阅:更多

5. 加载 NProgress 过渡组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 添加一个请求拦截器,用于设置请求过渡状态
axios.interceptors.request.use((config) => {
// 请求开始,蓝色过渡滚动条开始出现
NProgress.start();
return config;
}, (error) => {
return Promise.reject(error);
});

// 添加一个返回拦截器
axios.interceptors.response.use((response) => {
// 请求结束,蓝色过渡滚动条消失
NProgress.done();
return response;
}, (error) => {
// 请求结束,蓝色过渡滚动条消失
// 即使出现异常,也要调用关闭方法,否则一直处于加载状态很奇怪
NProgress.done();
return Promise.reject(error);
});

NProgress 的使用主要有两个方法,当调用 NProgress.start(); 时在浏览器顶部就会出现蓝色小条,当调用 NProgress.done(); 蓝色小条就会消失。我们分别在请求开始和接收到响应调用这两个方法。

nprogress

6. 网络请求成功处理

1
2
3
4
5
6
7
8
9
10
11
.then((response) => 
// >>>>>>>>>>>>>> 请求成功 <<<<<<<<<<<<<<
console.log(`【${opt.method} ${opt.url}】请求成功,响应数据:%o`, response);

// 打印业务错误提示
if (response.data && response.data.code != '0000') {
message.error(response.data.message);
}

return { ...response.data };
})

网络请求状态码为 200-300 表示成功,此时还应该判断业务处理是否成功。这个根据具体项目具体规定,比如微信小程序有一套 场景值(见下图)。在实际项目中可以自行规定 code = ‘0000’ 业务处理完全没问题,code = ‘1111’ 校验不通过,code = ‘2222’ 数据库出错等等。

最后别忘了要返回具体对象 { ...response.data }

微信小程序场景值

7. 网络请求失败处理

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
// 状态码错误信息
const codeMessage = {
200: '服务器成功返回请求的数据。',
201: '新建或修改数据成功。',
202: '一个请求已经进入后台排队(异步任务)。',
204: '删除数据成功。',
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
401: '用户没有权限(令牌、用户名、密码错误)。',
403: '用户得到授权,但是访问是被禁止的。',
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
406: '请求的格式不可得。',
410: '请求的资源被永久删除,且不会再得到的。',
422: '当创建一个对象时,发生一个验证错误。',
500: '服务器发生错误,请检查服务器。',
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。',
};

// ...........

.catch((error) => {
// >>>>>>>>>>>>>> 请求失败 <<<<<<<<<<<<<<
// 请求配置发生的错误
if (!error.response) {
return console.log('Error', error.message);
}

// 响应时状态码处理
const status = error.response.status;
const errortext = codeMessage[status] || error.response.statusText;

notification.error({
message: `请求错误 ${status}`,
description: errortext,
});

// 存在请求,但是服务器的返回一个状态码,它们都在2xx之外
const { dispatch } = store;

if (status === 401) {
dispatch(routerRedux.push('/user/login'));
} else if (status === 403) {
dispatch(routerRedux.push('/exception/403'));
} else if (status <= 504 && status >= 500) {
dispatch(routerRedux.push('/exception/500'));
} else if (status >= 404 && status < 422) {
dispatch(routerRedux.push('/exception/404'));
}

// 开发时使用,上线时删除
console.log(`【${opt.method} ${opt.url}】请求失败,响应数据:%o`, error.response);

return { code: status, message: errortext };
});

网络请求失败,首先需要根据 status 打印提示消息,告诉用户为什么请求失败。如响应码为 401,那么提示用户的文字就会是 用户没有权限(令牌、用户名、密码错误)。

对于常见的几个 status,要进行相应路由操作,跳转到不同页面以友好的方式提示用户。这些错误页面参考 Ant Design Pro

如果是 401 错误,表示用户没有权限访问或者用户名密码输入错误,应该跳转到登录页面:dispatch(routerRedux.push('/user/login'));

403:无权访问

404:访问页面不存在

500:服务器错误

二、dva 中 browserHistory 使用说明

1. 什么是 History

1.1 概述

history 是 window 对象的一个属性,用于记录浏览器访问记录。history 有三种类型:

  • hashHistory 老版本浏览器的 history;
  • browserHistory h5 的 history。现在的 web 项目都使用这种;
  • memoryHistory node环境下的 history,存储在 memory中。

1.2 如何辨别 history 属于哪一种?

  • 浏览器地址栏看网址,如果有 #,那么就是 hashHistory了;
  • 在代码中 ‘pushState’ in window.history 如果为 true 说明是 browserHistory。

这是因为 browserHistory API 有 pushState, replaceState, popState 属性。

1.3 不同类型 history 之间差异

使用 hashHistory,连续点击一个链接会报错:Warning: Hash history cannot PUSH the same path; a new entry will not be added to the history stack,使用 browserHistory 就不会出现这种情况。连续点击一个链接,第二次依然会有反应,但不会报错。

产生这种差异的原因是机制问题,hashHistory 制作一个栈来存放历史记录,且不允许连续两个历史记录相同。browserHistory 就允许连续两次历史记录相同。

测试:连续点击一个链接四次,再点击浏览器的后退按钮,看 hashHistory 与 browserHistory 的差别。

1.4 如何查看 history 中历史记录。

直接查看具体的历史记录只能到浏览器-选项-历史记录中查看。

通过代码的形式查看 window.history 打印出对象有个 length 属性,记录历史记录的条数。点击一条链接之后再打印 window.history 可以查看变化。

补充:浏览器中一个选项卡会单独记录它的历史记录,关闭浏览器选项卡重新打开之后又会从 1 开始计数。

2. dva 中如何使用 History

dva 官方文档提供的 api 可自由选择使用哪种 history,但是测试时貌似不行,只能使用 hashHistory,这个 bug 截止 2.0.3 版本还未修复。

1
2
3
4
import { browserHistory } from 'dva/router';
const app = dva({
history: browserHistory,
});

【替代方法】使用 history 这个第三方库。

安装第三方依赖包

1
$ npm install history

修改 index.js 文件

1
2
3
4
5
import { createBrowserHistory as createHistory } from 'history';

const app = dva({
history: createHistory()
});

通过以上配置就已经使用了 browserHistory 了,如果这里没有感官认识,可以换成 hashHistory 刷新页面观察浏览器地址栏变化。

  • 创建 hashHistory:import { createHashHistory as createHistory } from 'history';
  • 创建 memoryHistory:import { createMemoryHistory as createHistory } from 'history';

三、dva 中 dva-loading 使用说明

之前对 dva-loading 理解存在误区,认为只要在 index.js 中配置一下就没事了,事实上 dva-loading 只是提供当前异步加载方法的状态(异步加载中状态为 true,异步加载完成状态为 false),对应加载样式由各自组件自己控制,如:Antd 中 Table 组件自身的 loading 属性。并添加完整流程示例代码。

1. 异步加载状态组件 dva-loading

1.1 传统做法

比如请求一个用户页面,刚进去的时候由于要去服务器请求数据需要花费时间,这段时间页面应该处于不能点击的状态。

如果不使用这个组件,传统做法是写个蒙版组件,提供两个方法 start() 和 end(),当进行 ajax 请求开始时调用 start() 方法给整个页面加上一层蒙版,此时不能进行操作,在请求结束也就是 ajax 的 success 回调函数中调用 end() 方法关闭蒙版,表明数据已经请求到了,可以操作页面。

1.2 作用

该组件仅仅监听异步加载状态,这从它的调用方式就可以看出来 const isLoading = loading.effects['user/query'],其中 user/query 是 model 中的异步请求方法。

loading 在异步请求发出那一刻会持续监听该异步请求方法的状态,在异步请求结束之前 isLoading 的值一直是 true,当此次异步请求结束时 isLoading 的值变成 false,同时 loading 对象停止监听。

1.3 配置

dva 项目的 index.js 文件:

1
2
3
4
5
6
import createLoading from 'dva-loading';

const app = dva();

// 调用插件
app.use(createLoading());

配置完成后,在任何一个 dva 的 routes 组件中都会有一个 loading 对象,如果你对 dva 稍有了解的话,应该不难知道它在哪。比如下面这行代码中的 loading 对象就是由于上面的配置。

1
export default connect(({ app, loading }) => ({ app, loading }))(App);

打印一下 loading 对象,可看到内容如下:

1
2
3
4
5
loading: {
global: false,
models: {app: false},
effects: {app: false}
}

loading 有三个方法,其中 loading.effects['user/query'] 为监听单一异步请求状态,当页面处于异步加载状态时该值为 true,当页面加载完成时,自动监听该值为 false。

如果同时发出多个异步请求,需求是当所有异步请求都响应才能做下一步操作,可以使用 loading.global() 方法,该方法监听所有异步请求的状态。

1.4 怎么用?

使用 Antd 的 Table 组件 时,查阅 API 可以看到有个 loading 的属性。如果该属性值为 true,Table 组件自身会显示加载效果,该值为 false,加载效果消失。可以通过 loading 对象判断当前是否有异步加载。具体示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src > models >user.js
export default {
namespace: 'user',
state: {
userList: [], // 存放用户列表
},
effects: {
* query ({ payload = {} }, { call, put }) {
// 获取用户列表,赋值给 userList
// 使用 axios 或者 ajax 请求后台返回数据
const result = axios.request('xxx/xxx');
// 调用 reducers 中的 updateState 方法改变 state 中 userList 的值
yield put({ type: 'updateState', payload: { userList: result.data });
}
},
reducers: {
updateState (state, { payload }) {
return { ...state, ...payload };
},
},
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src > routes > user.js
import React from 'react';
import { connect } from 'dva';
import { Table } from 'antd';

const User = ({ dispatch, user, loading }) => {
/**
根据 loading.effects 对象判断当前异步加载是否完成,并将该值传递给 Table 组件的 loading 属性,
Table 组件会自己控制加载样式。dva-loading 在这里的作用只是提供异步加载的状态,
具体加载样式由对应组件自己提供。
*/
const isLoading = loading.effects['user/query']
const { userList } = user

return (
<Table
dataSource={userList}
loading={isLoading}
rowKey={record => record.id}
/>
);
}

export default connect(({ user, loading }) => ({ user, loading }))(User);

2. 动画组件 nprogress

2.1 作用

制作页面加载时动态页面,在页面顶端提供动态进度条,表明当前页面正在加载状态。

nprogress

2.2 用法

安装依赖包

1
$ npm install nprogress

xx.js 中

1
import NProgress from 'nprogress';

提供了两个方法:NProgress.start() 和 NProgress.done()。

发出网路请求时(ajax 请求后端接口)调用 NProgress.start() 方法,此时页面顶端会有蓝色动态进度条;在页面请求数据完毕时(可以认为是 ajax 的 success 回调函数),调用 NProgress.done() 方法,此时蓝色进度条会瞬间加载到 100% 然后消失。

3. 配合使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// app.js 
import React from 'react';
import { connect } from 'dva';
import NProgress from 'nprogress';

const App= ({ app, loading }) => {

let currHref = '';
const href = window.location.href; // 浏览器地址栏中地址
if (currHref !== href) { // currHref 和 href 不一致时说明进行了页面跳转
NProgress.start(); // 页面开始加载时调用 start 方法
if (!loading.global) { // loading.global 为 false 时表示加载完毕
NProgress.done(); // 页面请求完毕时调用 done 方法
currHref = href; // 将新页面的 href 值赋值给 currHref
}
}

}

export default withRouter(connect(({ app, loading }) => ({ app, loading }))(App));

四、dva 中 Websocket 使用说明

1. 概述

Websocket 是 H5 自带的一个 API,随着越来越多的浏览器都自适应了 H5 的特性,许多浏览器也内置了 WebSocket API。也就是说 WebSocket 和 window、document 一样作为全局变量可以直接使用。

要在浏览器端使用 WebSocket,首先需要服务端支持 WebSocket,假设现在服务端已提供 WebSocket 服务,访问地址:ws://localhost:8080,简单介绍下浏览器使用方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 连接 Websocket 服务端
const ws = new WebSocket("ws://localhost:8080");

// 监听连接上 Websocket 服务端触发事件
ws.onopen = function (e) {
console.log('连接上 ws 服务端了');
// ws.send() 给服务端发送数据
ws.send('我是客户端,我接收到你的请求了');
}

// 监听 Websocket 服务端传来消息触发事件
ws.onmessage = function(msg) {
// msg.data 接收服务端传递过来的数据
console.log('接收服务端发过来的消息: %o', msg.data);
};

// 监听 Websocket 服务端连接断开触发事件
ws.onclose = function (e) {
console.log('ws 连接关闭了');
}

注意事项: WebSocket 通信传递的数据是字符串,即便浏览器端传给服务端的是个对象,在服务端接收时也会变成字符串,可以通过 JSON.parse(msg.data) 解析成对象。

2. dva 中使用 WebSocket

下面是我写小说爬虫用到的部分代码,点击爬取,在浏览器端打印服务端爬虫日志。

浏览器端实时打印服务端日志

dva 是基于 React 的状态管理器,不能直接对 Dom 进行操作。要持续不断的接收服务端响应数据,需要在构造器中定义一个 state 属性进行接收。直接在 routes 目录下的页面中连接服务端 WebSocket 并调用 API 接口。

(用箭头函数写组件的写法是没有 state 特性的,这一点在 React 官方文档中有详细说明,具体可参考《React 开发中不得不注意的两个大坑》

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
// src/routes/novel/index.js
class NovelComp {
constructor (props) {
super(props);
this.state = { result: '' }; // 接收 Websocket 响应数据
}

handleSteal (flag) {
const ws = new WebSocket('ws://localhost:8080');
let result = this.state.result;

ws.onopen = function (e) {
console.log('连接上 ws 服务端了');
ws.send(JSON.stringify({ flag: flag, data: currentItem }));
}
ws.onmessage = function(msg) {
console.log('接收服务端发过来的消息: %o', msg);
result += msg.data + '\n';
that.setState({ result: result });
};
ws.onclose = function (e) {
console.log('ws 连接关闭了');
}
}

render () {
return (
<Modal>
<Button></Button>
....
<Modal/>
);
}
}

3. 一直连接 WebSocket

上面的 WebSocket 应用场景是点击按钮才会连接 WebSocket,关闭模态框 WebSocket 连接即断开。

有些需求需要 WebSocket 一直连接,比如 待办事项主动提醒。如果其它操作新增了一条通知,当前用户的通知条目应当自动变成 6,而不是下一次刷新完再更新。

待办事项主动提醒

由于 dva 本身的特性,如果刷新页面,dva 所有状态容器中的值都会清空,与此同时 WebSocket 连接也会断开,也就没法监听服务端传过来的数据。

解决方案: 修改 src/models/novel.js 的 subscriptions 属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
subscriptions: {
setupHistory ({ dispatch, history }) {
history.listen((location) => {
if (location.pathname.indexOf('reptile/novel')) {
const ws = new WebSocket('ws://localhost:8080');
dispatch({
type: 'updateState',
payload: { ws: ws },
});
}
})
},
},

subscriptions 下面 setupHistory 的作用是监听浏览器地址栏地址,如果地址变成当前页面 reptile/novel,就会创建 WebSocket 连接,这样就能保证每次进入这个页面的时候都已连接 WebSocket。

对于 待办事项主动提醒 这个需求,只需在布局文件中的 subscriptions 中添加上述代码即可。

五、dva 中解决 Incofont 图标冲突

1. 背景

ant-design 官网提供了一部分图标,但种类并不能满足实际开发,需要去阿里图标库挑选些的图标来使用。

2. 问题

Antd 表单

使用过 Antd 的应该知道上面反馈成功的图标显示有误,应该是一个蓝色的

3. 思路

起初以为是 antd 的样式没有引进来,可是想想又不对,反馈失败图标 —— 红色 × 是可以显示正常的,后来点了下整个网站发现只有反馈成功图标显示有问题,F12 打开控制台查源码如下:

反馈成功图标

可以看到,这里用到一个伪类选择器。内容为:’\E630’。

在此之前,我刚好去阿里图标库 Iconfont 自己挑选了一批图标,下载到本地,也就是这几个文件:

iconfont 图标

打开 iconfont.css 文件,发现果然与其中一个图标的 unicode 冲突了。而冲突的那个图标的样子就是最上面那张图的图形。

1
2
// iconfont.css
.anticon-stop:before { content: "\E630"; }

4. 解决

直接删除本地 iconfont.css 文件中冲突的那个图标是没用的,需要重新去阿里图标库,删除冲突的那个图标,重新下载。

首页
友链
归档
dkvirus
动态
RSS