# Axios 默认配置与合并策略详解
<|begin▁of▁sentence|># 1. 前言
在上一篇文章中,我们实现了`axios`的基础功能,但到目前为止,我们都只是实现了一个很简单的`axios`,一个最最基础的`axios`,我们还有很多功能没有实现,比如:`axios`的默认配置、拦截器、`xsrf`防御、上传下载进度监控等等。从本篇文章开始,我们就来把这些功能一一实现。
我们先从`axios`的默认配置开始,用过`axios`的同学都知道,`axios`提供了一些默认配置,用户只需要配置一些必需的参数,其他参数可以不必配置,`axios`会使用默认值。那么,`axios`的默认配置都有哪些呢?我们可以在`axios`官网的配置章节看到,如下:
- [请求配置](http://www.axios-js.com/zh-cn/docs/#请求配置)
- [响应配置](http://www.axios-js.com/zh-cn/docs/#响应结构)
从上面可以看到,`axios`提供的默认配置有很多,我们不可能在这一篇文章中全部实现,我们挑选一些常用的配置来实现,其他的配置实现方式都大同小异,同学们可以自行实现。
我们挑选以下几个配置作为我们第一阶段要实现的默认配置:
- `url`:请求的接口地址
- `method`:请求方法
- `baseURL`:请求的基准地址
- `transformRequest`:请求数据转换函数
- `transformResponse`:响应数据转换函数
- `headers`:请求头
- `params`:`url`参数
- `paramsSerializer`:`url`参数序列化函数
- `data`:请求体数据
- `timeout`:超时时间
- `withCredentials`:跨域时是否携带`cookie`
- `xsrfCookieName`:`xsrf`的`cookie`名称
- `xsrfHeaderName`:`xsrf`的`header`名称
- `onUploadProgress`:上传进度监控函数
- `onDownloadProgress`:下载进度监控函数
- `auth`:`HTTP`基础认证
- `validateStatus`:自定义合法状态码函数
# 2. 默认配置
## 2.1 定义默认配置
我们先在`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, */*",
},
},
xsrfCookieName: "XSRF-TOKEN",
xsrfHeaderName: "X-XSRF-TOKEN",
transformRequest: [
function (data: any, headers: any): any {
return data;
},
],
transformResponse: [
function (data: any): any {
return data;
},
],
validateStatus(status: number): boolean {
return status >= 200 && status < 300;
},
};
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` 常量,它包含了一些默认的配置值,并且这些配置是我们在未来会陆续实现的。其中 `headers` 做了特殊处理,请求头中 `common` 表示任何请求方法都有的请求头配置,然后我们还分别对不需要携带数据的请求方法如 `get`、`delete`、`head`、`options` 和需要携带数据的请求方法如 `post`、`put`、`patch` 分别进行了请求头配置,并且需要携带数据的请求方法我们默认设置了它的 `Content-Type` 属性为 `application/x-www-form-urlencoded`。
## 2.2 配置合并策略
定义了默认配置后,我们在发送请求的时候需要把用户传入的配置与这些默认配置做合并,合并的规则是:优先使用用户传入的配置,如果用户没有传入,则使用默认配置。
接下来,我们就来实现配置的合并策略。
我们在`src`目录下创建`core`目录,并在`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 {
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;
}
```
合并规则如下:
- 默认合并策略:如果`config2`中有配置,则使用`config2`中的配置,否则使用`config1`中的配置;
- 只从`config2`合并策略:只取`config2`中的配置,合并到最终配置中,`config1`中对应的配置忽略;
- 深度合并策略:对于`config1`和`config2`都是对象的配置,如`headers`,我们需要对它们进行深度合并,并且对于`config2`中的配置,如果它是普通对象,则与`config1`中对应的配置进行深度合并,否则直接取`config2`中的配置;如果`config2`中没有配置,但是`config1`中有配置,并且是普通对象,则对`config1`中的配置进行深度合并,否则直接取`config1`中的配置;
我们通过 `stratKeysFromVal2` 指定了哪些属性使用只从 `config2` 合并的策略,即 `url`、`params`、`data`,这些属性都是和请求强相关的,所以我们只从用户传入的配置 `config2` 中获取,默认配置 `config1` 中获取的无效。
我们还通过 `stratKeysDeepMerge` 指定了哪些属性使用深度合并策略,这里只对 `headers` 属性使用了深度合并策略,因为 `headers` 属性比较复杂,它包含了 `common`、`post`、`get` 等,我们需要对它们进行深度合并。
对于其它属性,我们使用默认合并策略。
## 2.3 修改 Axios 类
配置合并策略实现好之后,我们需要修改`Axios`类,在发送请求之前,先把用户传入的配置与默认配置做合并。
```typescript
// src/core/Axios.ts
import { AxiosRequestConfig, AxiosPromise, AxiosResponse } from "../types";
import { parseHeaders } from "../helpers/headers";
import { createError } from "../helpers/error";
import { isURLSameOrigin } from "../helpers/url";
import cookie from "../helpers/cookie";
import { isFormData } from "../helpers/util";
import defaults from "../defaults";
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();
// 待实现中间件
// 待实现请求/响应拦截器
// 发起请求
return dispatchRequest(config);
}
get(url: string, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithoutData("get", url, config);
}
delete(url: string, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithoutData("delete", url, config);
}
head(url: string, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithoutData("head", url, config);
}
options(url: string, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithoutData("options", url, config);
}
post(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithData("post", url, data, config);
}
put(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithData("put", url, data, config);
}
patch(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithData("patch", url, data, config);
}
_requestMethodWithoutData(
method: string,
url: string,
config?: AxiosRequestConfig
) {
return this.request(
Object.assign(config || {}, {
method,
url,
})
);
}
_requestMethodWithData(
method: string,
url: string,
data?: any,
config?: AxiosRequestConfig
) {
return this.request(
Object.assign(config || {}, {
method,
url,
data,
})
);
}
}
function dispatchRequest(config: AxiosRequestConfig): AxiosPromise {
throwIfCancellationRequested(config);
processConfig(config);
return xhr(config).then(
(res) => {
return transformResponseData(res);
},
(e) => {
if (e && e.response) {
e.response = transformResponseData(e.response);
}
return Promise.reject(e);
}
);
}
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, paramsSerializer, baseURL } = config;
if (baseURL && !isAbsoluteURL(url!)) {
return combineURL(baseURL, url!);
}
return buildURL(url!, params, paramsSerializer);
}
function transformRequestData(config: AxiosRequestConfig): any {
return transformRequest(config.data, config.headers, config.transformRequest);
}
function transformHeaders(config: AxiosRequestConfig): any {
const { headers = {}, data, method } = config;
return processHeaders(headers, data, method!.toLowerCase());
}
function transformResponseData(res: AxiosResponse): AxiosResponse {
res.data = transformResponse(
res.data,
res.headers,
res.config.transformResponse
);
return res;
}
function transformRequest(
data: any,
headers: any,
fns?: any
): any {
if (!fns) {
return data;
}
if (!Array.isArray(fns)) {
fns = [fns];
}
fns.forEach((fn: any) => {
data = fn(data, headers);
});
return data;
}
function transformResponse(
data: any,
headers: any,
fns?: any
): any {
if (!fns) {
return data;
}
if (!Array.isArray(fns)) {
fns = [fns];
}
fns.forEach((fn: any) => {
data = fn(data, headers);
});
return data;
}
function xhr(config: AxiosRequestConfig): AxiosPromise {
return new Promise((resolve, reject) => {
const {
url,
method = "get",
data = null,
headers,
responseType,
timeout,
cancelToken,
withCredentials,
xsrfCookieName,
xsrfHeaderName,
onDownloadProgress,
onUploadProgress,
auth,
validateStatus,
} = config;
const request = new XMLHttpRequest();
request.open(method.toUpperCase(), url!, true);
configureRequest();
addEvents();
processHeaders();
processCancel();
request.send(data);
function configureRequest(): void {
if (responseType) {
request.responseType = responseType;
}
if (timeout) {
request.timeout = timeout;
}
if (withCredentials) {
request.withCredentials = withCredentials;
}
}
function addEvents(): void {
request.onreadystatechange = function handleLoad() {
if (request.readyState !== 4) {
return;
}
if (request.status === 0) {
return;
}
const responseHeaders = parseHeaders(request.getAllResponseHeaders());
const responseData =
responseType && 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(createError("Network Error", config, null, request));
};
request.ontimeout = function handleTimeout() {
reject(
createError(
`Timeout of ${timeout} ms exceeded`,
config,
"ECONNABORTED",
request
)
);
};
if (onDownloadProgress) {
request.onprogress = onDownloadProgress;
}
if (onUploadProgress) {
request.upload.onprogress = onUploadProgress;
}
}
function processHeaders(): void {
if (isFormData(data)) {
delete headers["Content-Type"];
}
if ((withCredentials || isURLSameOrigin(url!)) && xsrfCookieName) {
const xsrfValue = cookie.read(xsrfCookieName);
if (xsrfValue && xsrfHeaderName) {
headers[xsrfHeaderName] = xsrfValue;
}
}
if (auth) {
headers["Authorization"] =
"Basic " + btoa(auth.username + ":" + auth.password);
}
Object.keys(headers).forEach((name) => {
if (data === null && name.toLowerCase() === "content-type") {
delete headers[name];
} else {
request.setRequestHeader(name, headers[name]);
}
});
}
function processCancel(): void {
if (cancelToken) {
cancelToken.promise
.then((reason) => {
request.abort();
reject(reason);
})
.catch(
/* istanbul ignore next */
() => {
// do nothing
}
);
}
}
function handleResponse(response: AxiosResponse): void {
if (!validateStatus || validateStatus(response.status)) {
resolve(response);
} else {
reject(
createError(
`Request failed with status code ${response.status}`,
config,
null,
request,
response
)
);
}
}
});
}
function throwIfCancellationRequested(config: AxiosRequestConfig): void {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
```
代码非常多,我们一点点来看。
首先,我们在`Axios`类的构造函数中增加了一个`defaults`属性,用来存储默认配置。
```typescript
export default class Axios {
defaults: AxiosRequestConfig;
constructor(initConfig: AxiosRequestConfig) {
this.defaults = initConfig;
}
// ...
}
```
然后,我们在`request`方法中,把用户传入的配置与默认配置做了合并。
```typescript
request(url: any, config?: any): AxiosPromise {
if (typeof url === "string") {
if (!config) {
config = {}
}
config.url = url
} else {
config = url
}
config = mergeConfig(this.defaults, config)
// ...
}
```
接下来,我们修改了`transformURL`函数,在函数内部增加了对`baseURL`的处理。
```typescript
function transformURL(config: AxiosRequestConfig): string {
const { url, params, paramsSerializer, baseURL } = config;
if (baseURL && !isAbsoluteURL(url!)) {
return combineURL(baseURL, url!);
}
return buildURL(url!, params, paramsSerializer);
}
```
如果配置了 `baseURL`,并且 `url` 不是绝对地址,我们会把 `baseURL` 和 `url` 做拼接,这里我们使用了 `isAbsoluteURL` 和 `combineURL` 两个辅助函数,我们在 `src/helpers/url.ts` 中实现它们。
```typescript
// src/helpers/url.ts
// ...
export function isAbsoluteURL(url: string): boolean {
return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url);
}
export function combineURL(baseURL: string, relativeURL: string): string {
return relativeURL
? baseURL.replace(/\/+$/, "") + "/" + relativeURL.replace(/^\/+/, "")
: baseURL;
}
```
最新文章
- 夜间行车安全:大灯、近光灯与远光灯使用全指南
- 新能源汽车充电设施与动力电池技术发展现状与未来趋势
- 电动汽车革命:固态电池与800V高压平台如何重塑未来出行
- 汽车的未来发展趋势
- 汽车发动机号码全解析:位置、作用与常见问题
- 智能汽车如何'看见'世界:激光雷达与多传感器融合技术解析
- 高速行驶中汽车引擎轰鸣声震耳欲聋
- 双环汽车科技新突破
- 车辆年检全攻略:材料、流程及常见问题解答
- 车险理赔全流程指南:从事故定损到维修清单审核
- ESP与AEB:汽车安全双保险的工作原理与性能对比
- 高强度钢材+盗抢险:双重守护爱车的安全屏障
- 宝马驾驶体验极致操控感受
- 动力电池革新与V2X技术引领电动汽车产业新变革
- 智能温控风扇系统优化汽车能量管理
- 自动驾驶与新能源革命:重塑未来出行新生态
- 智能仪表盘革命:从机械指针到全息投影的驾驶体验升级
- 智能驾驶与新能源:重塑未来出行的三大核心技术
- 双涡轮增压引擎动力强劲加速迅猛
- 电动汽车三电系统核心技术解析:电池、电机与电控
