当前位置 : 首页 > 维修数据

Axios拦截器实现:请求响应链式处理机制

<|begin▁of▁sentence|># 1. 前言 在上一篇文章中,我们实现了`axios`的请求和响应配置化,即用户可以配置`config`对象中的`url`、`method`、`params`、`data`、`headers`、`timeout`、`responseType`等属性。那么接下来,我们就来实现`axios`的请求响应拦截器功能。 # 2. 需求分析 在发送请求之前,我们可能需要对请求的配置`config`进行一些处理,例如:在请求头中添加`token`字段;在请求被`then`或`catch`处理之前,我们也需要对响应数据做一些处理,例如:根据响应状态码判断用户登录是否过期。这些在请求前和响应前处理函数我们称之为拦截器。 `axios`拦截器分为:请求拦截器和响应拦截器。它们又分别有成功拦截和失败拦截。 - 请求拦截器:在请求发送前进行一些处理 - 响应拦截器:在响应then/catch前进行一些处理 并且,拦截器也支持`Promise`链式调用,如下: ```javascript // 添加一个请求拦截器 axios.interceptors.request.use( function (config) { // 在发送请求之前可以做一些事情 return config; }, function (error) { // 处理请求错误 return Promise.reject(error); } ); // 添加一个响应拦截器 axios.interceptors.response.use( function (response) { // 处理响应数据 return response; }, function (error) { // 处理响应错误 return Promise.reject(error); } ); ``` 另外,用户也可以添加多个拦截器,并且多个拦截器会按照添加顺序执行,如下: ```javascript // 添加一个请求拦截器 axios.interceptors.request.use( function (config) { config.headers.token = "added by interceptor"; return config; }, function (error) { return Promise.reject(error); } ); // 再添加一个请求拦截器 axios.interceptors.request.use( function (config) { config.headers.age = "29"; return config; }, function (error) { return Promise.reject(error); } ); ``` 从上面代码中可以看到,我们添加了两个请求拦截器,一个是在请求头中添加`token`字段,一个是在请求头中添加`age`字段。那么这两个拦截器会按照添加顺序执行,最终在请求头中会有两个字段。 # 3. 整体设计 通过上面的需求分析,我们知道: - 拦截器分为请求拦截器和响应拦截器两种; - 每种拦截器都支持添加成功和失败的回调函数; - 并且可以添加多个拦截器,多个拦截器会按照添加顺序执行; 那么,我们应该如何设计呢? 其实,我们可以把`axios.interceptors.request`和`axios.interceptors.response`都定义为一个对象,该对象上有一个`use`方法来添加成功和失败的回调函数,并且我们可以把这些回调函数保存在对象内部的数组中,如下: ```javascript axios.interceptors = { request: [], response: [], }; axios.interceptors.request.use = (resolved, rejected) => { axios.interceptors.request.push({ resolved, rejected, }); }; axios.interceptors.response.use = (resolved, rejected) => { axios.interceptors.response.push({ resolved, rejected, }); }; ``` 当添加多个拦截器时,它们会按照添加顺序被保存在数组中。当真正发送请求的时候,再按照保存顺序取出执行。 OK,思路已经有了,接下来,我们就来实现拦截器功能。 # 4. 实现拦截器管理器 首先,我们为`Axios`类定义`interceptors`属性,如下: ```typescript export default class Axios { public defaults: AxiosRequestConfig; public interceptors: InterceptorManager; constructor(initConfig: AxiosRequestConfig) { this.defaults = initConfig; this.interceptors = { request: new InterceptorManager(), response: new InterceptorManager(), }; } // ... } ``` 从代码中可以看到,`interceptors` 属性是一个对象,该对象拥有两个属性:`request`和`response`,它们都是`InterceptorManager`的实例。那么,接下来我们就来实现`InterceptorManager`这个类。 ## 4.1 定义 InterceptorManager 类 `InterceptorManager`是一个泛型类,我们定义在`src/core/InterceptorManager.ts`中: ```typescript export interface OnFulfilled { (value: V): V | Promise; } export interface OnRejected { (error: any): any; } export interface Interceptor { onFulfilled?: OnFulfilled; onRejected?: OnRejected; } export default class InterceptorManager { public interceptors: Array | null>; constructor() { this.interceptors = []; } use(onFulfilled?: OnFulfilled, onRejected?: OnRejected): number { this.interceptors.push({ onFulfilled, onRejected, }); return this.interceptors.length - 1; } eject(id: number): void { if (this.interceptors[id]) { this.interceptors[id] = null; } } } ``` 我们定义了一个 `InterceptorManager` 类,该类被实例化后会有两个属性:`interceptors`,它是一个数组,用来存储拦截器;该类还提供了两个方法:`use` 和 `eject`。 - `use`:添加拦截器到 `interceptors` 中,并返回一个 `id` 用于删除; - `eject`:删除 `interceptors` 中某个拦截器; ## 4.2 修改 Axios 类型定义 由于我们给`Axios`类添加了`interceptors`属性,所以我们需要在`Axios`类型定义中添加该属性,如下: ```typescript export interface Axios { defaults: AxiosRequestConfig; interceptors: { request: InterceptorManager; response: InterceptorManager; }; request(config: AxiosRequestConfig): AxiosPromise; get(url: string, config?: AxiosRequestConfig): AxiosPromise; delete(url: string, config?: AxiosRequestConfig): AxiosPromise; head(url: string, config?: AxiosRequestConfig): AxiosPromise; options(url: string, config?: AxiosRequestConfig): AxiosPromise; post(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise; put(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise; patch(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise; } ``` OK,拦截器管理器已经定义好了,接下来我们就要在发送请求的时候来使用这些拦截器。 # 5. 实现拦截器链式调用 我们知道,拦截器是可以添加多个的,并且多个拦截器会按照添加顺序执行。那么,我们应该如何保证多个拦截器按照添加顺序执行呢? 其实,我们可以把发送请求的`dispatchRequest`函数和拦截器通过`Promise`链式调用来实现,如下: ```javascript // 组成一个`Promise`链 // 初始:dispatchRequest 是发送请求函数 let chain = [dispatchRequest, undefined]; // 把请求拦截器从头部插入 this.interceptors.request.forEach(interceptor => { chain.unshift(interceptor.onFulfilled, interceptor.onRejected); }); // 把响应拦截器从尾部插入 this.interceptors.response.forEach(interceptor => { chain.push(interceptor.onFulfilled, interceptor.onRejected); }); // 初始化一个已经resolve的Promise let promise = Promise.resolve(config); // 然后依次取出chain中的函数,并then到promise上,这样就实现了链式调用 while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); } return promise; ``` 从以上代码可以看出: - 我们首先构造一个数组`chain`,该数组的初始值是一个包含`dispatchRequest`函数和`undefined`的数组,为什么要有个`undefined`呢?因为我们`Promise`的`then`方法接收两个参数,第一个参数是成功回调,第二个参数是失败回调,而我们的拦截器有成功和失败两个回调,所以我们在添加拦截器的时候,需要同时添加成功和失败回调,如果没有,就用`undefined`占位。 - 然后,我们遍历请求拦截器,将每个请求拦截器的成功和失败回调依次从数组`chain`的头部插入; - 接着,再遍历响应拦截器,将每个响应拦截器的成功和失败回调依次从数组`chain`的尾部插入; - 最后,我们初始化一个已经`resolve`的`Promise`,然后依次取出`chain`数组中的函数,并`then`到`promise`上,这样就实现了拦截器的链式调用; OK,思路已经有了,接下来,我们就按照这个思路来改造`request`方法。 ## 5.1 改造 request 方法 我们在`src/core/Axios.ts`中改造`request`方法,如下: ```typescript request(config: AxiosRequestConfig): AxiosPromise { // 合并配置 const mergedConfig = mergeConfig(this.defaults, config); // 保存拦截器中间件 const chain: Array | undefined> = [ { onFulfilled: dispatchRequest, onRejected: undefined, }, ]; // 请求拦截器从前往后执行 this.interceptors.request.forEach(interceptor => { interceptor && chain.unshift(interceptor); }); // 响应拦截器从后往前执行 this.interceptors.response.forEach(interceptor => { interceptor && chain.push(interceptor); }); // 初始化promise let promise = Promise.resolve(mergedConfig); // 链式调用拦截器 while (chain.length) { const { onFulfilled, onRejected } = chain.shift()!; promise = promise.then(onFulfilled, onRejected); } return promise; } ``` 注意:这里我们定义了一个`InterceptorMiddleware`接口,因为`chain`数组中的每一项都是一个对象,该对象包含`onFulfilled`和`onRejected`两个属性,所以我们需要定义一个接口来描述这个对象,如下: ```typescript export interface InterceptorMiddleware { onFulfilled: OnFulfilled; onRejected?: OnRejected; } ``` OK,`request`方法已经改造完毕,接下来,我们就可以编写`demo`来测试拦截器功能了。 # 6. demo 编写 在 `examples` 目录下创建 `interceptor`目录,在 `interceptor`目录下创建 `index.`: ``` < lang="en"> interceptor ``` 该`demo`中,我们分别添加了请求拦截器和响应拦截器,在请求拦截器中,我们给请求头添加了`token`字段;在响应拦截器中,我们给响应数据添加了`name`字段。 接着,我们在 `examples` 目录下创建一个服务器,这里我们使用 `express` 来搭建一个简易服务器,在 `examples` 目录下创建 `server.js`: ```javascript const express = require("express"); const bodyParser = require("body-parser"); const app = express(); // 使用body-parser中间件 app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); app.get("/api/handleRequest", function(req, res) { res.json({ msg: "hello world", }); }); app.listen(3000, function() { console.log("server is listening on port 3000"); }); ``` 最后,我们在根目录下的`index.`中加上启动该`demo`的入口: ```
  • interceptor
  • ``` # 7. 运行 demo 我们按照之前的方法运行demo,在浏览器中打开 http://localhost:8000/ 点击 interceptor,通过F12的控制台我们可以看到:请求已经正常发出,并且请求头中已经加上了`token`字段,并且响应数据中也已经加上了`name`字段,如下: ![](~@/axios/07/01.png) ![](~@/axios/07/02.png) OK,这样我们就实现了拦截器功能。 # 8. 遗留问题 我们虽然已经实现了拦截器功能,但是目前还存在一个问题:如果我们添加多个拦截器,那么这些拦截器会按照添加顺序执行吗?我们不妨来测试一下。 我们修改`demo`,添加多个拦截器,如下: ```javascript // 添加请求拦截器1 axios.interceptors.request.use( function(config) { config.headers.token1 = "token1!!!"; return config; }, function(error) { return Promise.reject(error); } ); // 添加请求拦截器2 axios.interceptors.request.use( function(config) { config.headers.token2 = "token2!!!"; return config; }, function(error) { return Promise.reject(error); } ); // 添加响应拦截器1 axios.interceptors.response.use( function(response) { response.data.name1 = "response interceptor1"; return response; }, function(error) { return Promise.reject(error); } ); // 添加响应拦截器2 axios.interceptors.response.use( function(response) { response.data.name2 = "response interceptor2"; return response; }, function(error) { return Promise.reject(error); } ); ``` 我们添加了两个请求拦截器和两个响应拦截器,按照我们的设计,请求拦截器应该按照添加顺序执行,即先添加的先执行,后添加的后执行;而响应拦截器应该按照添加顺序的逆序执行,即先添加的后执行,后添加的先执行。 我们运行demo,通过F12的控制台我们可以看到:请求头中已经加上了`token1`和`token2`字段,并且响应数据中也已经加上了`name1`和`name2`字段,如下: ![](~@/axios/07/03.png) ![](~@/axios/07/04.png) 从图中可以看到,请求头中`token1`和`token2`字段都已经加上了,并且`token1`在`token2`的前面,这说明请求拦截器是按照添加顺序执行的;而响应数据中`name1`和`name2`字段也都加上了,并且`name2`在`name1`的前面,这说明响应拦截器是按照添加顺序的逆序执行的。 为什么会这样呢?因为我们在构造`chain`数组的时候,请求拦截器是从`chain`数组的头部插入的,所以先添加的请求拦截器会在数组的尾部,后添加的请求拦截器会在数组的头部,而我们在执行的时候是从数组头部开始依次取出执行的,所以后添加的请求拦截器会先执行,先添加的请求拦截器会后执行,这跟我们预期的顺序是相反的。 同样,响应拦截器是从`chain`数组的尾部插入的,所以先添加的响应拦截器会在数组的尾部,后添加的响应拦截器会在数组的头部,而我们在执行的时候是从数组头部开始依次取出执行的,所以后添加的响应拦截器会先执行,先添加的响应拦截器会后执行,这跟我们预期的顺序也是相反的。 所以,我们需要调整一下,让请求拦截器先添加的先执行,后添加的后执行;响应拦截器先添加的后执行,后添加的先执行。 那么,我们该如何调整呢? 其实很简单,我们只需要在添加拦截器的时候,把拦截器插入到`chain`数组中的顺序调整一下即可。 - 对于请求拦截器,我们应该把后添加的放在`chain`数组的尾部,先添加的放在`chain`数组的头部,这样在执行的时候,先添加的就会先执行,后添加的就会后执行; - 对于响应拦截器,我们应该把先添加的放在`chain`数组的尾部,后添加的放在`chain`数组的头部,这样在执行的时候,后添加的就会先执行,先添加的就会后执行; 所以,我们修改`request`方法,如下: ```typescript request(config: AxiosRequestConfig): AxiosPromise { // 合并配置 const mergedConfig = mergeConfig(this.defaults, config); // 保存拦截器中间件 const chain: Array | undefined> = [ { onFulfilled: dispatchRequest, onRejected: undefined, }, ]; // 请求拦截器从后往前执行 this.interceptors.request.forEach(interceptor => { interceptor && chain.unshift(interceptor); }); // 响应拦截器从前往后执行 this.interceptors.response.forEach(interceptor => { interceptor && chain.push(interceptor); }); // 初始化promise let promise = Promise.resolve(mergedConfig); // 链式调用拦截器 while (chain.length) { const { onFulfilled, onRejected } = chain.shift()!; promise = promise.then(onFulfilled, onRejected); } return promise; }

    栏目列表