当前位置 : 首页 > 购买指南

Axios拦截器实现原理与功能详解

<|begin▁of▁sentence|># 1. 前言 在之前的文章中,我们介绍了`axios`的拦截器,并简单实现了其功能。拦截器包括请求拦截器和响应拦截器,可以在请求发送前和响应返回后对数据进行处理。这是`axios`非常强大的一点,接下来,我们就来探究一下`axios`拦截器的设计思想,并编写代码实现。 # 2. 拦截器的使用 在介绍拦截器的设计思想之前,我们先来看下拦截器是如何使用的。 ## 2.1 添加拦截器 ```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); } ); ``` ## 2.2 移除拦截器 ```javascript const myInterceptor = axios.interceptors.request.use(function() { /*...*/ }); axios.interceptors.request.eject(myInterceptor); ``` ## 2.3 为自定义 axios 实例添加拦截器 ```javascript const instance = axios.create(); instance.interceptors.request.use(function() { /*...*/ }); ``` # 3. 拦截器的设计思想 通过上面的使用示例,我们可以看到: - `axios`对象上有一个`interceptors`对象属性,该属性又有`request`和`response`2 个属性,它们都有一个`use`方法和`eject`方法。`use`方法用于添加拦截器函数,`eject`方法用于移除拦截器函数。 - 当我们使用`use`方法添加拦截器函数时,该方法会返回一个拦截器`id`,以便在需要的时候通过`eject`方法将拦截器移除。 - 另外,我们还可以为自定义的`axios`实例添加拦截器。 那么,根据以上使用方式,我们可以提炼出以下要点: 1. `axios`对象上有`interceptors`属性,且该属性有`request`和`response`属性; 2. `interceptors.request`和`interceptors.response`对象上有`use`和`eject`方法; 3. `use`方法接收两个参数,分别是处理成功和处理失败的函数,并且返回一个拦截器`id`; 4. `eject`方法接收拦截器`id`作为参数,用来移除拦截器; 5. 可以为自定义`axios`实例添加拦截器; 那么,接下来,我们就根据以上要点来实现拦截器。 # 4. 拦截器的实现 ## 4.1 实现思路 根据上面的分析,我们可以在`Axios`构造函数上添加一个`interceptors`属性,该属性又有`request`和`response`两个属性,这两个属性都是`InterceptorManager`类的实例,该类是用来管理拦截器的,它内部会有`use`和`eject`方法,分别用来添加和移除拦截器。 另外,当通过`use`方法添加拦截器时,每一个拦截器函数都会被保存到该类实例的`handlers`数组中,并且每一个拦截器函数都会有一个`id`,该`id`就是当前拦截器在`handlers`数组中的索引下标,同时`use`方法会返回该`id`,以便后续通过`eject`方法将拦截器移除。 OK,思路已经理清,接下来,我们就开始编写代码实现。 ## 4.2 代码实现 ### 4.2.1 创建 InterceptorManager 类 我们先创建一个`InterceptorManager`类,该类是用来管理拦截器的,代码如下: ```typescript // src/core/InterceptorManager.ts import { ResolvedFn, RejectedFn } from "../types"; interface Interceptor { resolved: ResolvedFn; rejected?: RejectedFn; } export default class InterceptorManager { private interceptors: Array | null>; constructor() { this.interceptors = []; } use(resolved: ResolvedFn, rejected?: RejectedFn): number { this.interceptors.push({ resolved, rejected, }); return this.interceptors.length - 1; } eject(id: number): void { if (this.interceptors[id]) { this.interceptors[id] = null; } } } ``` 我们创建了一个`InterceptorManager`类,该类被定义为一个泛型类,因为我们即需要管理请求拦截器又需要管理响应拦截器,而请求拦截器和响应拦截器的参数类型是不同的,所以我们需要使用泛型参数`T`。 在该类的内部,我们定义了一个私有属性`interceptors`,它是一个数组,用来存储拦截器,数组的每一项是一个`Interceptor`类型对象,该对象包含`resolved`和`rejected`属性。 接着,我们给该类添加了`use`方法,该方法接收两个参数,分别是`resolved`函数和`rejected`函数,调用该方法会向`interceptors`数组中`push`一个`Interceptor`对象,并返回该对象在数组中的索引,也就是拦截器`id`。 最后,我们给该类添加了`eject`方法,该方法接收拦截器`id`作为参数,将`interceptors`数组中对应`id`的项置为`null`。这里我们并没有将该拦截器从数组中删除,而是置为`null`,这样做是为了避免我们在遍历`interceptors`数组时,删除某项后数组的索引发生变化,导致`eject`的`id`不是之前`use`返回的`id`。 ### 4.2.2 修改 Axios 类 创建好`InterceptorManager`类之后,我们需要在`Axios`类中创建`interceptors`属性,如下: ```typescript // src/core/Axios.ts import { InterceptorManager } from "./InterceptorManager"; interface Interceptors { request: InterceptorManager; response: InterceptorManager; } export default class Axios { interceptors: Interceptors; constructor() { this.interceptors = { request: new InterceptorManager(), response: new InterceptorManager(), }; } } ``` 我们在`Axios`类中创建了`interceptors`属性,该属性是一个对象,包含`request`和`response`属性,它们分别是`InterceptorManager`类的实例,并且`InterceptorManager`类的泛型参数分别为`AxiosRequestConfig`和`AxiosResponse`。 OK,这样,我们就完成了拦截器的添加和移除功能。但是,光有添加和移除还不行,我们还需要在发送请求的时候遍历所有添加的拦截器,并依次执行。 ### 4.2.3 遍历拦截器 我们在发送请求的时候,需要先遍历请求拦截器,然后发送请求,最后遍历响应拦截器。那么,我们该如何遍历呢? 我们可以在`Axios`类的`request`方法中,先定义一个数组,该数组中保存了所有要执行的函数,包括请求拦截器、发送请求、响应拦截器。然后,我们依次执行该数组中的函数。如下: ```javascript const chain = [ { resolved: dispatchRequest, rejected: undefined, }, ]; this.interceptors.request.forEach((interceptor) => { chain.unshift(interceptor); }); this.interceptors.response.forEach((interceptor) => { chain.push(interceptor); }); ``` 我们先把发送请求的函数`dispatchRequest`放在`chain`数组的中间,然后遍历请求拦截器,将每个请求拦截器从`chain`数组的前面插入;接着遍历响应拦截器,将每个响应拦截器从`chain`数组的后面插入。这样,当我们依次执行`chain`数组中的函数时,就可以保证先执行请求拦截器,然后发送请求,最后执行响应拦截器。 但是,我们该如何执行`chain`数组中的函数呢?我们可以使用`Promise`来链式调用,如下: ```javascript let promise = Promise.resolve(config); while (chain.length) { const { resolved, rejected } = chain.shift()!; promise = promise.then(resolved, rejected); } return promise; ``` 我们先创建一个已经`resolve`的`promise`,然后循环`chain`数组,将数组中的每一项的`resolved`函数和`rejected`函数添加到`promise.then`的参数中,这样就相当于通过`Promise`的链式调用方式依次执行`chain`数组中的函数。 OK,思路已经理清,接下来,我们就按照该思路修改`Axios`类的`request`方法。 ### 4.2.4 修改 request 方法 我们修改`Axios`类的`request`方法,如下: ```typescript // src/core/Axios.ts request(url: any, config?: any): AxiosPromise { if (typeof url === 'string') { if (!config) { config = {}; } config.url = url; } else { config = url; } const chain: Array> = [ { resolved: dispatchRequest, rejected: undefined } ]; this.interceptors.request.forEach(interceptor => { chain.unshift(interceptor); }); this.interceptors.response.forEach(interceptor => { chain.push(interceptor); }); let promise = Promise.resolve(config); while (chain.length) { const { resolved, rejected } = chain.shift()!; promise = promise.then(resolved, rejected); } return promise; } ``` 在上面的代码中,我们先判断了第一个参数`url`是否为字符串类型,如果是的话,说明用户第一个参数传的是`url`,第二个参数是`config`,那么我们将`url`添加到`config`中;如果不是字符串类型,说明用户第一个参数直接传的就是`config`,那么我们将`url`赋值给`config`。 接着,我们定义了`chain`数组,该数组中默认存放发送请求的函数`dispatchRequest`。 然后,我们遍历所有的请求拦截器,将每个请求拦截器从`chain`数组的前面插入;接着遍历所有的响应拦截器,将每个响应拦截器从`chain`数组的后面插入。 最后,我们创建了一个已经`resolve`的`promise`,循环`chain`数组,将数组中的每一项的`resolved`函数和`rejected`函数添加到`promise.then`的参数中,通过`Promise`的链式调用方式依次执行`chain`数组中的函数。 另外,我们还需要给`InterceptorManager`类添加`forEach`方法,用来遍历`interceptors`数组中的拦截器。 ### 4.2.5 实现 forEach 方法 我们在`InterceptorManager`类中添加`forEach`方法,如下: ```typescript // src/core/InterceptorManager.ts export default class InterceptorManager { private interceptors: Array | null>; constructor() { this.interceptors = []; } use(resolved: ResolvedFn, rejected?: RejectedFn): number { this.interceptors.push({ resolved, rejected, }); return this.interceptors.length - 1; } forEach(fn: (interceptor: Interceptor) => void): void { this.interceptors.forEach((interceptor) => { if (interceptor !== null) { fn(interceptor); } }); } eject(id: number): void { if (this.interceptors[id]) { this.interceptors[id] = null; } } } ``` 在`forEach`方法中,我们遍历`interceptors`数组,并将数组中的每一项作为参数传给传入的`fn`函数,但是需要注意的是,如果数组中的某一项为`null`,那么我们就不遍历它。 OK,至此,我们就实现了拦截器的添加、移除和遍历功能。 # 5. 编写 demo 接下来,我们编写一个 `demo` 来测试下我们实现的拦截器功能是否正常。 ```javascript // examples/interceptor/app.ts import axios from "../../src/index"; axios.interceptors.request.use((config) => { config.headers.test += "1"; return config; }); axios.interceptors.request.use((config) => { config.headers.test += "2"; return config; }); axios.interceptors.request.use((config) => { config.headers.test += "3"; return config; }); axios.interceptors.response.use((res) => { res.data += "1"; return res; }); let interceptor = axios.interceptors.response.use((res) => { res.data += "2"; return res; }); axios.interceptors.response.use((res) => { res.data += "3"; return res; }); axios.interceptors.response.eject(interceptor); axios({ url: "/interceptor/get", method: "get", headers: { test: "", }, }).then((res) => { console.log(res.data); }); ``` 在该 `demo` 中,我们添加了 3 个请求拦截器,每个拦截器都在请求头 `headers` 的 `test` 字段上追加了一个数字,所以最终 `test` 字段的值是 `123`。 然后,我们又添加了 3 个响应拦截器,每个拦截器都在响应数据的后面追加了一个数字,所以最终响应数据后面会追加 `123`。但是,我们在添加完第二个响应拦截器的时候,保存了该拦截器的 `id`,然后又移除了该拦截器,所以最终响应数据后面追加的是 `13`。 我们在命令行中执行: ```bash # 同时开启客户端和服务端 npm run server | npm start ``` 然后我们打开浏览器,访问:`http://localhost:8000/`,然后点击 `interceptor`,通过`F12`的 `network` 可以看到我们发送的请求,并且请求头中 `test` 字段的值为 `123`,并且响应数据为 `{"message":"hello world"}13`,如下: ![](~@/axios/14/01.png) ![](~@/axios/14/02.png) OK,测试结果正常。 # 6. 遗留问题 我们虽然已经实现了拦截器的基本功能,但是仍然存在一些问题,例如:如果我们想在请求拦截器中做一些异步操作,那么我们的代码就无法正常工作。因为我们在遍历`chain`数组的时候,是同步执行的,而异步操作是无法通过同步方式获取到结果的。 不过,这并不影响我们理解拦截器的实现原理,而且`axios`官方也是这么实现的,所以我们暂时先不考虑异步的情况,后续如果有需要,我们再对其进行优化。 # 7. 总结 本篇文章中,我们介绍了拦截器的使用方式,分析了拦截器的设计思想,并实现了拦截器的添加、移除和遍历功能。 拦截器的实现思路如下: 1. 创建一个`InterceptorManager`类,用来管理拦截器,该类内部有一个`interceptors`数组,用来存储拦截器,并且有`use`和`eject`方法,分别用来添加和移除拦截器; 2. 在`Axios`类中创建`interceptors`属性,该属性包含`request`和`response`两个属性,它们分别是`InterceptorManager`类的实例; 3. 在`Axios`类的`request`方法中,先定义一个数组`chain`,默认存放发送请求的函数`dispatchRequest`; 4. 然后遍历所有的请求拦截器,将每个请求拦截器从`chain`数组的前面插入;接着遍历所有的响应拦截器,将每个响应拦截器从`chain`数组的后面插入; 5. 最后,通过`Promise`的链式调用方式依次执行`chain`数组中的函数; 至此,`axios`的拦截器我们就实现完毕了。

栏目列表