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`实例,并设置了实例默认配置:超时时间为
最新文章
- 汇编语言程序设计实验:数据操作与调试技巧
- 汽车散热器冷却系统优化设计提升性能
- 4S店查勘员必备:安全气囊检测与钣金变形评估指南
- 汽车面漆修复全攻略:从喷枪使用到清漆保养
- 充电桩与电动汽车的未来发展
- 汽车胎压、刹车片与雨刮器保养全攻略
- 车险理赔全流程指南:从定损到维修的完整解析
- 从“电子乐器”到“数字静音”:NVH电驱如何重塑电动出行体验
- 铜合金在汽车制造中广泛应用提升性能
- 车辆严重损毁无法启动
- Axios拦截器原理:链式调用与请求响应流程解析
- 汽车的未来发展趋势
- 智能网联与新能源:V2X、固态电池和MaaS重塑未来出行
- 奔驰驾驶体验豪华舒适
- 动力电池革新与V2X技术引领电动汽车产业新变革
- 汽车防盗报警器实时监控安全防护
- 热传导影响汽车发动机散热效率
- 轮胎花纹全解析:如何选择与保养提升行车安全
- 新能源与智能网联技术引领未来出行革命
- 激光雷达与车联网技术如何重塑未来汽车安全格局
