axios 拦截器机制是如何实现的?

本文是"axios源码系列"第四篇,你可以查看以下链接了解过去的内容。

  1. axios 是如何实现取消请求的?
  2. 你知道吗?axios 请求是 JSON 响应优先的
  3. axios 跨端架构是如何实现的?

axios 不仅提供了一套跨平台请求,另外还提供了"拦截器"这一中间件机制,方便你在请求之前、响应之后做些操作处理。

axios 拦截器简介

axios 中的拦截器分"请求拦截器"和"响应拦截器"。

请求拦截器是在请求正式发起前调用的。

js 复制代码
// Add a request interceptor
axios.interceptors.request.use(function (config) {
    // Do something before request is sent
    return config;
  }, function (error) {
    // Do something with request error
    return Promise.reject(error);
  });

请求拦截器是通过 axios.interceptors.request.use() 方法注册的,你可以多次调用,这样可以同时设置多个拦截器。

请求拦截器的作用允许你在请求正式发起前,对请求配置信息做最后的统一修改。

当然,拦截器也支持移除。

js 复制代码
const interceptorID = axios.interceptors.request.use(function () {/*...*/});
axios.interceptors.request.eject(interceptorID);

axios.interceptors.request.use() 会返回当前拦截器在内部 ID 值(一个数值),你可以通过 axios.interceptors.eject(interceptorID) 移除这个拦截器。

响应拦截器与此同理。

js 复制代码
// Add a response interceptor
axios.interceptors.response.use(function (response) {
    // Any status code that lie within the range of 2xx cause this function to trigger
    // Do something with response data
    return response;
  }, function (error) {
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
    return Promise.reject(error);
  });

响应拦截器是通过 axios.interceptors.response.use() 方法注册的,你可以多次调用,这样可以同时设置多个拦截器。

响应拦截器不仅作用于有响应的返回(比如 2xx、 4xx、5xx),对于像网络请求意外中断(比如超时)、跨域请求错误(CORS)这样没有响应的请求同样适用。

响应拦截器的作用是允许你在响应在给用户处理之前,先对响应结果(比如 reponse.data)做统一处理。

同时,你可以通过 interceptors.response.eject(interceptorID) 移除特定的响应拦截器。

Axios 实例

你可以将 axios 库暴露出来的 axios 对象近似看成 是内部 Axios 类的实例。

Axios 类实例在创建的时候,会挂载一个对象属性 interceptors,这个对象又包含 request 和 response 2 个属性,它们都是 InterceptorManager 类的实例对象。

js 复制代码
// /v1.6.8/lib/core/Axios.js
class Axios {
  constructor(instanceConfig) {
    this.defaults = instanceConfig;
    this.interceptors = {
      request: new InterceptorManager(),
      response: new InterceptorManager()
    };
  }

拦截器类

InterceptorManager 类也就是拦截器类了,它的大概结构如下。

js 复制代码
// /v1.6.8/lib/core/InterceptorManager.js
class InterceptorManager {
  constructor() {
    this.handlers = [];
  }
    
  // Add a new interceptor to the stack
  use(fulfilled, rejected, options) {/* ... */}
  // Remove an interceptor from the stack
  eject(id) {/* ... */}
  // Clear all interceptors from the stack
  clear() {/* ... */}
  // Iterate over all the registered interceptors
  forEach(fn) {/* ... */}
}

InterceptorManager 内部维护了一个拦截器数组 handlers。

实例上除了前面介绍过的 use() 和 eject() 方法,还有另外 2 个 clear()、forEach() 方法,这 4 个方法本质上都是对内部 handlers 数组进行操作。

先说 use()。

js 复制代码
// /v1.6.8/lib/core/InterceptorManager.js#L18
use(fulfilled, rejected, options) {
  this.handlers.push({
    fulfilled,
    rejected,
    synchronous: options ? options.synchronous : false,
    runWhen: options ? options.runWhen : null
  });
  return this.handlers.length - 1;
}

use() 方法是用来注册拦截器的,也就是向 handlers 数组添加一个成员。

use() 接收 3 个参数,第一个参数和第二个参数分别用来定义拦截器正常操作和异常捕获的逻辑;最后一个选项参数 options 是可选的,定位拦截器行为,不太常用,为避免行文冗长,不会介绍。

use() 会返回当前拦截器在内部 handlers 数组的索引值,是后续用来移除拦截器的标识(interceptorID)。

再是 eject(id)。

js 复制代码
// /v1.6.8/lib/core/InterceptorManager.js#L35-L39
eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
}

就是将内部拦截器置空(null),比较直观,没什么好说的。

还有 clear()。

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

就是清空内部 handlers 数组。

最后是 forEach(fn)。

js 复制代码
// /v1.6.8/lib/core/InterceptorManager.js#L62-L68
forEach(fn) {
  this.handlers.forEach(function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
}

用来遍历所有拦截器,不过这里会做为空判断,确保会忽略被移除的拦截。

到这里我们就讲清楚拦截器实例了。

那么 axios 是在发请求前后是如何让拦截器生效的呢?

拦截器实现原理

在上文的学习中,我们了解到 axios 请求会由内部 axios._request() 方法处理,而在 axios._request() 方法内部,会调用 dispatchRequest() 方法进行实际请求。

不过,dispatchRequest() 方法执行前后,其实是有拦截器执行逻辑的,不过之前没说,现在我们就来看下。

在 axios 内部,实际的请求会包含拦截器的执行逻辑,以便做中间操作。

收集拦截器

在此之前,我们就要先收集拦截器。

js 复制代码
// /v1.6.8/lib/core/Axios.js#L115-L131
const requestInterceptorChain = [];
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
  requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});

const responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
  responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});

这里使用了 2 个变量 requestInterceptorChain 和 responseInterceptorChain 来分别收集使用 axios.interceptors.request.use() 和 axios.interceptors.response.use() 注册的拦截器。

收集的过程采用的是 interceptor 实例的 forEach() 方法,这样就能过滤掉被移除的拦截器。

这里有 2 个点值得注意:

  1. 拦截器推入 2 个 Chain 的方式是 .unshift/push(interceptor.fulfilled, interceptor.rejected) 而非 .unshift/push(interceptor),其实 2 种方式都行,不过 axios 选择了前者
  2. 另外,请求拦截器的推入方式是使用 unshift()(而非响应拦截器的 push()),就表示后注册的请求拦截器会先执行,这一点需要注意
js 复制代码
axios.interceptors.request.use(
  (config) => {
    console.log('request interceptor 1')
    return config;
  }
)

axios.interceptors.request.use(
  (config) => {
    console.log('request interceptor 2')
    return config;
  }
)

axios.get('https://httpstat.us/200')
  
// request interceptor 2
// request interceptor 1

拼接请求链

收集好拦截器后,就着手拼接请求链了。

js 复制代码
// /v1.6.8/lib/core/Axios.js#L133-L150
let promise;
let i = 0;
let len;

const chain = [
  ...requestInterceptorChain,
  ...[dispatchRequest.bind(this), undefined],
  ...responseInterceptorChain
]
len = chain.length;

promise = Promise.resolve(config);

while (i < len) {
  promise = promise.then(chain[i++], chain[i++]);
}

return promise;

请求链 chain 以请求拦截器开始、实际请求居中、响应拦截器最后的方式拼接。

整个 Promise 链以请求配置 config 作为起点执行,到 dispatchRequest() 之后返回 response,因此后续响应拦截器的入参变成 response 了。

当我们以 axios.get('httpstat.us/200').then(....then(console.log) "https://httpstat.us/200').then(console.log)") 请求时,对 axios 来说,完整的请求链是:

js 复制代码
Promise.resolve({ url: 'https://httpstat.us/200' })
  .then(
    requestInterceptorFulfilled,
    requestInterceptorRejected,
  )
  .then(
    dispatchRequest.bind(this),
  )
  .then(
    responseInterceptorFulfilled,
    responseInterceptorRejected,
  )
  .then(
    console.log
  )

由此,我们便讲完了 axios 的拦截器的执行原理以及 axios 的完整请求链结构。

由请求链结构看错误处理

明白了 axios 请求链结构,我们就能明白 axios 中抛错场景的底层逻辑。

第一个请求拦截器出错

通过前面的学习,我们已经知道请求拦截器的执行跟注册顺序是相反的,第一个请求拦截器会最后执行。

当第一个请求拦截器出错时,由于 dispatchRequest() 部分并没有处理异常的部分。

js 复制代码
.then(
  dispatchRequest.bind(this),
)

所以,错误会直接穿透到第一个响应拦截器,交由对应的 rejected 函数处理。

js 复制代码
axios.interceptors.request.use(
  (config) => {
    console.log('request interceptor 1')
    throw new Error('Oops 1')
  }
)

axios.interceptors.response.use(
  undefined,
  (error) => {
    console.log('[error] response interceptor 1', error)
  }
)

axios.get('https://httpstat.us/200').then(console.log)

效果如下:

由于第一个响应拦截器的 onReject 函数没有返回值,所以 .then(console.log) 最终打印出来的时 undefined。

最后一个请求拦截器出错

最后一个请求拦截器会最先执行,它的错误会被倒数第二个请求拦截器的 rejected 函数处理。

js 复制代码
axios.interceptors.request.use(
  undefined,
  (error) => {
    console.log('[error] request interceptor 1', error)
  }
)

axios.interceptors.request.use(
  (config) => {
    console.log('request interceptor 2')
    throw new Error('Oops 2')
  }
)

axios.interceptors.response.use(
  undefined,
  (error) => {
    console.log('[error] response interceptor 1', error)
  }
)

axios.get('https://httpstat.us/200').then(console.log)

效果如下:

最后一个请求拦截器"request interceptor 2"出错后,会由"request interceptor 1"的 rejected 函数处理。

由于"request interceptor 1"的 rejected 函数没有返回值,导致传递给 dispatchRequest() 实际执行时接收到的 config 是 undefined,内部就会报错了。

dispatchRequest() 的错误会由 "response interceptor 1"的 rejected 函数处理,由于"response interceptor 1"的 rejected 函数没有返回值,导致 .then(console.log) 打印结果 undefined。

axios 请求出错

axios 请求如果出错,会先经过响应拦截器处理。

js 复制代码
axios.interceptors.response.use(
  undefined,
  (error) => {
    console.log('[error] response interceptor 1', error)
  }
)

axios.get('https://httpstat.uss/200').then(console.log)

效果如下:

我们请求了一个无效地址,结果报错被响应拦截器处理,由于没有返回值,导致 .then(console.log) 打印结果 undefined。

响应拦截器出错

响应拦截器之后就是用户自己的处理逻辑了,如果是在响应拦截器中出错,那么就会错误就会落入用户的 catch 处理逻辑。

js 复制代码
axios.interceptors.response.use(
  (response) => {
    console.log('response interceptor 1')
    throw new Error('Oops 1')
  }
)

axios.get('https://httpstat.uss/200').catch(err => console.log('err >>>', err))

效果如下:

总结

axios 中提供了一个中间件机制称为"拦截器",分请求拦截器和响应拦截器 2 种。请求拦截器在请求发起之前执行,允许你修改请求配置,执行顺序与注册顺序相反;响应拦截器在请求完成之后执行,允许你修改响应对象,执行顺序与注册顺序一样。

本文带大家由浅入深地了解了 axios 的拦截器原理,并查看了 axios 完整的请求链,最后基于请求链结构了解 axios 中抛错处理场景的底层逻辑。

希望大家阅读之后能有所收获,感谢你的阅读。再见!

相关推荐
dr李四维8 分钟前
iOS构建版本以及Hbuilder打iOS的ipa包全流程
前端·笔记·ios·产品运营·产品经理·xcode
I_Am_Me_22 分钟前
【JavaEE进阶】 JavaScript
开发语言·javascript·ecmascript
雯0609~29 分钟前
网页F12:缓存的使用(设值、取值、删除)
前端·缓存
℘团子এ33 分钟前
vue3中如何上传文件到腾讯云的桶(cosbrowser)
前端·javascript·腾讯云
学习前端的小z38 分钟前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
前端百草阁1 小时前
【TS简单上手,快速入门教程】————适合零基础
javascript·typescript
彭世瑜1 小时前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
FØund4041 小时前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
Backstroke fish1 小时前
Token刷新机制
前端·javascript·vue.js·typescript·vue
zwjapple1 小时前
typescript里面正则的使用
开发语言·javascript·正则表达式