当前位置 : 首页 > 车辆数据

Axios拦截器实现:请求响应全流程控制

<|begin▁of▁sentence|># 1. 前言 在上一篇文章中,我们实现了`axios`的请求和响应配置化,即用户可以配置`url`、`method`、`params`、`data`、`headers`、`timeout`、`responseType`等等。另外,我们还实现了配置的默认值以及配置的合并策略。那么接下来,我们就来实现`axios`的核心功能:**拦截器**。 # 2. 拦截器介绍 `axios`拦截器的作用是我们在发送请求之前和响应之后可以做一些操作,例如:在发送请求之前,在请求头中添加`token`字段;在响应之后,我们根据`http`状态码来判断用户的登录状态是否过期等等。 `axios`拦截器分为两种:请求拦截器和响应拦截器。 - 请求拦截器:在发送请求之前做一些操作。 - 响应拦截器:在响应之后做一些操作。 并且,`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); } ); ``` 另外,我们还可以随时删除拦截器,例如: ```javascript const myInterceptor = axios.interceptors.request.use(function() { /*...*/ }); axios.interceptors.request.eject(myInterceptor); ``` # 3. 实现思路 通过上面的介绍,我们可以总结出如下几点: - 拦截器分为请求拦截器和响应拦截器两种; - 可以为同一个`axios`实例添加多个拦截器,拦截器的执行顺序按照添加顺序执行; - 拦截器也支持成功和失败回调,并且支持返回`Promise`; - 我们还可以随时删除拦截器; 那么,接下来,我们就来实现拦截器。 ## 3.1 接口定义 根据需求,我们需要给`axios`对象添加一个`interceptors`属性,该属性又有两个属性:`request`和`response`,它们都是拦截器管理类`InterceptorManager`的实例。 ```typescript export interface Axios { // 新增interceptors属性 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; } ``` ## 3.2 拦截器管理类-InterceptorManager 根据需求,我们需要实现一个拦截器管理类,这个类需要提供三个方法:`use`、`eject`、`forEach`。 - `use`方法用来添加拦截器,它接收两个参数:成功回调和失败回调,并且会返回一个`id`用于删除该拦截器; - `eject`方法用来删除拦截器,它接收一个参数,即`use`方法返回的`id`; - `forEach`方法用来遍历所有拦截器,它接收一个参数,该参数是一个函数,该函数会接收每一个拦截器作为参数; 并且,该类还需要内部维护一个数组,用来存储添加的拦截器。 OK,思路理清之后,我们就来实现这个拦截器管理类。 我们在`src`目录下创建`core`目录,用来存放核心代码,然后在`core`目录下创建`InterceptorManager.ts`文件。 ```typescript // src/core/InterceptorManager.ts 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> = []; use(onFulfilled?: OnFulfilled, onRejected?: OnRejected): number { this.interceptors.push({ onFulfilled, onRejected, }); return this.interceptors.length - 1; } eject(id: number) { if (this.interceptors[id]) { this.interceptors[id] = null; } } forEach(fn: (interceptor: Interceptor) => void): void { this.interceptors.forEach(interceptor => { if (interceptor !== null) { fn(interceptor); } }); } } ``` 代码说明: - 我们定义了一个`InterceptorManager`泛型类,并且定义了一个公共属性`interceptors`,它是一个数组,数组的每一项是一个`Interceptor`类型的对象或`null`,初始值为空数组。 - `use`方法接收两个参数:`onFulfilled`和`onRejected`,这两个参数都是可选的。在函数内部,我们把这两个参数组合成一个对象`Interceptor`并`push`到`interceptors`中,并且返回它在数组中的索引`id`。 - `eject`方法接收一个参数`id`,即`use`方法返回的`id`,在函数内部,我们根据`id`找到`interceptors`中对应的拦截器,然后将其置为`null`。这里我们并没有使用`splice`等方法将该拦截器从数组中删除,而是置为`null`,主要是因为当我们遍历拦截器的时候,遍历的是`interceptors`,而`interceptors`中可能有些项是`null`,所以我们遍历的时候需要把`null`的项过滤掉,这样做是为了避免当我们删除拦截器后,`interceptors`中每一项的索引发生变化,从而导致`eject`方法删除错误的拦截器。 - `forEach`方法接收一个参数`fn`,该参数是一个函数,该函数会接收每一个拦截器作为参数。在函数内部,我们遍历`interceptors`,并且将不为`null`的拦截器作为参数传给`fn`。 ## 3.3 给 Axios 类添加 interceptors 属性 定义好拦截器管理类之后,我们就需要给`Axios`类添加`interceptors`属性,并且初始化`request`和`response`。 我们在`src`目录下的`core`目录中创建`Axios.ts`文件。 ```typescript // src/core/Axios.ts import { AxiosRequestConfig, AxiosPromise, AxiosResponse } from "../types"; import dispatchRequest from "./dispatchRequest"; import InterceptorManager from "./InterceptorManager"; // 定义拦截器接口 interface Interceptors { request: InterceptorManager; response: InterceptorManager; } export default class Axios { // 公共属性 public interceptors: Interceptors; constructor() { // 初始化interceptors this.interceptors = { request: new InterceptorManager(), response: new InterceptorManager(), }; } request(config: AxiosRequestConfig): AxiosPromise { return dispatchRequest(config); } get(url: string, config?: AxiosRequestConfig): AxiosPromise { return this.request( Object.assign(config || {}, { method: "get", url, }) ); } delete(url: string, config?: AxiosRequestConfig): AxiosPromise { return this.request( Object.assign(config || {}, { method: "delete", url, }) ); } head(url: string, config?: AxiosRequestConfig): AxiosPromise { return this.request( Object.assign(config || {}, { method: "head", url, }) ); } options(url: string, config?: AxiosRequestConfig): AxiosPromise { return this.request( Object.assign(config || {}, { method: "options", url, }) ); } post(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise { return this.request( Object.assign(config || {}, { method: "post", url, data, }) ); } put(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise { return this.request( Object.assign(config || {}, { method: "put", url, data, }) ); } patch(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise { return this.request( Object.assign(config || {}, { method: "patch", url, data, }) ); } } ``` OK,`interceptors`属性添加好了,接下来我们就要修改`request`方法,在发送请求之前先执行请求拦截器,在接收到响应之后先执行响应拦截器。 ## 3.4 实现拦截器链 在发送请求之前,我们先要执行所有的请求拦截器,然后再发送请求;在接收到响应之后,我们先要执行所有的响应拦截器,然后再将响应数据返回。所以,我们需要在`request`方法中实现拦截器链。 思路如下: 1. 首先,我们创建一个数组`chain`,该数组保存的默认项是发送请求的函数`dispatchRequest`和`undefined`。这里为什么要有`undefined`呢?因为我们拦截器的`use`方法可以只传入一个成功回调,而失败回调是可选的,并且我们在组合`Promise`链的时候,数组的每两项分别对应成功回调和失败回调。所以这里我们先用`undefined`占个位,是为了保证`chain`中都是成对的。 2. 然后,我们遍历所有的请求拦截器,将每个请求拦截器的成功回调和失败回调`unshift`到`chain`数组的前面。 3. 接着,我们遍历所有的响应拦截器,将每个响应拦截器的成功回调和失败回调`push`到`chain`数组的后面。 4. 此时,`chain`数组应该是这样的: ```javascript [ 请求拦截器成功回调1, 请求拦截器失败回调1, 请求拦截器成功回调2, 请求拦截器失败回调2, ..., dispatchRequest, undefined, 响应拦截器成功回调1, 响应拦截器失败回调1, 响应拦截器成功回调2, 响应拦截器失败回调2, ... ] ``` 5. 然后,我们初始化一个`Promise`,`Promise`的`resolve`参数是 config,然后遍历`chain`,将`chain`的每两项(成功回调和失败回调)作为`then`的参数,从而形成一个`Promise`调用链。 OK,我们按照这个思路来修改`request`方法: ```typescript // src/core/Axios.ts request(config: AxiosRequestConfig): AxiosPromise { // 初始化一个数组,存放拦截器和发送请求的函数 const chain: Array | InterceptorRejected | undefined> = [ { resolved: dispatchRequest, rejected: undefined, }, ]; // 遍历请求拦截器,插入到chain数组的前面 this.interceptors.request.forEach(interceptor => { if (interceptor) { chain.unshift(interceptor); } }); // 遍历响应拦截器,插入到chain数组的后面 this.interceptors.response.forEach(interceptor => { if (interceptor) { chain.push(interceptor); } }); // 初始化一个Promise,resolve参数是config let promise = Promise.resolve(config); // 循环chain数组,将每两项(成功回调,失败回调)作为then的参数,形成Promise调用链 while (chain.length) { const { onFulfilled, onRejected } = chain.shift()!; promise = promise.then(onFulfilled, onRejected); } return promise; } ``` 但是,我们这样写会报错,因为`chain`数组中的每一项可能是`Interceptor`类型,也可能是`{resolved: dispatchRequest, rejected: undefined}`类型,而这两种类型是不兼容的,所以我们需要统一`chain`数组中的每一项的类型。 我们观察`chain`数组中的每一项,无论是`Interceptor`类型还是`{resolved: dispatchRequest, rejected: undefined}`类型,它们都有`onFulfilled`和`onRejected`属性,所以我们可以定义一个接口,让这两种类型都实现这个接口。 我们在`src/core/InterceptorManager.ts`中定义这个接口: ```typescript // src/core/InterceptorManager.ts export interface Interceptor { onFulfilled?: OnFulfilled; onRejected?: OnRejected; } // 新增 export interface InterceptorResolved { onFulfilled?: OnFulfilled; onRejected?: OnRejected; } ``` 然后,修改`Axios.ts`中的`request`方法: ```typescript // src/core/Axios.ts request(config: AxiosRequestConfig): AxiosPromise { // 初始化一个数组,存放拦截器和发送请求的函数 const chain: Array> = [ { onFulfilled: dispatchRequest, onRejected: undefined, }, ]; // 遍历请求拦截器,插入到chain数组的前面 this.interceptors.request.forEach(interceptor => { if (interceptor) { chain.unshift(interceptor); } }); // 遍历响应拦截器,插入到chain数组的后面 this.interceptors.response.forEach(interceptor => { if (interceptor) { chain.push(interceptor); } }); // 初始化一个Promise,resolve参数是config let promise = Promise.resolve(config); // 循环chain数组,将每两项(成功回调,失败回调)作为then的参数,形成Promise调用链 while (chain.length) { const { onFulfilled, onRejected } = chain.shift()!; promise = promise.then(onFulfilled, onRejected); } return promise; } ``` 这样,我们就实现了拦截器链。 ## 3.5 修改 dispatchRequest 函数 由于我们拦截器链中第一个`Promise`的`resolve`参数是`config`,所以请求拦截器的成功回调的参数是`config`,而请求拦截器的成功回调可以返回一个新的`config`,也可以返回一个`Promise`,所以我们需要修改`dispatchRequest`函数,让它接收一个`config`参数,并且返回一个`Promise`。 我们在`src/core`目录下创建`dispatchRequest.ts`文件。 ```typescript // src/core/dispatchRequest.ts import { AxiosRequestConfig, AxiosPromise, AxiosResponse } from "../types"; import xhr from "./xhr"; import { buildURL } from "../helpers/url"; import { transformRequest, transformResponse } from "../helpers/data"; import { processHeaders } from "../helpers/headers"; export default function dispatchRequest(config: AxiosRequestConfig): AxiosPromise { processConfig(config); return xhr(config).then(res => { return transformResponseData(res); }); } function processConfig(config: AxiosRequestConfig): void { config.url = transformURL(config); config.headers = transformHeaders(config); config.data = transformRequestData(config); } function transformURL(config: AxiosRequestConfig): string { const { url, params } = config; return buildURL(url!, params); } function transformRequestData(config: AxiosRequestConfig): any { return transformRequest(config.data); } function transformHeaders(config: AxiosRequestConfig): any { const { headers = {}, data } = config; return processHeaders(headers, data); } function transformResponseData(res: AxiosResponse): AxiosResponse { res.data = transformResponse(res.data); return res; } ``` 这里,我们把之前写在`xhr.ts`中的`processConfig`和`transformResponseData`函数移到了`dispatchRequest.ts`中,并且做了一些修改。 ## 3.6 修改 xhr 函数 由于`dispatchRequest`函数已经处理了`config`,所以`xhr`函数中就不需要再处理`config`了,我们修改`xhr`函数: ```typescript // src/core/xhr.ts import { AxiosRequestConfig, AxiosPromise, AxiosResponse } from "../types"; import { parseHeaders } from "../helpers/headers"; export default function xhr(config: AxiosRequestConfig): AxiosPromise { return new Promise((resolve, reject) => { const { data = null, url, method = "get", 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 !== "text" ? request.response : request.responseText; const response: AxiosResponse = { data: responseData, status: request.status, statusText: request.statusText, headers: responseHeaders, config, request, }; handleResponse(response); }; request.onerror = function handleError() { reject(new Error("Network Error")); }; request.ontimeout = function handleTimeout() { reject(new Error(`Timeout of ${timeout} ms exceeded`)); }; function handleResponse(response: AxiosResponse)

栏目列表