当前位置 : 首页 > 保养数据

Axios拦截器实现:支持异步请求与响应处理

<|begin▁of▁sentence|># 1. 前言 在上一篇文章中,我们实现了`axios`的请求和响应配置化,即用户可以配置`url`、`method`、`params`、`data`、`headers`、`timeout`、`responseType`等等。另外,我们还实现了配置的默认值以及配置的合并策略。那么接下来,我们就来实现`axios`的核心功能:**拦截器**。 # 2. 需求分析 在发送请求之前,我们往往需要对请求的配置做一些处理,例如:在请求头中添加`token`字段;在请求发送之前,显示`loading`动画。而在请求响应之后,我们往往也需要对响应数据做一些处理,例如:将响应数据中的`data`字段解析出来;响应错误时,根据错误状态码给用户相应的错误提示。 对于以上这些需求,我们当然可以一一地在`then`和`catch`中去处理,但是这样会非常麻烦,并且如果多个请求都有相同的需求,那么就会出现大量重复的代码,这显然是我们不愿意看到的。所以,`axios`为我们提供了拦截器功能,它分为两种:请求拦截器和响应拦截器。 - **请求拦截器**:在发送请求之前可以做一些事情; - **响应拦截器**:在响应之后可以做一些事情; 并且,`axios`的拦截器还可以设置多个,多个拦截器会按照设置的顺序依次执行。 # 3. 拦截器管理 我们先来看下`axios`官方提供的拦截器使用示例: ```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); } ); ``` 从示例中我们可以看到: - `axios`对象上有一个`interceptors`对象属性,该属性又有`request`和`response`2 个属性,它们都有一个`use`方法,`use`方法支持两个参数,第一个参数类似`Promise`的`resolve`函数,第二个参数类似`Promise`的`reject`函数。我们可以在`resolve`函数和`reject`函数中执行同步代码或者是异步代码逻辑。 - 并且,我们可以在`resolve`函数中拿到配置对象`config`,并可以对其修改然后返回;在响应拦截器的`resolve`函数中还可以拿到响应对象`response`,并可以对其修改然后返回。 # 4. 整体设计 通过上面的分析,我们可以设计出拦截器的整体结构: 1. 我们创建一个`Axios`类,在类里面创建一个`interceptors`对象属性,该属性又有`request`和`response`2 个属性,它们都是`InterceptorManager`类的实例; 2. `InterceptorManager`类是用来管理拦截器的,并且这个类上需要实现`use`方法来添加拦截器,另外,添加后的拦截器我们还要能够将其 eject( eject 就是删除拦截器的意思),所以`InterceptorManager`类还需要实现`eject`方法来删除拦截器; 3. 另外,`InterceptorManager`类还需要一个`forEach`方法,来遍历所有注册的拦截器; OK,思路已经清晰,接下来我们就来实现拦截器。 # 5. 拦截器实现 ## 5.1 创建 InterceptorManager 类 我们先来创建`InterceptorManager`类,根据上面的设计,该类需要实现三个实例方法:`use`、`eject`、`forEach`。 我们在`src`目录下创建`interceptorManager.ts`文件: ```typescript // src/interceptorManager.ts export interface ResolvedFn { (val: T): T | Promise; } export interface RejectedFn { (error: any): any; } export 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; } 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; } } } ``` 代码说明: - 我们定义了一个`InterceptorManager`泛型类,并且定义了三个实例方法:`use`、`eject`、`forEach`; - `use`方法接收两个参数,`resolved`函数和`rejected`函数,调用`use`的时候,我们把这两个参数组成一个对象`interceptor`,并将其`push`到`interceptors`中,并且返回它在`interceptors`中的索引`id`,以便之后删除; - `eject`方法接收一个参数,即拦截器的`id`,删除拦截器时我们将其置为`null`,并且我们不会改变`interceptors`数组的长度,而是通过`id`来查找对应的拦截器,将其置为`null`,这样就不会影响之后注册的拦截器在`interceptors`中的索引`id`; - `forEach`方法接收一个函数作为参数,并且遍历`interceptors`,然后将每一个不为`null`的`interceptor`作为参数传给传入的函数; ## 5.2 修改 Axios 类 创建好`InterceptorManager`类后,我们需要在`Axios`类中创建`interceptors`对象属性,该对象属性包含`request`和`response`两个属性,它们都是`InterceptorManager`类的实例。 我们先在`src`目录下的`axios.ts`文件中引入`InterceptorManager`类: ```typescript // src/axios.ts import InterceptorManager from "./interceptorManager"; ``` 然后,给`Axios`类添加`interceptors`属性: ```typescript // src/axios.ts interface Interceptors { request: InterceptorManager; response: InterceptorManager; } export default class Axios { defaults: AxiosRequestConfig; interceptors: Interceptors; constructor(initConfig: AxiosRequestConfig) { this.defaults = initConfig; this.interceptors = { request: new InterceptorManager(), response: new InterceptorManager(), }; } // ... } ``` OK,`interceptors`对象属性添加好了,接下来我们就要实现拦截器的执行逻辑了。 ## 5.3 实现拦截器链式调用 我们知道,拦截器的执行顺序是:**先添加的先执行,后添加的后执行**,并且,请求拦截器是在发送请求之前执行,而响应拦截器是在响应之后执行。那么,我们如何把请求拦截器、发送请求、响应拦截器构成一条链式来调用呢? 我们可以把请求拦截器、发送请求、响应拦截器都放在一个数组链中,结构如下: ```javascript [ 请求拦截器2的resolve函数, 请求拦截器2的reject函数, 请求拦截器1的resolve函数, 请求拦截器1的reject函数, 发送请求, undefined, 响应拦截器1的resolve函数, 响应拦截器1的reject函数, 响应拦截器2的resolve函数, 响应拦截器2的reject函数, ] ``` 然后,我们再让这个数组链中的函数按照注册的顺序依次执行,这样就实现了拦截器的链式调用。 OK,我们接下来就按照这个思路来实现。 首先,我们在`Axios`类的`request`方法中,先构建这样一个数组链: ```typescript // src/axios.ts request(url: any, config?: any): AxiosPromise { if (typeof url === 'string') { if (!config) { config = {}; } config.url = url; } else { config = url; } config = mergeConfig(this.defaults, config); // 构建一个数组链 const chain: Array | any> = [ { resolved: dispatchRequest, rejected: undefined, }, ]; // 请求拦截器从后往前压入数组链的前面 this.interceptors.request.forEach((interceptor) => { chain.unshift(interceptor); }); // 响应拦截器从前往后压入数组链的后面 this.interceptors.response.forEach((interceptor) => { chain.push(interceptor); }); // ... } ``` 然后,我们让这个数组链中的函数依次执行,我们可以使用`Promise`来实现: ```typescript // src/axios.ts request(url: any, config?: any): AxiosPromise { if (typeof url === 'string') { if (!config) { config = {}; } config.url = url; } else { config = url; } config = mergeConfig(this.defaults, config); const chain: Array | any> = [ { resolved: dispatchRequest, rejected: undefined, }, ]; this.interceptors.request.forEach((interceptor) => { chain.unshift(interceptor); }); this.interceptors.response.forEach((interceptor) => { chain.push(interceptor); }); // 创建一个已经resolved的Promise,resolve的值是config let promise = Promise.resolve(config); // 循环数组链,依次执行数组链中的函数 while (chain.length) { const { resolved, rejected } = chain.shift()!; promise = promise.then(resolved, rejected); } return promise; } ``` 代码说明: - 我们先创建一个已经`resolved`的`Promise`,`resolve`的值是`config`; - 然后,我们循环数组链,每次从数组链中取出一个对象,该对象包含`resolved`和`rejected`两个函数; - 然后,我们用`promise.then`方法依次执行`resolved`和`rejected`函数,并且将返回的`Promise`赋值给`promise`,这样就可以实现链式调用; - 最后,返回`promise`; OK,拦截器的链式调用就实现了。 ## 5.4 修改 dispatchRequest 函数 我们之前实现的`dispatchRequest`函数,它返回的是一个`AxiosPromise`,即`Promise`类型的`Promise`对象。但是我们在拦截器链中调用`dispatchRequest`函数时,我们希望它返回的是`AxiosResponse`类型的响应对象,而不是`Promise`对象,因为我们在拦截器链中是通过`promise.then`来链式调用的,如果返回的是`Promise`对象,那么就会打破这个链式调用。 所以,我们需要修改`dispatchRequest`函数,让它返回一个`Promise`对象,并且在`Promise`的`resolve`函数中返回`AxiosResponse`类型的响应对象。 我们在`src`目录下的`dispatchRequest.ts`文件中修改`dispatchRequest`函数: ```typescript // src/dispatchRequest.ts export default function dispatchRequest(config: AxiosRequestConfig): AxiosPromise { return new Promise((resolve, reject) => { const { url, method = "get", data, headers, responseType, timeout } = config; const request = new XMLHttpRequest(); if (responseType) { request.responseType = responseType; } if (timeout) { request.timeout = timeout; } request.open(method.toUpperCase(), url, true); request.onreadystatechange = function handleLoad() { if (request.readyState !== 4) return; if (request.status === 0) return; const responseHeaders = parseHeaders(request.getAllResponseHeaders()); const responseData = responseType && responseType !== "text" ? request.response : request.responseText; const response: AxiosResponse = { data: responseData, status: request.status, statusText: request.statusText, headers: responseHeaders, config, request, }; resolve(response); }; request.onerror = function handleError() { reject(new Error("Network Error")); }; request.ontimeout = function handleTimeout() { reject(new Error(`Timeout of ${timeout} ms exceeded`)); }; Object.keys(headers).forEach((name) => { if (data === null && name.toLowerCase() === "content-type") { delete headers[name]; } else { request.setRequestHeader(name, headers[name]); } }); request.send(data); }); } ``` 我们将`dispatchRequest`函数改造成返回一个`Promise`对象,并且在`Promise`的`resolve`函数中返回`AxiosResponse`类型的响应对象。 # 6. 编写 demo OK,拦截器我们已经实现好了,接下来我们编写 `demo` 来测试下。 我们在 `examples` 目录下创建 `interceptors.` 文件: ``` < lang="en"> interceptors demo ``` 然后我们在 `server/index.js` 中添加一个新的路由: ```javascript // server/index.js router.post("/api/interceptors", function(req, res) { res.json({ data: "interceptors" }); }); ``` 接着,在命令行中执行: ```bash # 同时开启客户端和服务端 npm run server | npm start ``` 接着,在浏览器中打开 `http://localhost:8000/examples/interceptors.`,然后点击 `interceptors`,通过`F12`的 `network` 面板我们可以看到请求已经正常发出,并且响应也已经正常返回,并且打开控制台,我们可以看到拦截器的执行顺序: ![](~@/axios/08/01.png) 从控制台我们可以看到,请求拦截器是按照添加的顺序执行的,而响应拦截器是按照添加的顺序逆序执行的。并且,我们也可以看到,请求拦截器中给`headers`添加的字段已经生效,响应拦截器中给`data`添加的字段也已经生效。 # 7. 修改 types 定义 由于我们给`Axios`类添加了`interceptors`属性,所以我们需要在`types/index.ts`中定义`interceptors`的类型。 ```typescript // types/index.ts 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; } ``` 另外,我们还需要把`InterceptorManager`类的定义也导出: ```typescript // types/index.ts export interface InterceptorManager { use(resolved: ResolvedFn, rejected?: RejectedFn): number; eject(id: number): void; forEach(fn: (interceptor: Interceptor) => void): void; } export interface ResolvedFn { (val: T): T | Promise; } export interface RejectedFn { (error: any): any; } export interface Interceptor { resolved: ResolvedFn; rejected?: RejectedFn; } ``` OK,`types`定义也修改好了。 # 8. 遗留问题 我们虽然已经实现了拦截器,但是还存在一个问题:如果我们想在拦截器中异步执行一些逻辑,那么我们的拦截器还能正常工作吗? 例如,我们在请求拦截器中添加一个异步逻辑: ```javascript axios.interceptors.request.use(async (config) => { config.headers.test += "1"; await new Promise((resolve) => setTimeout(resolve, 1000)); return config; }); ``` 按照我们的实现,请求拦截器会按照添加的顺序执行,但是我们在请求拦截器中添加了异步逻辑,那么我们的拦截器链就会等待这个异步逻辑执行完毕后再执行下一个拦截器,这样就会导致请求发送的延迟。 但是,我们目前的实现是不支持异步的,因为我们在拦截器链中是通过`promise.then`来链式调用的,而`promise.then`中的回调函数是同步执行的,所以如果我们在拦截器中添加了异步逻辑,那么我们的拦截器链就会被打断。 那么,我们该如何支持异步拦截器呢?其实也很简单,我们只需要在拦截器链中返回一个`Promise`对象,然后在`Promise`的`resolve`函数中执行下一个拦截器即可。 由于时间

栏目列表