移花接木 · 给 Fetch 安装 Axios 拦截器

前言

在几个项目中用了 fetch,总体感觉还不错,优点就是灵活,可以根据不同的项目需求定制封装不同的请求。但是缺点也是灵活 🤣,正因为定制化,所以每个项目都得再复制一份然后魔改成需要的样子。

能不能像 Axios 一样,封装并提取一些公共部分?比如今天想和大家分享的 Interceptor 拦截器。

Axios 拦截器源码浅析

所谓拦截,有点类似于钩子的概念,在真正的请求发起之前和响应结束之后这两段时机内额外做一些其他事情,比如统一修改请求配置和处理响应结果。

1. 拦截器的模型设计

我们使用 axios.interceptors.request.use(resolved, rejected) 来注册拦截器。这里的 resolved(或者叫 fulfilled)和 rejected 最终都以回调函数的形式传递给了 Promise.prototype.then(onFulfilled, onRejected),用以处理成功和失败的情况。

除了注册功能,拦截器还能做很多其他事情,比如添加多个拦截器,删除拦截器,清空拦截器等。所以,我们可以将拦截器单独抽离出一个 InterceptorManager 类。

既然可以添加多个拦截器,那么这个类就可以维护一个数组,它的大概形状如下所示:

js 复制代码
class InterceptorManager {
    constructor() {
        this.handlers = [];
    }

    // 注册拦截器
    use(fulfilled, rejected) { /* ... */ }

    // 删除拦截器
    eject() { /* ... */ }

    // 清空拦截器
    clear() {
        if (this.handlers) {
            this.handlers = [];
        }
    }

    // 遍历拦截器
    forEach() { /* ... */ }
}

除简单的 clear 外,其他几个我们依次来看下源码是怎么实现的。

use 注册

use 其实就是增删改查中的增,将收集到的 fulfilledrejected 存入数组。

js 复制代码
use(fulfilled, rejected, options) {
    this.handlers.push({
      fulfilled,
      rejected,
      // ...
    });
    return this.handlers.length - 1;
}

注意: 它返回一个当前拦截器所在数组的 index。这样做的目的是将其当成 ID 使用,我们可以在注册时,接受这个 ID,在不需要的时候调用 eject 方法删除它(虽然不常用)。

eject 删除

eject 接收一个 ID,即 use 返回的拦截器所在数组的下标,eject 根据 ID 将其所对应的拦截器置为 null

js 复制代码
eject(id) {
    if (this.handlers[id]) {
        this.handlers[id] = null;
    }
}

之所以置为 null,也很好理解,因为直接删除的话,数组中元素的下标就乱掉了。

forEach 遍历

forEach 接收一个回调,它会在遍历的同时跳过已被 eject "删除"的拦截器,并执行回调,将每一个拦截器作为参数传入该回调。

js 复制代码
forEach(fn) {
    utils.forEach(this.handlers, function forEachHandler(h) {
      if (h !== null) {
        fn(h);
      }
    });
}

forEach 方法是为了多个拦截器链式调用时准备的,这个我们待会儿会讲到。

2. 拦截器的任务编排设计

什么数据结构能在某个成员的前面和后面添加其他成员,并依次遍历执行呢?没错,就是队列。

队列的先进先出(FIFO)原则,保证了拦截器的执行时机,即先添加的请求拦截会在请求本身之前执行,后添加的响应拦截会在请求本身之后执行。

拦截器队列

我们先在 Axios 类中实例化刚刚创建好的 InterceptorManager,请求、拦截各一个:

js 复制代码
class Axios {
  constructor(instanceConfig) {
    // other code ...
    this.interceptors = {
      request: new InterceptorManager(),
      response: new InterceptorManager()
    };
  }
}

以请求拦截为例,遍历 InterceptorManager 收集到的拦截器,备份到 Axios 维护的请求拦截器队列中,这里就用到了上文提到的 forEach 方法。

js 复制代码
request(configOrUrl, config) {
    // other code ...
    const requestInterceptorChain = [];

    this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
        // other code ...
        requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
    });
}

注意: 这里使用 unshift 进行添加,因为请求拦截是在请求本身之前。同理,响应拦截队列就应该使用 push 进行添加。

完整队列的链式调用

现在,Axios 中有了两个队列:请求拦截队列、响应拦截队列。我们得将它们组合到一起。此外,还差一样重要的东西:请求本身(dispatchRequest)。

dispatchRequest 用于真正的数据请求,这里我们先不用管它具体是怎么实现的。

我们再创建一个 chain 数组,将 dispatchRequest 放入其中:

js 复制代码
const chain = [dispatchRequest.bind(this), undefined];

注意: 这里还传入了 undefined。这是因为作为 .then() 的两个回调参数,onFulfilledonRejected 总是成对出现,你可以将 dispatchRequest 看成 onFulfilled,将 undefined 看成 onRejected

再把之前两个拦截器队列统统融入进来:

js 复制代码
chain.unshift.apply(chain, requestInterceptorChain);
chain.push.apply(chain, responseInterceptorChain);

至此,Axios 终于有了一个完整的队列。最后一步,我们需要一个引子,将整个队列执行起来。

既然请求拦截需要对 config 进行编辑,那我们就利用 config 创建一个给定 fulfilled 状态的 Promise 对象,有了这个引子,我们就能续上 chain 队列:遍历 chain,并将其传递给 promise,让它一直调用下去:

js 复制代码
request() {
    // other code ...

    let promise;
    let i = 0;
    let len;

    if (!synchronousRequestInterceptors) {
        const chain = [dispatchRequest.bind(this), undefined];
        chain.unshift.apply(chain, requestInterceptorChain);
        chain.push.apply(chain, responseInterceptorChain);
        len = chain.length;

        // 把 config 初始化为一个 Promise 对象,开始链式调用
        promise = Promise.resolve(config);

        while (i < len) {
            // 依次取出执行 onFulfilled 和 onRejected 方法
            // 将执行后的结果传给下一个拦截器
            promise = promise.then(chain[i++], chain[i++]);
        }

        return promise;
    }

    // other code ...
}

我们可以结合这张示意图来理解拦截器的设计。

实现 Fetch 拦截器

有了前置知识的了解,给 Fetch 安装拦截器就简单多了,InterceptorManager 类可以直接拿来用,我们简化其中的拦截器实现即可。

ts 复制代码
import InterceptorManager from './interceptorManager';
import dispatchRequest from './dispatchRequest';

type ResolvedFn<T = any> = (val: T) => T | Promise<T>;

type RejectedFn = (error: any) => any;

interface Interceptor<T> {
    resolved: ResolvedFn<T>;
    rejected?: RejectedFn;
}

interface Interceptors {
    request: InterceptorManager<RequestInit>;
    response: InterceptorManager<any>;
}

class FetchHttp {
    private readonly defaultConfig: RequestInit = {
        credentials: 'include', // 默认支持发送跨域 cookie
    };

    interceptors: Interceptors;

    constructor(config) {
        this.interceptors = {
            request: new InterceptorManager(),
            response: new InterceptorManager(),
        };

        // todo: 处理合并 config ......
    }

    async request(url: string, config: RequestInit = {}) {
        // other code ...

        // 声明一个 Promise 队列,存放所有的拦截器(包括真正用于请求的 dispatchRequest)
        const chain:  Array<Interceptor<RequestInit>> = [{
            resolved: dispatchRequest,
            rejected: undefined,
        }];

        // 将请求拦截塞进 chain 前面
        this.interceptors.request.forEach(interceptor => {
            chain.unshift(interceptor);
        });

        // 将响应拦截塞进 chain 后面
        this.interceptors.response.forEach(interceptor => {
            chain.push(interceptor);
        });

        // 利用 config 初始化一个 promise
        let promise = Promise.resolve({
            url,
            ...this.defaultConfig,
        });

        // 执行任务
        while (chain.length) {
            const { resolved, rejected } = chain.shift()!;
            promise = promise.then(resolved, rejected);
        }

        return promise;
    }
}

这里,我们用对象 [{ resolved: dispatchRequest, rejected: undefined }] 替代了原先的 [dispatchRequest, undefined],这种写法看上去更好理解,对 TS 而言也会有更好的类型支持。

总结

  1. 探寻 Axios 源码如何实现拦截器;
  2. 如法炮制给 Fetch 添加了拦截器。

篇幅有限,获取 TypeScript 版完整 Fetch 代码,可直接翻阅 FetchHttp 🤞❤️。关于 Fetch 的基本知识,可以参阅往期文章 Fetch API 的使用和采坑记录

现在,你可以将封装的 Fetch 上传到公司内部的 NPM 私有仓库。这样,就不用每次都 CP 再魔改了。

但是,如果将 Fetch 封装成了 Fexios,那我为什么不直接使用 Axios 呢?

参考资料

axios 源码系列之拦截器的实现

手写axios的拦截器实现原理

Axios | Github

Axios-InterceptorManager | Github

Axios-dispatchRequest | Github

相关推荐
m0_7482561417 分钟前
前端 MYTED单篇TED词汇学习功能优化
前端·学习
小白学前端6661 小时前
React Router 深入指南:从入门到进阶
前端·react.js·react
web130933203982 小时前
前端下载后端文件流,文件可以下载,但是打不开,显示“文件已损坏”的问题分析与解决方案
前端
outstanding木槿2 小时前
react+antd的Table组件编辑单元格
前端·javascript·react.js·前端框架
好名字08213 小时前
前端取Content-Disposition中的filename字段与解码(vue)
前端·javascript·vue.js·前端框架
隐形喷火龙3 小时前
element ui--下拉根据拼音首字母过滤
前端·vue.js·ui
m0_748241123 小时前
Selenium之Web元素定位
前端·selenium·测试工具
风无雨3 小时前
react杂乱笔记(一)
前端·笔记·react.js
前端小魔女3 小时前
2024-我赚到自媒体第一桶金
前端·rust
鑫~阳3 小时前
快速建站(网站如何在自己的电脑里跑起来) 详细步骤 一
前端·内容管理系统cms