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

Axios进阶:配置化与拦截器功能实现

<|begin▁of▁sentence|># 1. 前言 在上一篇文章中,我们实现了`axios`的基础功能,但到目前为止,我们都只是实现了一个很简单的`axios`,一个库如果只能发送请求那是远远不够的,我们还需要为其添加很多功能,比如:配置化、拦截器、取消请求等等。那么从这篇文章开始,我们就来为我们的`axios`添加这些功能。 # 2. 配置化 ## 2.1 需求分析 到目前为止,我们的`axios`都是一个单例,一旦我们修改了`axios`的默认配置,那么接下来的所有请求都会受到影响。但是在实际项目中,我们可能希望有些请求使用默认配置,而有些请求使用自定义配置。那么,我们该如何实现呢? 我们期望`axios`对象上有一个`defaults`属性,该属性保存着默认配置,并且我们还可以在发送请求时传入配置来覆盖默认配置,并且还可以创建新的`axios`实例,在新的`axios`实例上可以传入新的配置来覆盖默认配置。 ## 2.2 代码实现 ### 2.2.1 创建 Axios 类 首先,我们先来创建一个`Axios`类,在该类的构造函数上添加一个`defaults`属性用来保存默认配置,并且把之前`axios`函数上的逻辑挪到该类中。 ```typescript // src/core/Axios.ts import { AxiosRequestConfig, AxiosResponse } from "../types"; import dispatchRequest from "./dispatchRequest"; export default class Axios { defaults: AxiosRequestConfig; constructor(initConfig: AxiosRequestConfig) { this.defaults = initConfig; } 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, }) ); } } ``` ### 2.2.2 修改 axios 对象 然后,我们修改`axios`对象,让它变成`Axios`类的实例,并且在`axios`对象上添加一个`create`函数,该函数可以创建一个新的`Axios`实例。 ```typescript // src/axios.ts import { AxiosInstance, AxiosRequestConfig } from "./types"; import Axios from "./core/Axios"; import { extend } from "./helpers/util"; import defaults from "./defaults"; function createInstance(config: AxiosRequestConfig): AxiosInstance { const context = new Axios(config); const instance = Axios.prototype.request.bind(context); extend(instance, context); return instance as AxiosInstance; } const axios = createInstance(defaults); axios.create = function create(config: AxiosRequestConfig) { return createInstance(extend(defaults, config)); }; export default axios; ``` OK,我们创建了`Axios`类,并且把`axios`变成了`Axios`类的实例。另外,我们还在`axios`上添加了`create`函数,该函数可以创建一个新的`Axios`实例。 ### 2.2.3 默认配置 另外,我们还需要定义一些默认配置,我们在`src`目录下创建`defaults.ts`文件,用来存放默认配置。 ```typescript // src/defaults.ts import { AxiosRequestConfig } from "./types"; const defaults: AxiosRequestConfig = { method: "get", timeout: 0, headers: { common: { Accept: "application/json, text/plain, */*", }, }, }; const methodsNoData = ["delete", "get", "head", "options"]; methodsNoData.forEach((method) => { defaults.headers[method] = {}; }); const methodsWithData = ["post", "put", "patch"]; methodsWithData.forEach((method) => { defaults.headers[method] = { "Content-Type": "application/x-www-form-urlencoded", }; }); export default defaults; ``` 我们定义了默认配置,包括默认的请求方法、超时时间、请求头等。并且我们还为没有`data`的请求方法和有`data`的请求方法分别设置了不同的请求头。 ### 2.2.4 合并配置 我们在发送请求的时候,传入的配置可以和默认配置进行合并,合并的逻辑我们在`src/core/mergeConfig.ts`中实现。 ```typescript // src/core/mergeConfig.ts import { AxiosRequestConfig } from "../types"; import { deepMerge, isPlainObject } from "../helpers/util"; const strats = Object.create(null); function defaultStrat(val1: any, val2: any): any { return typeof val2 !== "undefined" ? val2 : val1; } function fromVal2Strat(val1: any, val2: any): any { if (typeof val2 !== "undefined") { return val2; } } function deepMergeStrat(val1: any, val2: any): any { if (isPlainObject(val2)) { return deepMerge(val1, val2); } else if (typeof val2 !== "undefined") { return val2; } else if (isPlainObject(val1)) { return deepMerge(val1); } else if (typeof val1 !== "undefined") { return val1; } } const stratKeysFromVal2 = ["url", "params", "data"]; stratKeysFromVal2.forEach((key) => { strats[key] = fromVal2Strat; }); const stratKeysDeepMerge = ["headers"]; stratKeysDeepMerge.forEach((key) => { strats[key] = deepMergeStrat; }); export default function mergeConfig( config1: AxiosRequestConfig, config2?: AxiosRequestConfig ): AxiosRequestConfig { if (!config2) { config2 = {}; } const config = Object.create(null); for (let key in config2) { mergeField(key); } for (let key in config1) { if (!config2[key]) { mergeField(key); } } function mergeField(key: string): void { const strat = strats[key] || defaultStrat; config[key] = strat(config1[key], config2![key]); } return config; } ``` 合并配置的逻辑比较复杂,我们定义了三种合并策略: - 默认策略:如果`val2`存在则返回`val2`,否则返回`val1`; - 只取`val2`策略:如果`val2`存在则返回`val2`,否则返回`undefined`; - 深度合并策略:如果`val2`是普通对象则深度合并,否则返回`val2`,如果`val2`不存在则返回`val1`; 然后我们根据不同的属性使用不同的合并策略,比如`url`、`params`、`data`等属性使用只取`val2`策略,`headers`使用深度合并策略,其他属性使用默认策略。 ### 2.2.5 修改 dispatchRequest 最后,我们需要修改`dispatchRequest`函数,在发送请求之前,将默认配置和传入的配置进行合并。 ```typescript // src/core/dispatchRequest.ts import { AxiosPromise, AxiosRequestConfig, AxiosResponse } from "../types"; import xhr from "./xhr"; import { buildURL } from "../helpers/url"; import { transformRequest, transformResponse } from "../helpers/data"; import { processHeaders } from "../helpers/headers"; import mergeConfig from "./mergeConfig"; export default function dispatchRequest( config: AxiosRequestConfig ): AxiosPromise { // 合并配置 config = mergeConfig(this.defaults, config); // 转换URL config.url = transformURL(config); // 转换请求头 config.headers = transformHeaders(config); // 转换请求数据 config.data = transformRequestData(config); // 转换响应数据 config.data = transformResponseData(config); return xhr(config).then( (res) => { return transformResponseData(res); }, (e) => { if (e && e.response) { e.response = transformResponseData(e.response); } return Promise.reject(e); } ); } function transformURL(config: AxiosRequestConfig): string { const { url, params } = config; return buildURL(url, params); } function transformRequestData(config: AxiosRequestConfig): any { return transformRequest(config.data); } function transformResponseData(res: AxiosResponse): AxiosResponse { res.data = transformResponse(res.data); return res; } function transformHeaders(config: AxiosRequestConfig): any { const { headers = {}, data } = config; return processHeaders(headers, data); } ``` ## 2.3 demo 编写 在 `examples` 目录下创建 `config`目录,在 `config`目录下创建 `index.`: ``` < lang="en"> config example ``` 接着再创建 `app.ts` 作为入口文件: ```typescript import axios from "../../src/axios"; axios({ url: "/api/config", method: "post", data: { a: 1, }, }).then((res) => { console.log(res.data); }); ``` 接着在 `server/server.js` 添加新的接口路由: ```javascript // 配置接口 router.post("/api/config", function(req, res) { res.json(req.body); }); ``` 最后在根目录下的`index.`中加上启动该`demo`的入口: ```
  • config
  • ``` OK,我们在命令行中执行: ```bash # 同时开启客户端和服务端 npm run server | npm start ``` 接着我们打开 `chrome` 浏览器,访问 即可访问我们的 `demo` 了,我们点击 `config`,通过`F12`的 `network` 部分我们可以看到发送的请求,并且请求头中已经带上了我们默认配置的 `Content-Type` 字段。 ![](~@/axios/05/01.png) 我们还可以通过`axios.create`创建一个新的`axios`实例,并且传入新的配置,然后使用新的`axios`实例发送请求。 ```typescript import axios from "../../src/axios"; const instance = axios.create({ timeout: 1000, headers: { "X-CUSTOM-HEADER": "custom header", }, }); instance({ url: "/api/config", method: "post", data: { a: 1, }, }).then((res) => { console.log(res.data); }); ``` 我们再次访问 点击 `config`,通过`F12`的 `network` 部分我们可以看到发送的请求,并且请求头中已经带上了我们自定义的 `X-CUSTOM-HEADER` 字段,并且超时时间也变成了 1 秒。 ![](~@/axios/05/02.png) # 3. 拦截器 ## 3.1 需求分析 拦截器是`axios`非常重要的一个特性,它可以在请求发送之前和响应返回之后做一些事情。比如:在请求发送之前,我们可以为请求添加一些公共的参数,或者在响应返回之后,我们可以对响应数据进行一些处理。 我们期望`axios`对象上有一个`interceptors`属性,该属性上有`request`和`response`两个属性,它们都是`InterceptorManager`的实例,我们可以通过`use`方法添加拦截器,并且`use`方法返回一个`id`,可以通过`eject`方法取消拦截器。 ## 3.2 代码实现 ### 3.2.1 定义拦截器管理器 首先,我们先来定义拦截器管理器`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; } 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`方法添加拦截器,`forEach`方法遍历拦截器,`eject`方法取消拦截器。 ### 3.2.2 修改 Axios 类 然后,我们需要修改`Axios`类,在`Axios`类中添加`interceptors`属性,并且修改`request`方法,在发送请求之前先执行请求拦截器,在接收到响应之后先执行响应拦截器。 ```typescript // src/core/Axios.ts import { AxiosPromise, AxiosRequestConfig, AxiosResponse, ResolvedFn, RejectedFn, } from "../types"; import dispatchRequest from "./dispatchRequest"; import InterceptorManager from "./InterceptorManager"; 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(), }; } request(config: AxiosRequestConfig): AxiosPromise { const chain: Array<{ resolved: ResolvedFn | ((config: AxiosRequestConfig) => AxiosPromise); rejected?: RejectedFn; }> = [ { 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; } // ... 其他方法 } ``` 我们在`Axios`类中添加了`interceptors`属性,该属性上有`request`和`response`两个属性,它们都是`InterceptorManager`的实例。 然后我们修改了`request`方法,在发送请求之前,我们先遍历请求拦截器,将请求拦截器添加到`chain`数组的前面,然后遍历响应拦截器,将响应拦截器添加到`chain`数组的后面。然后我们通过`Promise`链式调用的方式,依次执行`chain`数组中的函数。 ### 3.2.3 修改 axios 对象 最后,我们需要修改`axios`对象,将`interceptors`属性暴露出去。 ```typescript // src/axios.ts import { AxiosInstance, AxiosRequestConfig } from "./types"; import Axios from "./core/Axios"; import { extend } from "./helpers/util"; import defaults from "./defaults"; function createInstance(config: AxiosRequestConfig): AxiosInstance { const context = new Axios(config); const instance = Axios.prototype.request.bind(context); extend(instance, context); return instance as AxiosInstance; } const axios = createInstance(defaults); axios.create = function create(config: AxiosRequestConfig) { return createInstance(extend(defaults, config)); }; export default axios; ``` ## 3.3 demo 编写 在 `examples` 目录下创建 `interceptor`目录,在 `interceptor`目录下创建 `index.`: ``` < lang="en"> interceptor example ``` 接着再创建 `app.ts` 作为入口文件: ```typescript import axios from "../../src/axios"; 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; });

    栏目列表