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

Axios配置合并原理与实现详解

<|begin▁of▁sentence|># 1. 前言 在之前的文章中,我们介绍了`axios`的拦截器,并实现了以下三个功能: 1. 正常发送请求,并添加拦截器; 2. 发送请求,移除拦截器; 3. 发送请求,添加临时拦截器; 接下来,我们趁热打铁,把`axios`的配置合并也实现了。我们知道,在`axios`中,发送请求的时候可以传入一个配置对象,而在创建`axios`实例的时候还可以传入一个配置对象,另外还还有一个全局的默认配置对象。那么,当这三个配置对象同时存在的时候,`axios`内部是如何将这三个配置对象合并到一起的呢?接下来,我们就来探究下。 # 2. 配置合并原理 我们先来看下`axios`中关于配置合并的使用示例: ## 2.1 使用示例 ### 2.1.1 全局默认配置 ```javascript axios.defaults.timeout = 0; axios.defaults.withCredentials = true; ``` ### 2.1.2 自定义实例默认配置 ```javascript const instance = axios.create({ timeout: 1000, withCredentials: false, }); ``` ### 2.1.3 请求中配置 ```javascript instance.get("/config", { timeout: 5000, }); ``` ### 2.1.4 合并优先级 请求配置 > 自定义实例配置 > 全局默认配置 ## 2.2 合并原理 从上面的示例中我们可以看到:`axios`中的配置对象分三种:全局默认配置、自定义实例默认配置和请求中配置。当这三者同时存在的时候,`axios`会按照优先级进行合并,优先级高的会覆盖优先级低的,最终生成一个最终的配置对象,然后这个最终的配置对象会被传入请求中。 那么,这个合并是如何实现的呢?我们先来看下源码中是怎么实现的。 ## 2.3 源码实现 在`axios`源码中,关于配置合并的函数是在`lib/core/mergeConfig.js`中,代码如下: ```javascript var utils = require("../utils"); /** * Config-specific merge-function which creates a new config-object * by merging two configuration objects together. * * @param {Object} config1 * @param {Object} config2 * @returns {Object} New object resulting from merging config2 to config1 */ module.exports = function mergeConfig(config1, config2) { // eslint-disable-next-line no-param-reassign config2 = config2 || {}; var config = {}; var valueFromConfig2Keys = ["url", "method", "data"]; var mergeDeepPropertiesKeys = ["headers", "auth", "proxy", "params"]; var defaultToConfig2Keys = [ "baseURL", "transformRequest", "transformResponse", "paramsSerializer", "timeout", "timeoutMessage", "withCredentials", "adapter", "responseType", "xsrfCookieName", "xsrfHeaderName", "onUploadProgress", "onDownloadProgress", "decompress", "maxContentLength", "maxBodyLength", "maxRedirects", "transport", "httpAgent", "httpsAgent", "cancelToken", "socketPath", "responseEncoding", "validateStatus", ]; utils.forEach(valueFromConfig2Keys, function valueFromConfig2(prop) { if (typeof config2[prop] !== "undefined") { config[prop] = config2[prop]; } }); utils.forEach(mergeDeepPropertiesKeys, function mergeDeepProperties(prop) { if (utils.isObject(config2[prop])) { config[prop] = utils.deepMerge(config1[prop], config2[prop]); } else if (typeof config2[prop] !== "undefined") { config[prop] = config2[prop]; } else if (utils.isObject(config1[prop])) { config[prop] = utils.deepMerge(config1[prop]); } else if (typeof config1[prop] !== "undefined') { config[prop] = config1[prop]; } }); utils.forEach(defaultToConfig2Keys, function defaultToConfig2(prop) { if (typeof config2[prop] !== "undefined") { config[prop] = config2[prop]; } else if (typeof config1[prop] !== "undefined") { config[prop] = config1[prop]; } }); var axiosKeys = valueFromConfig2Keys .concat(mergeDeepPropertiesKeys) .concat(defaultToConfig2Keys); var otherKeys = Object.keys(config1).concat(Object.keys(config2)).filter(function filterAxiosKeys(key) { return axiosKeys.indexOf(key) === -1; }); utils.forEach(otherKeys, function otherKeysDefault(prop) { if (typeof config2[prop] !== "undefined") { config[prop] = config2[prop]; } else if (typeof config1[prop] !== "undefined") { config[prop] = config1[prop]; } }); return config; }; ``` 从上面代码中我们可以看到,`mergeConfig`函数接收两个配置对象`config1`和`config2`,然后将这两个配置对象合并成一个新的配置对象`config`并返回。 在合并过程中,`axios`将配置属性分为了四类: 1. `valueFromConfig2Keys`:这些属性只从`config2`中获取,如果`config2`中有则使用`config2`中的,否则不使用; 2. `mergeDeepPropertiesKeys`:这些属性需要深合并,即如果`config1`和`config2`中都有该属性,那么需要将这两个属性深合并;如果只有`config2`中有,则使用`config2`中的;如果只有`config1`中有,则使用`config1`中的; 3. `defaultToConfig2Keys`:这些属性优先从`config2`中获取,如果`config2`中有则使用`config2`中的,否则从`config1`中获取; 4. 其他属性:这些属性优先从`config2`中获取,如果`config2`中有则使用`config2`中的,否则从`config1`中获取; 其中,深合并是通过`utils.deepMerge`函数实现的,这个函数我们之前已经实现过了,这里就不再赘述。 OK,以上就是`axios`中配置合并的原理,接下来,我们就按照源码中的思路来实现我们自己的配置合并。 # 3. 实现配置合并 ## 3.1 创建合并函数 我们首先在`src/core`目录下创建`mergeConfig.ts`文件,然后在该文件中实现`mergeConfig`函数。 ```typescript // src/core/mergeConfig.ts import { isPlainObject, deepMerge } from "../helpers/util"; const strats = Object.create(null); // 默认合并策略:优先取config2中的属性,如果config2中没有则取config1中的 function defaultStrat(val1: any, val2: any): any { return typeof val2 !== "undefined" ? val2 : val1; } // 只取config2中的合并策略,如果config2中没有则不管 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; } } // 以下属性使用只取config2中的合并策略 const stratKeysFromVal2 = ["url", "method", "data"]; stratKeysFromVal2.forEach((key) => { strats[key] = fromVal2Strat; }); // 以下属性使用深拷贝合并策略 const stratKeysDeepMerge = ["headers", "auth"]; stratKeysDeepMerge.forEach((key) => { strats[key] = deepMergeStrat; }); /** * 合并两个配置对象 * @param config1 配置1 * @param config2 配置2 */ 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; } ``` 在上面的代码中,我们首先定义了三种合并策略函数: 1. `defaultStrat`:默认合并策略,优先取`config2`中的属性,如果`config2`中没有则取`config1`中的; 2. `fromVal2Strat`:只取`config2`中的合并策略,如果`config2`中没有则不管; 3. `deepMergeStrat`:深拷贝合并策略,如果`config2`中的属性是普通对象,则与`config1`中的属性深合并;如果`config2`中的属性不是普通对象但有值,则取`config2`中的属性;如果`config2`中的属性没有值,但是`config1`中的属性是普通对象,则取`config1`中的属性深合并后的值;如果`config1`中的属性不是普通对象但有值,则取`config1`中的属性; 然后,我们定义了三种策略分别对应的属性: 1. `stratKeysFromVal2`:这些属性使用`fromVal2Strat`策略,即只取`config2`中的属性; 2. `stratKeysDeepMerge`:这些属性使用`deepMergeStrat`策略,即深拷贝合并; 3. 其他属性:使用默认合并策略; 最后,我们实现了`mergeConfig`函数,该函数接收两个配置对象`config1`和`config2`,然后遍历这两个配置对象的所有属性,根据属性名获取对应的合并策略函数,然后使用该策略函数合并属性值,最后返回合并后的配置对象。 ## 3.2 在 Axios 类中使用合并函数 配置合并函数已经实现好了,接下来我们就需要在`Axios`类中使用它。我们在发送请求的时候,需要将全局默认配置、实例默认配置和请求配置合并成一个最终的配置对象,然后将这个最终的配置对象传入请求中。 我们在`src/core/Axios.ts`中修改`request`方法: ```typescript // src/core/Axios.ts import mergeConfig from "./mergeConfig"; export default class Axios { // 新增默认配置 defaults: AxiosRequestConfig; constructor(initConfig: AxiosRequestConfig) { // 新增默认配置 this.defaults = initConfig; } request(url: any, config?: any): AxiosPromise { if (typeof url === "string") { if (!config) { config = {}; } config.url = url; } else { config = url; } // 合并配置 config = mergeConfig(this.defaults, config); // 设置请求方法,默认为get config.method = config.method.toLowerCase() || "get"; // 调用拦截器 const chain: PromiseChain[] = [ { 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`类新增了`defaults`属性,该属性存储了实例的默认配置。然后在`request`方法中,我们使用`mergeConfig`函数将实例默认配置和请求配置合并成一个最终的配置对象。 另外,我们还需要修改`src/axios.ts`中创建`axios`实例时的代码: ```typescript // src/axios.ts import { AxiosInstance } from "./types"; import Axios from "./core/Axios"; import { extend } from "./helpers/util"; import defaults from "./defaults"; function createInstance(): AxiosInstance { const context = new Axios(defaults); // 传入默认配置 const instance = Axios.prototype.request.bind(context); extend(instance, context); return instance as AxiosInstance; } const axios = createInstance(); export default axios; ``` 我们创建了一个`defaults`对象,该对象存储了全局的默认配置,然后在创建`axios`实例的时候,将这个`defaults`对象传入`Axios`类的构造函数中,这样`axios`实例的`defaults`属性就是全局的默认配置。 ## 3.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; ``` 在上面的代码中,我们定义了一个默认配置对象`defaults`,该对象包含了一些默认的配置,如默认请求方法为`get`,默认超时时间为`0`,以及一些默认的请求头。 另外,我们还根据请求方法是否需要数据,分别设置了不同的请求头。对于不需要数据的请求方法(如`delete`、`get`、`head`、`options`),我们设置了一个空对象;对于需要数据的请求方法(如`post`、`put`、`patch`),我们设置了默认的`Content-Type`为`application/x-www-form-urlencoded`。 ## 3.4 处理请求 headers 在之前,我们处理请求`headers`的时候,只是简单的将传入的`headers`直接设置到`xhr`对象上,但是现在我们有了默认配置,所以我们需要将默认配置中的`headers`和请求配置中的`headers`合并成一个最终的`headers`。 我们在`src/helpers/headers.ts`中新增一个`flattenHeaders`函数,用来将`headers`打平: ```typescript // src/helpers/headers.ts import { Method } from "../types"; /** * 将headers打平,将common和method对应的headers合并到同一级 * @param headers * @param method */ export function flattenHeaders(headers: any, method: Method): any { if (!headers) { return headers; } headers = deepMerge(headers.common, headers[method], headers); const methodsToDelete = ["get", "post", "head", "put", "patch", "delete", "common", "options"]; methodsToDelete.forEach((method) => { delete headers[method]; }); return headers; } ``` 这个函数的作用是将`headers`中的`common`和`method`对应的`headers`合并到同一级,然后删除`common`和`method`对应的`headers`。 然后,我们在`src/core/dispatchRequest.ts`中修改`transformConfig`函数,在发送请求之前,将`headers`打平: ```typescript // src/core/dispatchRequest.ts import { flattenHeaders } from "../helpers/headers"; function transformConfig(config: AxiosRequestConfig): void { const { url, params } = config; config.url = transformURL(url, params); config.data = transform(config.data, config.headers, config.transformRequest); // 打平headers config.headers = flattenHeaders(config.headers, config.method!); } ``` ## 3.5 修改类型定义 由于我们新增了一些类型,所以我们需要修改`src/types/index.ts`文件: ```typescript // src/types/index.ts export interface AxiosRequestConfig { url?: string; method?: Method; data?: any; params?: any; headers?: any; responseType?: XMLHttpRequestResponseType; timeout?: number; transformRequest?: AxiosTransformer | AxiosTransformer[]; transformResponse?: AxiosTransformer | AxiosTransformer[]; [propName: string]: any; } export interface AxiosTransformer { (data: any, headers?: any): any; } ``` 我们新增了`transformRequest`和`transformResponse`两个属性,这两个属性是函数或函数数组,用来转换请求数据和响应数据。 # 4. 编写 demo 接下来,我们编写一个`demo`来测试下我们的配置合并是否正常工作。 `examples/configMerge.` ``` < lang="en"> 配置合并示例 ``` 在上面的`demo`中,我们首先设置了全局默认配置:请求方法为`post`,超时时间为`1000ms`,公共请求头`token`为`abcdefg`。 然后,我们创建了一个`axios`实例,并设置了实例默认配置:超时时间为

栏目列表