我们在使用 Axios 的过程中,或多或少地要用到它的拦截器,例如要实现:

  1. 数据转换;
  2. 添加额外的数据;
  3. 输出或上报接口的请求时间、失败率等数据;

这些需求,使用拦截器就能非常容易地实现。那么 axios 的拦截器怎么使用,内部又是怎么实现的,这篇文章让我们一探究竟。

1. 拦截器的使用

在 axios 中,拦截器分为请求拦截器和响应拦截器。顾名思义,请求拦截器是在发出请求之前按照顺序执行的,响应拦截器是在收到响应之后(无论接口返回的是否成功)按照顺序执行的。

如果我们要统计每个接口的耗时,可以先在请求拦截器中添加一个时间戳,在响应拦截器中减去这个时间戳,就是这个请求的完整耗时:

// 获取当前时间
const getTime = () => {
    if (typeof performance?.now === "function") {
        return window.performance.now();
    }
    return Date.now();
};
// 接口上报
const reportCgi = (response, config) => {
    // 响应失败时response为空
    const { config: conf } = response || { config };
    // 在响应拦截器中计算这个请求的耗时
    console.log("response", conf.url, getTime() - conf.requestime);
};
axios.interceptors.request.use((config) => {
    // 在请求拦截器中添加发起请求的时间
    return { ...config, ...{ requesttime: getTime() } };
});
axios.interceptors.response.use(
    (response) => {
        reportCgi(response);
        return response;
    },
    (error) => {
        reportCgi(error.response, error.config);
        return error;
    }
);

同时,我们还能添加多个请求拦截器和响应拦截器:

axios.interceptors.request.use((config) => {
    // 这里假设要先获取一个token
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve({ ...config, ...{ token: Math.random() } });
        }, 500);
    });
});
axios.interceptors.request.use((config) => {
    // 在请求拦截器中添加发起请求的时间
    return { ...config, ...{ requesttime: getTime() } };
});

除此之外,axios 的拦截器还能做很多事情,如输出请求 log 和响应 log,方便在移动端进行调试;上报接口的统计数据等。

2. 拦截器是怎么实现的

拦截器在我们进行接口请求时,非常的方便。那么它内部是如何实现的呢?如何维护多个拦截器并按照顺序执行的呢?

2.1 拦截器的实现

这里的关键文件就是 InterceptorManager.js,这里的代码也比较少,我们一点一点地看它是怎么实现的:

var utils = require("./../utils");

function InterceptorManager() {
    // 存储所有的拦截器,但请求拦截器和响应拦截器是分开的
    this.handlers = [];
}
/**
 * 添加拦截器
 * fulfilled: 成功时执行的,在Promise.resolve中
 * rejected: 失败时执行的,在Promise.reject中
 *
 * 返回当前添加的拦截器的ID,用于清除这个拦截器
 */
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
    // 把传入的在resolve和reject中要执行的方法添加到数组中
    this.handlers.push({
        fulfilled: fulfilled,
        rejected: rejected,
    });
    return this.handlers.length - 1;
};

/**
 * 根据id请求拦截器
 *
 * id: 刚才use方法返回的那个数据
 */
InterceptorManager.prototype.eject = function eject(id) {
    if (this.handlers[id]) {
        this.handlers[id] = null;
    }
};

/**
 * 迭代所有的拦截器
 *
 * 这里会跳过之前使用eject方法设置为null的拦截器
 *
 * @param {Function} fn 对所有拦截器都执行的一个方法
 */
InterceptorManager.prototype.forEach = function forEach(fn) {
    utils.forEach(this.handlers, function forEachHandler(h) {
        if (h !== null) {
            fn(h);
        }
    });
};

module.exports = InterceptorManager;

InterceptorManager 维护着 handlers 里面所有的拦截器,对外提供了 3 个方法:

  • use: 添加拦截器,接受 2 个参数,一个是 Promise 成功时执行的,第 2 个时 Promise 失败时执行的;
  • eject: 根据 id 清除这个拦截器;
  • forEach: 循环所有的拦截器,并跳过所有为空的拦截器;

InterceptorManager 并不区分是请求拦截器还是响应拦截器,它只是维护他自己的一组拦截器罢了。若创建多个对象,即可分别维护各自的拦截器。

function Axios(instanceConfig) {
    this.defaults = instanceConfig;
    this.interceptors = {
        request: new InterceptorManager(), // 请求拦截器
        response: new InterceptorManager(), // 响应拦截器
    };
}

将拦截器添加到 interceptors 的 request 和 response 两个属性中后,我们就可以像上面的那样调用 use 方法添加拦截器了。request 中维护的是请求拦截器,response 中维护的是响应拦截器。

2.2 将拦截器串联起来

创建一个chain数组,把所有的拦截器都放进去。我们首先把真正请求接口的方法放进去:

// dispatchRequest 用于请求数据,这里我们先展示不管怎么实现的
// 这里把 dispatchRequest 也当做拦截器添加到队列中
// 每2个是一组,前面用于Promise.resolve, 后面的1个用户Promise.reject
var chain = [dispatchRequest, undefined];

然后把请求拦截器放到 chain 的前面,因为我们要在发起请求之前先执行请求拦截器:

// 拦截器调用forEach方法,把每一个请求拦截器都添加到chain的前面
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    // 每2个是一组,前面用于Promise.resolve, 后面的1个用户Promise.reject
    // 由此也能看到,越是后添加的请求拦截器,越会是先执行
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
});

再把响应拦截器方法 chain 的后面,因为我们要在收到响应之后才执行响应拦截器:

// 拦截器调用forEach方法,把每一个响应拦截器都添加到chain的后面
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    // 响应拦截器按照顺序执行
    chain.push(interceptor.fulfilled, interceptor.rejected);
});

现在已经把所有的请求拦截器、数据请求和响应拦截器都串联起来了:

然后依次执行就可以:


// 把config初始化为一个Promise对象,方便后面的使用
var promise = Promise.resolve(config);

while (chain.length) {
    // 依次取出执行resolve和reject方法
    // 将执行后的结果传给下一个拦截器
    promise = promise.then(chain.shift(), chain.shift());
}

拦截器的功能就实现啦。

3. 总结

我们学习了 axios 中拦截器的思路,也可以在自己实现的一些功能组件中,使用这种机制,方便更多功能的扩展。