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`字段,如下:


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`字段,如下:


从图中可以看到,请求头中`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;
}
最新文章
- 自动驾驶革命:激光雷达与深度学习重塑未来出行
- 汽车钣金变形登记证书:办理流程与重要性解析
- 高强度钢材+盗抢险:双重守护爱车的安全屏障
- 智能驾驶技术加速演进:激光雷达与V2X引领汽车产业变革
- 新手必看:变速箱、手刹与胎压的安全驾驶全攻略
- 激光雷达:自动驾驶汽车的智慧之眼
- 毫米波雷达:智能驾驶的核心传感器技术解析与应用
- 毫米波雷达:智能驾驶的安全守护者与未来趋势
- 充电桩与三元锂电池:高能量密度时代的协同发展
- 碳纤维:让汽车轻量化革命提速的超级材料
- 智能液晶屏革新车险服务:车损险显示技术解析与未来展望
- 驾驶证与汽车使用指南
- 智能驾驶与新能源:未来出行的三大技术革命
- 道路救援冷却液汽车必备紧急情况快速降温
- 汽车交强险查勘五大关键点:从安全防护到责任认定
- 汽车电瓶保养全攻略:延长寿命的关键技巧
- 智能驾驶与新能源革命:未来汽车技术发展全景
- 碳纤维与铝合金革新:汽车一体化设计与电子系统整合趋势
- 汽车照明效果优化方案
- 车辆严重损毁无法启动
