数据请求是咱们开发中很是重要的一环,如何优雅地进行抽象处理,不是一件很容易的事情,也是常常被忽略的事情,处理很差的话,重复的代码散落在各处,维护成本极高。webpack
因此咱们须要好好梳理下数据请求涉及到哪些方面,对它有总体的管控,从而设计出扩展性高的方案。ios
下面咱们以 axios
这个请求库进行讲解。git
假如咱们在页面中发出一个 POST 请求,相似这样:github
axios.post('/user/create', { name: 'beyondxgb' }).then((result) => {
// do something
});
复制代码
后来发现须要防止 CSRF
,那咱们须要在请求中的 headers
加上 X-XSRF-TOKEN
,因此变成这样:web
axios.post('/user/create', { name: 'beyondxgb' }, {
headers: {
'X-XSRF-TOKEN': 'xxxxxxxx',
},
}).then((result) => {
// do something
});
复制代码
这时能够发现,难道每次发起 post
请求都须要这样配置吗?因此会想到把这部分配置抽离出来,抽象出相似这样一个方法:json
function post(url, data, config) {
return axios.post(url, data, {
headers: {
'X-XSRF-TOKEN': 'xxxxxxxx',
},
...config,
});
}
复制代码
因此咱们须要对参数配置进行抽象。axios
到了测试流程的时候,发现服务端的请求不是总返回成功的,那怎么办?那就 catch
处理一下:api
post('/user/create', { name: 'beyondxgb' }).then((result) => {
// do something
}).catch((error) => {
// deal with error
// 200
// 503
// SESSION EXPIRED
// ...
});
复制代码
写下来总感受哪里不对啊,原来请求错误有这么多状况,我整个项目有不少请求数据的地方呢,这部分代码确定是通用的,抽象出来!promise
function dealWithRequestError(error) {
// deal with error
// 200
// 503
// SESSION EXPIRED
// ...
}
function post(url, data, config) {
return axios.post(url, data, {
headers: {
'X-XSRF-TOKEN': 'xxxxxxxx',
},
...config,
}).catch(dealWithRequestError);
}
复制代码
因此咱们须要对异常处理进行抽象。网络
项目上线前业务方可能提出稳定性的需求,这时咱们须要对请求进行监控,把接口请求成功和失败的状况都记录下来。一样,咱们把这部分代码也要写到公用的地方,相似这样:
function post(url, data, config) {
return axios.post(url, data, {
headers: {
'X-XSRF-TOKEN': 'xxxxxxxx',
},
...config,
}).then((result) => {
// 记录成功状况
...
return result;
})
.catch((error) => {
// 记录失败状况
...
return dealWithRequestError(error);
);
}
复制代码
因此咱们须要对请求监控进行抽象。
从上面对一个简单的 post
请求的案例分析中,咱们能够看到,数据请求主要涉及三方面 参数配置、异常处理 和 请求监控。上面例子的处理仍是比较粗糙,总体上仍是须要进行代码组织和分层。
首先,咱们处理下参数的配置,上面的例子只是对 post
请求做了分析,其实对于其余好比 get
,put
都同样的,咱们能够对这些请求做统一的处理。
request.js
import axios from 'axios';
// The http header that carries the xsrf token value { X-XSRF-TOKEN: '' }
const csrfConfig = {
'X-XSRF-TOKEN': '',
};
// Build uniform request
async function buildRequest(method, url, params, options) {
let param = {};
let config = {};
if (method === 'get') {
param = { params, ...options };
} else {
param = JSON.stringify(params);
config = {
headers: {
...csrfConfig,
},
};
config = Object.assign({}, config, options);
}
return axios[method](url, param, config);
}
export const get = (url, params = {}, options) => buildRequest('get', url, params, options);
export const post = (url, params = {}, options) => buildRequest('post', url, params, options);
复制代码
这样的话,咱们对外就暴露出 get
和 post
的方法,其余请求相似,在此只用 get
和 post
做为示例,入参分别是 API地址,数据 和 扩展配置。
其实异常处理场景会比较复杂,不是简单地 catch
一下,每每伴随着业务逻辑和UI的交互,异常主要有两方面,全局异常和业务异常。
全局异常,也能够说是通用的异常,好比服务端返回503,网络异常,登陆失效,无权限等,这些异常是能够预料并可控的,只要和服务端约定好格式,捕获下异常再展现出来便可。
业务异常,指的是和业务逻辑紧密相关的,好比提交失败,数据校验失败等,这些异常每每每一个接口有不同的状况,并且须要个性化展现错误,因此这部分可能不能进行统一处理,有时候须要把展现错误交到 View
层去实现。
在实现上,咱们不会直接在上面的请求方法中直接 catch
,而是利用 axios
提供的 interceptors
功能,这样能够将异常的处理和核心的请求方法隔离出来,毕竟这部分是要和 UI
进行交互的。咱们来看看如何实现:
error.js
import axios from 'axios';
// Add a response interceptor
axios.interceptors.response.use((response) => {
const { config, data } = response;
// 和服务端约定的 Code
const { code } = data;
switch (code) {
case 200:
return data;
case 401:
// 登陆失效
break;
case 403:
// 无权限
break;
default:
break;
}
if (config.showError) {
// 接口配置指定须要个性化展现错误
return Promise.reject(data);
}
// 默认展现错误
// ... Toast error
}, (error) => {
// 通用错误
if (axios.isCancel(error)) {
// Request cancel
} else if (navigator && !navigator.onLine) {
// Network is disconnect
} else {
// Other error
}
return Promise.reject(error);
});
复制代码
axios
的 interceptors
功能,其实就是一个链式调用,能够在请求前和请求后作事情,这里咱们在请求后进行拦截处理,对返回的数据进行校验和捕获异常,对于通用的错误咱们直接经过 UI
交互将错误展现出来,对于业务上的错误咱们检查下接口有没有配置说要个性化展现错误,若是有的话,将错误处理交给页面,若是没有的话,进行错误兜底处理。
请求监控这块和异常处理相似,只不过这里只是记录状况,不涉及到 UI
上的交互或者和业务代码的交互,因此能够把这部分逻辑直接写在异常处理那里,或者在请求后再添加一个拦截器,单独处理。
monitor.js
axios.interceptors.response.use((response) => {
const { status, data, config } = response;
// 根据返回的数据和接口参数配置,对请求进行埋点
}, (error) => {
// 根据返回的数据和接口参数配置,对请求进行埋点
});
复制代码
比较建议这样作,保持每一个模块独立,符合单一功能原则(SRP)。
好了,到如今为止,参数配置、异常处理 和 请求监控 都设计完了,有三个文件:
request.js
:请求库配置,对外暴露出 get
,post
方法。error.js
:请求的一些异常处理,涉及到和外面对接的是该接口是否须要个性化展现错误。monitor.js
:请求的状况记录,比较独立的一块。那在页面上调用的时候能够这样子:
import { get, post } from 'request.js';
get('/user/info').then((data) => {});
post('/user/update', { name: 'beyondxgb' }, { showError: true }).then((data) => {
if (data.code !== 200) {
// 展现错误
} else {
// do something
}
});
复制代码
再仔细思考下,以为还不是最完美的,API
名称直接在页面上引用,这样会给本身埋坑,若是后面 API
名称改了,并且这个 API
在多个页面被调用,那维护成本就高了。咱们有两种方法,第一种就是将全部 API
独立配置在一个文件中,给页面去读取,第二种办法就是咱们在请求库和页面以前再加一层,叫 service
,也就是所谓的服务层,对外暴露接口方法给页面,这样页面彻底不须要关注接口是什么或者接口是如何取数据的,并且之后接口的任何修改,只要在服务层进行修改便可,对页面没有任何影响。
固然我是采起第二种方法,相似这样子:
services.js
import { get, post } from 'request.js';
// fetch random data
export async function fetchRandomData(params) {
return get('https://randomuser.me/api', params);
}
// update user info
export async function updateUserInfo(params, options) {
return post('/user/info', params, { showError: true, ...options });
}
复制代码
这样子的话,页面就不会直接和请求库进行交互,而是跟服务层获取对应的方法。
import { fetchRandomData, updateUserInfo } from 'services.js';
fetchRandomData().then((data) => {});
updateUserInfo({ name: 'beyondxgb' }).then((data) => {
if (data.code !== 200) {
// 展现错误
} else {
// do something
}
});
复制代码
咱们来看看最终的方案是这样子的:
上面讲的都是以 axios
这个请求库为例,其实思想是互通的,换一个请求库也是同样的处理的方法。不知你们有没有注意到,把请求库参数配置和异常处理两个模块独立出来,彻底是利用了 interceptors
的特性,这也是我喜欢 axios
的缘由之一,我以为这个设计得很好,相似中间件的作法,在请求数据到达页面以前,咱们能够经过写拦截器对数据进行过滤、加工、校验、异常监控等。
我以为任何一个请求库均可以实现这个功能,就算请求库是有历史包袱,也能够本身在外面包一层。好比说有请求库 abc
,它有一个 request
方法,能够这样复写它:
import abc from 'abc';
function dispatchRequest(options) {
const reqConfig = Object.assign({}, options);
return abc.request(reqConfig).then(response => ({
response,
options,
})).catch(error => (
Promise.reject({
error,
options,
})
));
}
class Request {
constructor(config) {
this.default = config;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager(),
};
}
}
Request.prototype.request = function request(config = {}) {
// Add interceptors
const chain = [dispatchRequest, undefined];
let promise = Promise.resolve(options);
// Add request interceptors
this.interceptors.request.forEach((interceptor) => {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
// Add response interceptors
this.interceptors.response.forEach((interceptor) => {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
复制代码
前面咱们很好地解决了数据请求的问题,还有另外一方面,也是和数据请求紧密相关的,就是数据模拟(Mock) 了,在项目开发前期服务端没有准备好数据以前,咱们只有本身在本地进行 Mock
数据了,或者不少公司已经有比较好的平台实现这个功能了,我这里介绍下不借助平台,只是在本地启动一个小工具便可实现 Mock
数据。
这里我本身写了一个小工具 @ris/mock,只要把它做为中间件注入到 webpack-dev-server
中就行了。
webpack.config.js
const mock = require('@ris/mock');
module.exports = {
//...
devServer: {
compress: true,
port: 9000,
after: (app) => {
// Start mock data
mock(app);
},
}
};
复制代码
这时候在项目根目录创建 mock
文件夹,文件夹里建一个 rules.js
文件,rules.js
里面配置的是接口的映射规则,相似这样子:
module.exports = {
'GET /api/user': { name: 'beyondxgb' },
'POST /api/form/create': { success: true },
'GET /api/cases/list': (req, res) => { res.end(JSON.stringify([{ id: 1, name: 'demo' }])); },
'GET /api/user/list': 'user/list.json',
'GET /api/user/create': 'user/create.js',
};
复制代码
配置规则后,请求接口的时候,就会被转发,转发的时候能够是一个 对象
,函数
,文件
,详细使用能够参考文档。
在数据请求方案的设计中,也证明了咱们的“写代码”是“程序设计”,而不是“程序编写”,咱们要对本身的代码负责,如何让本身的代码可维护性高,易扩展,是优秀工程师的基本素养。
以上的方案已沉淀在 RIS 中,包含代码组织结构和技术实现,能够初始化一个 Standard 应用看看,以前的文章《RIS,建立 React 应用的新选择》 有简单提过,欢迎你们体验。