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

Axios拦截器实现全解析:从原理到实战应用

<|begin▁of▁sentence|># 1. 前言 在之前的文章中,我们介绍了`axios`的拦截器,并用示意图展示了拦截器在请求从发出到响应返回过程中的工作流程。我们知道,拦截器分为请求拦截器和响应拦截器两种。请求拦截器的作用是拦截请求,在请求发送之前做一些事情;响应拦截器的作用是拦截响应,在响应返回到`then/catch`之前做一些事情。从之前的示意图中我们可以看到,拦截器的执行过程是一个链式调用的方式,并且每个拦截器都可以设置两个回调函数,一个表示成功,一个表示失败。并且我们还可以随时在拦截器数组中添加新的拦截器,另外,我们也支持删除某个拦截器。 那么,接下来,我们就来自己实现一下`axios`的拦截器。 # 2. 需求分析 我们实现拦截器大致需要实现以下功能: 1. 拦截器分为请求拦截器和响应拦截器两种; 2. 每种拦截器都可以设置多个,并且每个拦截器都可以设置两个回调函数,一个用于成功,一个用于失败; 3. 每种拦截器都支持通过`use`方法添加; 4. 每种拦截器都支持通过`eject`方法删除; 5. 拦截器的执行过程是一个链式调用的方式; # 3. 实现思路 ## 3.1 定义拦截器管理类 首先,我们可以为拦截器定义一个管理类,该类里面可以提供`use`、`eject`等方法供外界使用,并且该类还需要维护一个数组,用来存储用户通过`use`方法添加的拦截器。 ## 3.2 定义拦截器接口 其次,我们需要定义拦截器的接口,因为每个拦截器都可以设置两个回调函数,一个表示成功,一个表示失败,并且每个拦截器都还要有一个`id`,以便后续删除。 ## 3.3 把拦截器挂载到axios实例上 然后,我们还需要把定义好的请求拦截器管理类和响应拦截器管理类挂载到`axios`实例上,并且起名为`interceptors`,它的结构如下: ```javascript axios.interceptors = { request: new InterceptorManager(), response: new InterceptorManager() } ``` ## 3.4 修改request函数 最后,我们还需要修改`request`函数,在`request`函数内部来组织拦截器与真正请求的调度关系。 OK,以上就是我们实现拦截器的大致思路,接下来,我们就按照这个思路来一步步实现它。 # 4. 拦截器管理类实现 ## 4.1 定义拦截器接口 我们创建一个`InterceptorManager`拦截器管理类,在该类中,我们定义一个数组`interceptors`用来存储用户添加的拦截器,并且每个拦截器我们规定它是一个对象,其中`fulfilled`表示成功时的回调,`rejected`表示失败时的回调,并且每个拦截器还要有一个`id`,以便后续删除。如下: ```typescript export interface ResolvedFn { (val: T): T | Promise } export interface RejectedFn { (error: any): any } export interface Interceptor { fulfilled: ResolvedFn rejected?: RejectedFn id: number } ``` ## 4.2 实现InterceptorManager类 我们为`InterceptorManager`类定义如下方法: - `use`:添加拦截器,返回拦截器`id`,以便删除; - `eject`:删除拦截器; - `forEach`:遍历拦截器; 具体实现如下: ```typescript export default class InterceptorManager { // 拦截器数组 private interceptors: Array | null> constructor() { this.interceptors = [] } // 添加拦截器 use(fulfilled: ResolvedFn, rejected?: RejectedFn): number { this.interceptors.push({ fulfilled, rejected, id: this.interceptors.length }) 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 } } } ``` 代码说明: - 在`use`方法中,每添加一个拦截器,我们就为它创建一个对象,并分配一个`id`,然后将该对象`push`到`interceptors`中,并且返回这个`id`; - 在`eject`方法中,我们通过`id`来查找对应的拦截器,找到后将其置为`null`,这里我们并没有使用`splice`等数组方法来删除,是为了避免删除后数组索引发生变化,导致`forEach`遍历时错乱; - 在`forEach`方法中,我们遍历所有拦截器,并执行传入的回调,并且会跳过为`null`的拦截器; OK,拦截器管理类就实现好了。 # 5. 把拦截器挂载到axios实例上 拦截器管理类实现好之后,我们需要把它挂载到`axios`实例上,并且起名为`interceptors`,它的结构如下: ```javascript axios.interceptors = { request: new InterceptorManager(), response: new InterceptorManager() } ``` 首先,我们在`src/core/Axios.ts`中定义`interceptors`属性,如下: ```typescript import { InterceptorManager } from "./InterceptorManager"; // 定义接口 export interface Interceptors { request: InterceptorManager response: InterceptorManager } // 在Axios类中添加interceptors属性 export default class Axios { defaults: AxiosRequestConfig interceptors: Interceptors constructor(initConfig: AxiosRequestConfig) { this.defaults = initConfig this.interceptors = { request: new InterceptorManager(), response: new InterceptorManager() } } // ... } ``` 然后,我们还需要在`src/core/InterceptorManager.ts`中把`InterceptorManager`类导出,如下: ```typescript export * from "./InterceptorManager"; ``` # 6. 修改request函数 拦截器挂载好之后,接下来我们就需要在`request`函数内部来组织拦截器与真正请求的调度关系了。 在之前的文章中,我们介绍拦截器工作流程的时候,画过一张图,如下: ![](~@/axios/05/01.png) 从图中我们可以看到,请求拦截器是在请求之前执行,响应拦截器是在响应之后执行,并且整个过程是一个链式调用的方式。那么,我们该如何在`request`函数内部实现这个链式调用呢? 其实,我们可以把整个请求的调度看做是一个链条,这个链条上的每个节点都是一个Promise对象,而每个Promise对象都拥有两个方法:`then`和`catch`。那么,我们就可以把整个链条拆分成多个Promise对象,然后通过`then`方法把它们串联起来。 具体思路如下: 1. 首先,我们创建一个数组`chain`,该数组用来存放链条上的每个节点,每个节点都是一个Promise对象; 2. 然后,我们把请求拦截器中的每个拦截器都放到`chain`数组的前面,并且每个拦截器都有两个回调函数,一个表示成功,一个表示失败; 3. 接着,我们把真正的请求`dispatchRequest`放到`chain`数组中; 4. 最后,我们把响应拦截器中的每个拦截器都放到`chain`数组的后面,并且每个拦截器都有两个回调函数,一个表示成功,一个表示失败; 5. 然后,我们通过`Promise.resolve(config)`初始化一个Promise对象,然后通过`while`循环依次取出`chain`数组中的每个节点,然后通过`then`方法把它们串联起来; 6. 最后,返回这个Promise对象; OK,思路清晰之后,接下来我们就按照这个思路来修改`request`函数,如下: ```typescript 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 => { if (interceptor !== null) { chain.unshift(interceptor) } }) // 响应拦截器:往数组后面压入 this.interceptors.response.forEach(interceptor => { if (interceptor !== null) { chain.push(interceptor) } }) // 初始化一个promise,resolve参数是config let promise = Promise.resolve(config) // 循环chain数组,取出每个拦截器,然后通过then串联起来 while (chain.length) { const { resolved, rejected } = chain.shift()! promise = promise.then(resolved, rejected) } return promise } ``` 代码说明: - 首先,我们创建一个数组`chain`,并把真正的请求`dispatchRequest`函数包装成一个对象放到`chain`中; - 然后,我们遍历请求拦截器,把每个请求拦截器都通过`unshift`方法放到`chain`数组的前面,注意:这里是从后往前遍历,所以先添加的请求拦截器会后执行; - 接着,我们遍历响应拦截器,把每个响应拦截器都通过`push`方法放到`chain`数组的后面,注意:这里是从前往后遍历,所以先添加的响应拦截器会先执行; - 然后,我们初始化一个`Promise`对象,`resolve`的参数是`config`; - 接着,我们通过`while`循环依次取出`chain`数组中的每个节点,然后通过`then`方法把它们串联起来; - 最后,返回这个`Promise`对象; # 7. 编写demo OK,拦截器实现好了之后,我们就编写一个`demo`来测试下效果如何。 我们在`examples`目录下创建`interceptor.`文件,代码如下: ``` < lang="en"> interceptor demo ``` 然后,我们在`server/interceptor.js`中添加接口路由,如下: ```javascript router.get("/api/interceptor/get", function(req, res) { res.end("hello, interceptor"); }); ``` 接着,我们打开命令行,执行: ```bash # 同时开启客户端和服务端 npm run server | npm start ``` 接着,我们打开浏览器,访问:`http://localhost:8000/`,然后点击 `interceptor`,通过`F12`的 `Network` 部分我们可以看到请求已正常发出,并且打开浏览器控制台,我们可以看到打印出的日志如下: ![](~@/axios/05/02.png) 从图中我们可以看到,请求拦截器按照添加的顺序从后往前执行,响应拦截器按照添加的顺序从前往后执行,并且整个过程是一个链式调用的方式。 我们再来测试下删除拦截器,在`demo`中,我们添加完拦截器之后,再删除某个拦截器,如下: ```javascript // 添加请求拦截器1 const requestInterceptorId1 = axios.interceptors.request.use(function(config) { console.log("request interceptor1 resolved"); return config; }); // 添加请求拦截器2 const requestInterceptorId2 = axios.interceptors.request.use(function(config) { console.log("request interceptor2 resolved"); return config; }); // 删除请求拦截器2 axios.interceptors.request.eject(requestInterceptorId2); // 添加响应拦截器1 const responseInterceptorId1 = axios.interceptors.response.use(function(response) { console.log("response interceptor1 resolved"); return response; }); // 添加响应拦截器2 const responseInterceptorId2 = axios.interceptors.response.use(function(response) { console.log("response interceptor2 resolved"); return response; }); // 删除响应拦截器1 axios.interceptors.response.eject(responseInterceptorId1); ``` 然后,我们再次刷新页面,打开浏览器控制台,我们可以看到打印出的日志如下: ![](~@/axios/05/03.png) 从图中我们可以看到,请求拦截器2和响应拦截器1都被删除了,并没有执行。 OK,以上就是拦截器的实现。 # 8. 遗留问题 虽然我们已经实现了拦截器的基本功能,但是还是有一些细节问题需要处理,例如: 1. 在`request`函数中,我们通过`Promise.resolve(config)`初始化了一个`Promise`对象,然后通过`then`方法串联起所有拦截器,但是如果在某个拦截器中抛出异常,那么这个异常会被后面的拦截器的`rejected`回调捕获,但是如果最后一个拦截器也抛出异常,那么这个异常就会变成`Uncaught (in promise)`,我们应该如何处理? 2. 在拦截器中,我们可以修改`config`或`response`,但是我们在`request`函数中并没有对修改后的`config`或`response`做任何处理,我们应该如何处理? 这些问题,我们会在后面的文章中一一解决。 # 9. 总结 本篇文章我们实现了`axios`的拦截器功能。我们首先分析了拦截器的需求,然后介绍了实现思路,接着我们按照思路一步步实现了拦截器管理类,并把拦截器挂载到`axios`实例上,最后我们修改了`request`函数,在`request`函数内部组织拦截器与真正请求的调度关系。实现完成后,我们还编写了`demo`测试了拦截器的效果。 最后,我们还遗留了两个问题,这些问题我们会在后面的文章中解决。

栏目列表