【前端探索】告别烂代码!用责任链模式封装网络请求

3,624 阅读10分钟

前言

用vue开发web页面的时候,axios几乎是必选的网络框架,我们有时候需要在请求发出前和收到响应后,对数据做一些处理,这时候就会用到axios的拦截器,如果拦截器中我们需要处理的逻辑太过复杂,有什么方案可以优化么?

将会学到

通过本文章,您将学到:

  • axios拦截器的使用
  • 用Typescript实现“责任链模式”
  • 怎么理解设计模式

待改造的代码

业务背景

作者开发一个活动的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判断,多个处理代码杂糅在一个文件中。

其实,我们可以把代码实现映射到现实生活,现实生活中我们的食品加工流水线,处理不同的加工步骤,会建立不同的加工节点,混合材料->造型->加热->包装,请求处理的过程,也可以封装成一个个的节点,每个节点只处理自己责任范围内的工作,这符合“单一职责原则”。

开始实操之前,我们先来了解一些基本知识。

什么是责任链模式?

责任链模式是一种行为设计模式, 允许你将请求沿着处理者链进行发送。 收到请求后, 每个处理者均可对请求进行处理, 也可以选择是否将其传递给链上的下个处理者。

*备注:有些文章对责任链模式会说:“直到有一个处理者进行处理,责任链就结束”,但是作者觉得这种说法不对,这样责任链就太不灵活了,真实的情况应该是:每一个处理者,都可以选择是否处理,是否向下传递。

axios的拦截器是什么?

官方文档的说明是:在请求或响应被 thencatch 处理前拦截它们。

使用方法如下:

// 添加请求拦截器
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源码,我们可以发现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的拦截器已经是链式处理了,为什么还要用责任链优化?

确实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两个方法。

  • checkHandle判断环境等各种因素,决定是否需要在改节点进行处理,返回true处理,返回false不处理。
  • 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链接和参数,在两个节点内部,有调用另外的责任链,去进行不同环境的处理(责任链模式的关键在于责任的分离,具体链子怎么组织,可以灵活处理)。

类似的,响应拦截器也实现了三个节点,于是,整体的请求流程和节点结构就是下面这样。

请求流程图 (2)

在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));

整体的目录结构如下图,红框中每一个文件,都是一个责任链节点,每个责任链节点都是对节点抽象类接口的实现。

这样改造后,代码量虽然有所增加,但是可读性和可扩展性却是极大极大的提升,效果显著。

以后如果需要增加一个处理方法,只需要修改对应的节点,或者新增一个节点再接入到链条中合适的位置。不用再每次都去修改入口文件了。

注意啦

总结一些的感悟:

  • 使用TypeScrpit的优势,除了类型检查,更重要的是“更好的面向接口编程”!!
  • 使用什么样的设计模式不重要!!!设计模式只是让设计符合设计原则的手段,即使没学设计模式,符合设计原则的也是好代码。
  • 设计模式还是要学!!!学了设计模式能更快设计出符合设计原则的好代码。

再再再贴一下六大设计原则

  1. 依赖倒置原则:高层模块不应该依赖底层模块。

  2. 开闭原则:对拓展开放,对修改封闭。

  3. 单一职责原则:一个类的职责只有一个。

  4. 替换原则(里氏替换原则):子类必须能够替换父类。

  5. 接口隔离原则:暴露给用户的接口小而完备。

  6. 迪米特法则(最少知识原则):一个对象应该对其他对象保持最少了解。

参考文献

TypeScript责任链模式讲解和代码示例

axios拦截器执行顺序的源码解释

axios源码分析——拦截器

往期好文

告别烂代码 第二期上线了!!!

【前端探索】告别烂代码第二期!用策略模式封装分享组件

其他好文

【三年前端开发的思考】如何有效地阅读需求?

【前端探索】图片加载优化的最佳实践

【前端探索】移动端H5生成截图海报的探索

【前端探索】H5获取用户定位?看这一篇就够了

【前端探索】微信小程序跳转的探索——开放标签为什么存在?

【前端探索】vConsole花式用法