用vue开发web页面的时候,axios几乎是必选的网络框架,我们有时候需要在请求发出前和收到响应后,对数据做一些处理,这时候就会用到axios的拦截器,如果拦截器中我们需要处理的逻辑太过复杂,有什么方案可以优化么?
通过本文章,您将学到:
作者开发一个活动的H5页面,页面被用在多个浏览器环境,比如微信、QQ、自己开发的app、其他第三方的app等等等等。由于后台接口对不同环境登录态的处理不同,需要判断不同环境,取cookie里面的不同值,传不同的登录态token,有时候还要针对某些特殊环境,对网络请求有一些特殊处理。
于是,axios拦截器的代码就变成了下面这样。
const requestInterceptor = (config) => {
// ...一些通用的处理
if (env.isQQ) {
// 对QQ的特殊处理
} else if (env.isWeixin) {
// 对微信登录态的特殊处理
} else if (env.isMyApp) {
// 对自己app的特殊处理
} else if (env.isApp1) {
// 特殊处理...
} else if (env.isApp2) {
// 特殊处理...
} else if (env.isApp3) {
// 特殊处理...
} else {
// 特殊处理
}
if (xxx) {
// 一些不怎么通用的特殊处理
}
if (yyy) {
// 另一些不怎么通用的特殊说处理
}
// ... 另外一些通用处理
return config;
};
旧代码的问题在于有太多的if-else判断,多个处理代码杂糅在一个文件中。
其实,我们可以把代码实现映射到现实生活,现实生活中我们的食品加工流水线,处理不同的加工步骤,会建立不同的加工节点,混合材料->造型->加热->包装,请求处理的过程,也可以封装成一个个的节点,每个节点只处理自己责任范围内的工作,这符合“单一职责原则”。
开始实操之前,我们先来了解一些基本知识。
责任链模式是一种行为设计模式, 允许你将请求沿着处理者链进行发送。 收到请求后, 每个处理者均可对请求进行处理, 也可以选择是否将其传递给链上的下个处理者。
*备注:有些文章对责任链模式会说:“直到有一个处理者进行处理,责任链就结束”,但是作者觉得这种说法不对,这样责任链就太不灵活了,真实的情况应该是:每一个处理者,都可以选择是否处理,是否向下传递。
官方文档的说明是:在请求或响应被 then
或 catch
处理前拦截它们。
使用方法如下:
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response;
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error);
});
通过阅读axios源码,我们可以发现axios拦截器本身也是链式的结构,其维护了一个数组chain,在这个chain数组中保存了拦截器函数。
这里需要注意下拦截器的执行顺序,请求拦截器是倒序执行的,响应拦截器是正序执行的。
为什么是这样的处理顺序呢?我们来看下具体代码:
// 将请求拦截器,和响应拦截器,以及实际的请求(dispatchRequest)的方法组合成数组,类似如下的结构
// [请求拦截器1success, 请求拦截器1error, 请求拦截器2success, 请求拦截器2error, dispatchRequest, undefined, 响应拦截器1success, 响应拦截器1error,响应拦截器2success, 响应拦截器2error]
var chain = [dispatchRequest, undefined];
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
可以看到,chain数组最原始状态是[dispatchRequest, undefined],dispatchRequest是实际发送请求的方法。
拦截器函数被压入chain数组的时候,请求拦截器是unshift压入的,响应拦截器是push压入的。
所以,我们按:请求拦截器1,请求拦截器2,响应拦截器1,响应拦截器2这样的顺序绑定拦截器,在chain中的数组,是**[请求拦截器2success, 请求拦截器2error, 请求拦截器1success, 请求拦截器1error, dispatchRequest, undefined, 响应拦截器1success, 响应拦截器1error, 响应拦截器2success, 响应拦截器2error]**。
执行请求的promise,是这样组织的。
// 开始执行整个请求流程(请求拦截器->dispatchRequest->响应拦截器)
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
于是,我们的执行顺序就变成了chain的顺序:请求拦截器2->请求拦截器1->实际请求->响应拦截器1->响应拦截器2。
确实axios已经是链式处理了,但是按改造前的代码,现在的处理方式还是不太优美,我们可以把这里面的if-else拆分成多个拦截器,但是这里面的代码还是有太多判断而且不利于扩展。
相比于axios的拦截器,责任链模式是一种更通用的,对请求进行各种不同处理的方式,更利于划分代码,而且以后就算不用axios了,现有的责任链也可以直接复用。
其次,使用TypeScrpit实现责任链模式,我们可以用面向接口的方式,来实现我们处理请求的方法。
!!!记住一点,使用TypeScrpit的优势,除了类型检查,更重要的是“更好的面向接口编程”。
了解基本的知识,我们就可以开始操作起来了。
首先定义责任链节点的抽象类,在传统责任链模式的基础上,我增加了handleRequest和handleRequestOnce两种运行责任链的方式,一种是一直顺序执行到没有下一个节点,一种是一直顺序执行知道有一个节点接受处理,按需选择。
// 请求拦截器责任链节点抽象类
export default abstract class BaseHandler {
// 下一个节点
private nextHandler: BaseHandler | null = null;
// 设置下一个节点
public setNextHandler(handler: BaseHandler): BaseHandler {
this.nextHandler = handler;
return this.nextHandler;
}
// 调用该节点的方法处理请求,处理后,调用下一节点继续处理
public handleRequest(config: any): void {
// 当前节点的处理
if (this.checkHandle(config)) {
this.handler(config);
}
// 继续执行下一个节点
if (this.nextHandler) {
this.nextHandler.handleRequest(config);
}
}
// 调用该节点的方法处理请求,有一个节点处理就直接退出(互斥的)
public handleRequestOnce(config: any): void {
// 当前节点的处理
if (this.checkHandle(config)) {
this.handler(config);
} else if (this.nextHandler) {
this.nextHandler.handleRequestOnce(config);
}
}
// 判断是否在这个节点处理
abstract checkHandle(config: any): boolean;
// 处理的方式
abstract handler(config: any): void;
}
我们实现抽象类,只需要实现checkHandle和handle两个方法。
比如下面一个处理小程序登录态的节点,其他的节点也是类似的实现。
import BaseHandler from '../../base-handler';
import { getUrlPara } from '../../../url';
import { env } from '../../../env';
// 处理小程序内嵌H5的请求参数
export default class WechatMpParamsHandler extends BaseHandler {
// 在王者人生小程序,且为QQ登录,才用小程序登录态,其他情况下都用微信登录态
checkHandle(): boolean {
return env.isMiniProgram;
}
handler(config: any): any {
if (getUrlPara('_ticket')) {
config.params._ticket = getUrlPara('_ticket');
}
return config;
}
}
在拦截器中,我们拼接责任链节点,并调用责任链处理请求和响应。
结合我们的业务场景,我们为请求拦截器实现了UrlInterceptor和ParamsInterceptor两个节点,分别处理请求的URL链接和参数,在两个节点内部,有调用另外的责任链,去进行不同环境的处理(责任链模式的关键在于责任的分离,具体链子怎么组织,可以灵活处理)。
类似的,响应拦截器也实现了三个节点,于是,整体的请求流程和节点结构就是下面这样。
在axios拦截器中使用责任链。
// 请求拦截器
const requestInterceptor = function (config) {
const requestHandlerChain = new UrlInterceptor(); // 请求的责任链节点:CGI的URL处理
requestHandlerChain.setNextHandler(new ParamsInterceptor()); // 请求的责任链节点:CGI的参数处理
requestHandlerChain.handleRequest(config);
return config;
};
// 响应拦截器
const responseInterceptor = (response) => {
const responseHandlerChain = new ParseDataInterceptor(); // 返回数据的责任链节点:解析新老框架的数据
responseHandlerChain.setNextHandler(new LoginInfoInterceptor()) // 返回数据的责任链节点:处理登录信息
.setNextHandler(new ErrorToastInterceptor()); // 返回数据的责任链节点:处理错误提示
responseHandlerChain.handleRequest(response);
return response;
};
// 请求拦截器
axiosInstance.interceptors.request.use(requestInterceptor, error => Promise.reject(error));
// 响应拦截器
axiosInstance.interceptors.response.use(responseInterceptor, error => Promise.reject(error));
整体的目录结构如下图,红框中每一个文件,都是一个责任链节点,每个责任链节点都是对节点抽象类接口的实现。
这样改造后,代码量虽然有所增加,但是可读性和可扩展性却是极大极大的提升,效果显著。
以后如果需要增加一个处理方法,只需要修改对应的节点,或者新增一个节点再接入到链条中合适的位置。不用再每次都去修改入口文件了。
总结一些的感悟:
再再再贴一下六大设计原则