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

# Axios 配置化实现:请求与响应拦截器优化

<|begin▁of▁sentence|># 1. 前言 在上一篇文章中,我们实现了`axios`的请求和响应拦截器功能,并且为它们编写了对应的测试用例。接下来,我们就来实现`axios`的配置化,所谓配置化,就是指`axios`可以根据我们传入的配置项来发送不同配置的请求。那么,在`axios`中都有哪些配置项呢?我们可以先来看一下官方`axios`的一些常用的配置项。 # 2. 官方 axios 常用配置项 以下是官方`axios`常用的配置项列表: - `url`:请求地址,必需项 - `method`:请求方法,默认为`get` - `baseURL`:请求的基础路径,当`url`为相对路径时,会自动添加在`url`前面,除非`url`为绝对路径 - `transformRequest`:允许在请求发送前对请求数据进行修改,只适用于`put`、`post`、`patch`方法,数组中的最后一个函数必须返回一个字符串或`ArrayBuffer`或`Stream` - `transformResponse`:允许在响应数据传递给`then/catch`前对响应数据进行修改 - `headers`:自定义请求头信息 - `params`:`URL`参数,必须是纯对象或者`URLSearchParams`对象 - `paramsSerializer`:负责`params`序列化的函数 - `data`:作为请求体发送的数据,只适用于`put`、`post`、`patch`方法 - `timeout`:请求超时时间,单位为毫秒,默认为`0`,表示永不超时 - `withCredentials`:表示跨域请求时是否需要使用凭证,默认为`false` - `adapter`:允许自定义处理请求,这会使测试更加简单,默认为`xhr`和`http` - `auth`:表示应该使用`HTTP`基础验证,并提供凭据 - `responseType`:表示服务器响应的数据类型,默认为`json`,可选项为`arraybuffer`、`blob`、`document`、`json`、`text`、`stream` - `xsrfCookieName`:用作`xsrf token`值的`cookie`名称,默认为`XSRF-TOKEN` - `xsrfHeaderName`: 带有`xsrf token`值的`http`请求头名称,默认为`X-XSRF-TOKEN` - `onUploadProgress`:允许为上传处理进度事件 - `onDownloadProgress`:允许为下载处理进度事件 - `maxContentLength`:定义允许的响应内容的最大尺寸 - `validateStatus`:定义对于给定的`HTTP`响应状态码是`resolve`还是`reject`。如果返回`true`,`resolve`;否则`reject` - `maxRedirects`:定义在`node.js`中重定向的最大数量,默认为`5` - `httpAgent`和`httpsAgent`:分别在`node.js`中用于定义在执行`http`和`https`时使用的自定义代理 - `proxy`:定义代理服务器的主机名称和端口 - `cancelToken`:指定用于取消请求的`cancel token` 以上是官方`axios`的一些常用配置项,当然,我们不可能在初始版本就实现这么多配置项,我们会在后续的迭代更新中逐渐添加。在初始版本中,我们先实现一些最常用的配置项,如:`url`、`method`、`headers`、`params`、`data`、`timeout`、`withCredentials`、`responseType`等。 OK,接下来我们就来实现这些配置项。 # 3. 接口定义 由于我们是用`TypeScript`来开发`axios`,所以我们需要先定义配置项的接口类型。 我们在`src`目录下创建`types`目录,用来存放所有的类型定义文件。然后在`types`目录下创建`index.ts`文件,用来统一导出所有的类型定义。 ## 3.1 定义请求方法类型 在发送请求时,我们需要指定请求方法,请求方法我们定义为字符串字面量类型,如下: ```typescript // types/index.ts export type Method = | "get" | "GET" | "delete" | "DELETE" | "head" | "HEAD" | "options" | "OPTIONS" | "post" | "POST" | "put" | "PUT" | "patch" | "PATCH"; ``` ## 3.2 定义请求配置接口 接下来,我们定义请求配置接口`AxiosRequestConfig`,它描述了发送请求时我们可以配置的选项,如下: ```typescript // types/index.ts export interface AxiosRequestConfig { url?: string; method?: Method; data?: any; params?: any; headers?: any; responseType?: XMLHttpRequestResponseType; timeout?: number; } ``` 其中,每个属性的含义如下: - `url`:请求地址,可选 - `method`:请求方法,可选,默认为`get` - `data`:请求体数据,可选,只有当请求方法为`post`、`put`、`patch`等时才有意义 - `params`:`URL`参数,可选 - `headers`:请求头信息,可选 - `responseType`:响应数据类型,可选,默认为`json` - `timeout`:超时时间,可选,单位为毫秒,默认为`0`,表示永不超时 ## 3.3 定义响应数据接口 当服务器返回响应数据时,`axios` 需要返回一个响应数据对象,该对象包括:服务端返回的数据、HTTP 状态码、状态消息、响应头、请求配置对象、请求 XMLHttpRequest 对象实例。我们定义响应数据接口`AxiosResponse`如下: ```typescript // types/index.ts export interface AxiosResponse { data: any; status: number; statusText: string; headers: any; config: AxiosRequestConfig; request: any; } ``` ## 3.4 定义 axios 函数接口 我们之前已经实现了`axios`方法,并且它既支持传入一个参数,也支持传入两个参数,如下: ```javascript axios({ url: "/api/add", method: "post", data: { a: 1, b: 2, }, }); axios("/api/add", { method: "post", data: { a: 1, b: 2, }, }); ``` 所以,我们需要为`axios`方法定义多种重载形式,如下: ```typescript // types/index.ts export interface Axios { 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; } export interface AxiosInstance extends Axios { (config: AxiosRequestConfig): AxiosPromise; (url: string, config?: AxiosRequestConfig): AxiosPromise; } ``` 我们定义了`Axios`接口,它描述了`axios`类中的公共方法,这些方法如下: - `request` - `get` - `delete` - `head` - `options` - `post` - `put` - `patch` 然后又定义了`AxiosInstance`接口继承`Axios`,并且它本身是一个函数,支持两种调用方式。 ## 3.5 定义响应 promise 对象接口 由于`axios`函数返回的是一个`promise`对象,我们可以定义一个`AxiosPromise`接口,它继承于`Promise`这个泛型接口: ```typescript // types/index.ts export interface AxiosPromise extends Promise {} ``` 这样的话,当`axios`返回的是`AxiosPromise`类型,那么`resolve`函数中的参数就是一个`AxiosResponse`类型。 ## 3.6 定义错误信息接口 另外,我们还需要定义`axios`函数抛出的异常类型,如下: ```typescript // types/index.ts export interface AxiosError extends Error { config: AxiosRequestConfig; code?: string; request?: any; response?: AxiosResponse; isAxiosError: boolean; } ``` OK,到目前为止,我们已经把之前写的代码中用到的类型都定义好了,接下来,我们就需要根据这些类型来修改之前的代码了。 # 4. 创建 Axios 类 ## 4.1 创建 Axios 类 由于`axios`是一个函数,并且它是一个混合对象,本身又是一个类,既可以直接调用,又可以使用`new`关键字创建实例,并且实例上面还有`get`、`post`等方法。所以,我们考虑把`axios`写成类的形式,然后把这个类的原型属性和实例属性混合到`axios`函数上。 我们在`src`目录下创建`core`目录,用来存放核心代码。然后在`core`目录下创建`Axios.ts`文件。 ```typescript // core/Axios.ts import { AxiosRequestConfig, AxiosPromise, AxiosResponse, Method, AxiosError, } from "../types"; import { parseHeaders } from "../helpers/headers"; import { createError } from "../helpers/error"; export default class Axios { request(config: AxiosRequestConfig): AxiosPromise { return this.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, }) ); } dispatchRequest(config: AxiosRequestConfig): AxiosPromise { return new Promise((resolve, reject) => { const { url, method = "get", data = null, 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); }; function handleResponse(response: AxiosResponse) { if (response.status >= 200 && response.status < 300) { resolve(response); } else { reject( createError( `Request failed with status code ${response.status}`, config, null, request, response ) ); } } request.onerror = function handleError() { reject(createError("Network Error", config, null, request)); }; request.ontimeout = function handleTimeout() { reject( createError(`Timeout of ${timeout} ms exceeded`, config, "ECONNABORTED", request) ); }; Object.keys(headers).forEach((name) => { if (data === null && name.toLowerCase() === "content-type") { delete headers[name]; } else { request.setRequestHeader(name, headers[name]); } }); request.send(data); }); } } ``` 我们创建了`Axios`类,在类中定义了对应的方法,并且把之前写的`axios`函数的功能移植到了`dispatchRequest`方法中。 ## 4.2 创建 axios 函数 创建好`Axios`类后,我们需要创建一个`axios`函数,这个函数的功能就是直接调用`Axios`类的`request`方法,并且把这个函数的原型指向`Axios`类的原型,这样`axios`函数就拥有了`Axios`类中的所有方法。另外,我们还要把`Axios`类的实例属性混合到`axios`函数上。 我们在`src`目录下创建`axios.ts`文件。 ```typescript // axios.ts import { AxiosInstance } from "./types"; import Axios from "./core/Axios"; import { extend } from "./helpers/util"; function createInstance(): AxiosInstance { const context = new Axios(); const instance = Axios.prototype.request.bind(context); extend(instance, context); return instance as AxiosInstance; } const axios = createInstance(); export default axios; ``` 我们创建了`createInstance`函数,在这个函数内部,我们首先实例化了`Axios`类,得到一个`context`,然后创建`instance`指向`Axios`类的`request`方法,并绑定了上下文`context`;接着通过`extend`方法把`context`中的实例方法和属性拷贝到`instance`上,这样就实现了`axios`即拥有`Axios`类中的所有方法又拥有`Axios`类实例中的所有属性;最后返回`instance`,由于`instance`的类型是`AxiosInstance`,所以我们可以将其断言为`AxiosInstance`类型。 另外,我们还需要实现`extend`方法,我们在`src/helpers`目录下创建`util.ts`文件。 ```typescript // helpers/util.ts export function extend(to: T, from: U): T & U { for (const key in from) { (to as T & U)[key] = from[key] as any; } return to as T & U; } ``` `extend`方法的实现非常简单,就是遍历`from`上的属性,将其添加到`to`上,这里涉及到一些交叉类型的知识,大家可以自行学习。 # 5. 修改拦截器管理器 由于我们把`axios`从函数改为类的方式,所以拦截器管理器我们也需要做相应的修改。 ## 5.1 定义拦截器管理器接口 首先,我们在`types/index.ts`中定义拦截器管理器接口。 ```typescript // types/index.ts export interface AxiosInterceptorManager { use(resolved: ResolvedFn, rejected?: RejectedFn): number; eject(id: number): void; } export interface ResolvedFn { (val: T): T | Promise; } export interface RejectedFn { (error: any): any; } ``` 然后,我们在`AxiosRequestConfig`配置中添加`interceptors`属性。 ```typescript // types/index.ts export interface AxiosRequestConfig { // 新增 interceptors?: Interceptors; } // 新增 export interface Interceptors { request: AxiosInterceptorManager; response: AxiosInterceptorManager; } ``` ## 5.2 实现拦截器管理器类 然后,我们修改`src/core/InterceptorManager.ts`文件,用类的形式实现。 ```typescript // 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; } 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; } } } ``` ## 5.3 在 Axios 类中添加拦截器 接下来,我们在`Axios`类中添加拦截器。 ```typescript // core/Axios.ts import { AxiosRequestConfig, AxiosPromise, AxiosResponse, Method, AxiosError, Interceptors, } from "../types"; import InterceptorManager from "./InterceptorManager"; import { parseHeaders } from "../helpers/headers"; import { createError } from "../helpers/error"; export default class Axios { interceptors: Interceptors; constructor() { this.interceptors = { request: new InterceptorManager(), response: new InterceptorManager(), }; } request(config: AxiosRequestConfig): AxiosPromise { // 省略其他代码 } // 省略其他方法 } ``` 我们在`Axios`类的构造函数中初始化了`interceptors`对象,它有两个属性,`request`和`response`,都是拦截器管理器的实例。 ## 5.4 实现拦截器链式调用 接下来,我们要在发送请求前先

栏目列表