# 1. 前言

在实际项目中,所有请求的请求配置对象config中有些字段其实都是相同的,例如请求超时事件timeout,亦或者说我们需要给所有请求都添加一个相同的字段,例如在进行身份认证的时候我们需要给所有请求都添加Authorization。我们现在实现的axios所有请求配置都是独立的,也就是说如果你需要给所有请求都加上某个配置字段,那么你需要在配置axios的配置对象的时候都加上这一字段,这无疑将会产生许多重复代码。而官方的axios为我们提供了默认配置对象axios.defaults,我们可以把所有相同的配置字段都写入该默认配置对象,那么这个配置字段将会在所有的请求中都生效。

接下来,我们也要实现这一默认配置功能。其实,这没有多么复杂,我们默认提供一个配置对象,然后只需将用户配置对象与默认配置对象进行合并,然后发出请求即可。

OK,我们接下来就来实现它。

# 2. 创建默认配置对象 defaults

根据官方axios文档给出的默认配置示例:

axios.defaults.baseURL = "https://api.example.com";
axios.defaults.headers.common["Authorization"] = AUTH_TOKEN;
axios.defaults.headers.post["Content-Type"] =
  "application/x-www-form-urlencoded";
1
2
3
4

其中:

  • axios.defaults.headers.common['Authorization'] = AUTH_TOKEN表示给所有请求的headers都添加Authorization,并且值为AUTH_TOKEN
  • axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';表示给所有POST请求的headers都添加Content-Type,并且值为application/x-www-form-urlencoded

有了以上概念,我们就可以创建出默认对象defaultes,我们在src目录下新建defaultes.ts文件,在该文件内创建默认对象defaultes,如下:

// src/defaultes.ts

import { AxiosRequestConfig } from "./types";

const defaults: AxiosRequestConfig = {
  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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

我们暂时为默认配置对象defaults中只添加了默认请求超时时间timeout和请求头headers,并且我们在headers中设置了common属性,用于存放所有请求都需要的请求头字段,另外与common同级下还创建了每个请求方式属性,用于存放不同请求所特有的请求头字段。例如像需要数据的请求方式postputpatch我们为其默认添加了Content-Type字段,而不需要数据的请求方式deletegetheadoptions则为其留空。(其实默认配置对象里面的内容远不止这些,详细内容可查看这里~ (opens new window)

OK,默认配置对象defaults就已经创建好了。

# 3. 向 Axios 类中添加默认配置对象

在官方axios中,从axios对象上可以点出来defaults对象,所以我们还需要将创建好的默认配置对象添加到Axios类中,从而可以在实例axios对象上点出来defaults

// src/core/Axios.ts

export default class Axios {
  defaults: AxiosRequestConfig;
  interceptors: {
    request: InterceptorManager<AxiosRequestConfig>;
    response: InterceptorManager<AxiosResponse<any>>;
  };
  constructor() {
    this.defaults = {};
    this.interceptors = {
      request: new InterceptorManager<AxiosRequestConfig>(),
      response: new InterceptorManager<AxiosResponse>()
    };
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

仅仅是这样还不行,虽然现在axios对象可以点出来defaults,但是点出来defaults却是一个空{},我们还应该把上面创建的默认配置对象传进来,确保axios对象点出来的是真正的defaults

我们把上面创建的默认配置对象通过Axios类的构造函数传进来,如下:


export default class Axios {
  defaults: AxiosRequestConfig;
  interceptors: {
    request: InterceptorManager<AxiosRequestConfig>;
    response: InterceptorManager<AxiosResponse<any>>;
  };
  constructor(defaultConfig: AxiosRequestConfig) {
    this.defaults = defaultConfig;
    this.interceptors = {
      request: new InterceptorManager<AxiosRequestConfig>(),
      response: new InterceptorManager<AxiosResponse>()
    };
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

然后在src/axios.ts中创建axios实例的地方接收该配置对象:

import { AxiosInstance, AxiosRequestConfig } from "./types";
import Axios from "./core/Axios";
import { extend } from "./helpers/util";
import defaults from "./defaultes";

function getAxios(config: AxiosRequestConfig): AxiosInstance {
  const context = new Axios(config);
  const axios = Axios.prototype.request.bind(context);

  extend(axios, context);

  return axios as AxiosInstance;
}

const axios = getAxios(defaults);

export default axios;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这样我们就可以在执行 getAxios创建 axios 对象的时候,把默认配置传入了。现在才算是把创建的默认配置对象defaults真正的添加到Axios类中了,另外,别忘了给Axios类的类型接口定义中添加该字段:

export interface Axios {
  defaults: AxiosRequestConfig;
  // ...
}
1
2
3
4

默认配置对象有了之后,接下来,我们就该把用户的配置对象跟默认配置对象做一合并,把合并后配置对象随着请求发出就大功告成啦。

# 4. 合并配置对象

所谓合并配置对象,就是将默认配置对象defaults与用户自己配置的请求配置对象config进行合并,然后将合并后的配置对象作为真正的请求配置对象发出请求。合并之前,我们先来观察一下要合并的两个对象:

defaults = {
  method: 'get',

  timeout: 0,

  headers: {
    common: {
      Accept: 'application/json, text/plain, */*'
    }
  }
}

userConfig = {
  url: '/config/post',
  method: 'post',
  data: {
    a: 1
  },
  headers: {
    test: '321'
  }
}

mergedConfig = {
  url: '/config/post',
  method: 'post',
  data: {
    a: 1
  },
  timeout: 0,
  headers: {
    common: {
      Accept: 'application/json, text/plain, */*'
    }
    test: '321'
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

通过观察,我们发现,这两个对象的合并可不是简简单单的字段合并,这里面要分情况处理:

  • 对于timeoutresponseType等这些常规属性,合并起来比较容易,即如果用户配置了就用用户配置的,如果用户没配置,则用默认配置的;
  • 对于一些属性如 urlmethodparamsdata,这些属性都是跟每个请求息息相关的,请求不同从而千变万化,所以像这四个属性我们在合并的时候不管默认配置对象里面有没有,我们只取用户配置的;
  • 对于headerauth等这些属性就比较复杂了,这些属性的合并可不是取这个不取那个的问题,而是要将默认配置的与用户配置的做一次深度合并。如在headers中,字段不相同的要拷贝合并在一起,字段相同的,内容不同也要拷贝合并在一起;

了解了以上三种情况后,接下来我们在合并的时候就要分情况处理。

首先,在src/core目录下创建mergeConfig.ts文件,在该文件内编写合并函数,函数框架如下:

import { AxiosRequestConfig } from "../types";

export default function mergeConfig(
  defaultConfig: AxiosRequestConfig,
  userConfig?: AxiosRequestConfig
): AxiosRequestConfig {
  let config = Object.create(null); // 创建空对象,作为最终的合并结果

  // 1.常规属性,如果用户配置了就用用户配置的,如果用户没配置,则用默认配置的;

  // 2.只接受用户配置,不管默认配置对象里面有没有,我们只取用户配置的;

  // 3.复杂对象深度合并

  return config;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

OK,接下里我们就根据不同情况分别处理。

# 4.1 常规属性

对于常规属性,我们遵循如果用户配置了就用用户配置的,如果用户没配置,则用默认配置的;

// 1.常规属性,如果用户配置了就用用户配置的,如果用户没配置,则用默认配置的;
let defaultToUserConfig = [
  "baseURL",
  "transformRequest",
  "transformResponse",
  "paramsSerializer",
  "timeout",
  "withCredentials",
  "adapter",
  "responseType",
  "xsrfCookieName",
  "xsrfHeaderName",
  "onUploadProgress",
  "onDownloadProgress",
  "maxContentLength",
  "validateStatus",
  "maxRedirects",
  "httpAgent",
  "httpsAgent",
  "cancelToken",
  "socketPath",
];
defaultToUserConfig.forEach((prop) => {
  userConfig = userConfig || {};
  // 如果用户配置里有
  if (typeof userConfig[prop] !== "undefined") {
    // 则用用户配置里的
    config[prop] = userConfig[prop];
    // 如果用户配置里没有,默认配置里有
  } else if (typeof defaultConfig[prop] !== "undefined") {
    // 则用默认配置里的
    config[prop] = defaultConfig[prop];
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 4.2 只接受用户配置

对于 urlmethodparamsdata这些属性,只接受用户配置,不管默认配置对象里面有没有,我们只取用户配置的;

// 2.只接受自定义配置,不管默认配置对象里面有没有,我们只取用户配置的;
let valueFromUserConfig = ["url", "method", "params", "data"];
valueFromUserConfig.forEach((prop) => {
  userConfig = userConfig || {};
  if (typeof userConfig[prop] !== "undefined") {
    config[prop] = userConfig[prop];
  }
});
1
2
3
4
5
6
7
8

# 4.3 复杂对象深度合并

对于headerauth等这些属性我们就要进行深度合并,例如在默认配置对象和用户配置对象的headers属性中,我们需要把两个headers内字段不相同的属性要拷贝合并在一起,如果属性字段相同的,那么属性内容不同也要拷贝合并在一起;

// 3.复杂对象深度合并
let mergeDeepProperties = ["headers", "auth", "proxy"];
mergeDeepProperties.forEach((prop) => {
  userConfig = userConfig || {};
  if (isObject(userConfig[prop])) {
    config[prop] = deepMerge(defaultConfig[prop], userConfig[prop]);
  } else if (typeof userConfig[prop] !== "undefined") {
    config[prop] = userConfig[prop];
  } else if (isObject(defaultConfig[prop])) {
    config[prop] = deepMerge(defaultConfig[prop]);
  } else if (typeof defaultConfig[prop] !== "undefined") {
    config[prop] = defaultConfig[prop];
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

对于上述代码,还是拿headers属性举个例子说明一下:

  • 如果在用户配置对象userConfig中配置了headers属性,并且该属性是个对象,那么就调用deepMerge函数把默认配置对象defaultConfig中的headers和用户配置对象userConfig中的headers进行合并,最后把合并结果放入最终返回的config对象中的headers;
  • 如果userConfig中的headers不是对象,并且不为空,那直接就把它放入最终返回的config对象中的headers;
  • 如果userConfig中的headers为空,表示用户没有配置该属性,并且如果defaultConfig中的headers是个对象,那就直接把defaultConfig中的headers深拷贝一份放入最终返回的config对象中的headers`;
  • 如果userConfig中的headers为空,并且defaultConfig中的headers不是对象,也不为空,那直接就把它放入最终返回的config对象中的headers;

这就是深度合并的逻辑,另外,这里面还调用的一个深度合并的工具函数deepMerge,接下来,我们就在src/helpers/util.ts中实现这个工具函数,该函数支持传入若干个对象,把传入的所有对象进行合并,最后返回。如下:

export function deepMerge(...objs: any[]): any {
  const result = Object.create(null);

  for (let i = 0; i < objs.length; i++) {
    const obj = objs[i];
    for (let key in obj) {
      assignValue(obj[key], key);
    }
  }

  function assignValue(val: any, key: string) {
    if (isObject(result[key]) && isObject(val)) {
      result[key] = deepMerge(result[key], val);
    } else if (isObject(val)) {
      result[key] = deepMerge({}, val);
    } else {
      result[key] = val;
    }
  }
  return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

代码说明:

  • 函数内部先创建了一个空对象result,作为最终返回的结果对象;
  • 然后遍历传进来所有对象,每个对象再遍历所有的属性,调用assignValue子函数将当前遍历的对象中的每个属性都拷贝到result上;
  • 把所有传进来的对象遍历完毕后,即把所有对象的所有属性都拷贝到了result上,最终将result返回;

# 4.4 添加到 request 方法中

OK,合并逻辑实现好之后,我们就可以在Axios类的request方法中将默认配置对象与用户配置对象进行合并了。

// src/core/Axios.ts

import mergeConfig from "./mergeConfig";

request(url: any, config?: any): AxiosPromise {
    if (typeof url === "string") {
      config = config ? config : {};
      config.url = url;
    } else {
      config = url;
    }

    config = mergeConfig(this.defaults, config);

    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 5. 扁平化 headers

经过上面的配置对象合并后,其他属性都可以了,但是合并出来的headers却是如下形式的:

headers: {
  common: {
    Accept: 'application/json, text/plain, */*'
  },
  post: {
    'Content-Type':'application/x-www-form-urlencoded'
  }
}
1
2
3
4
5
6
7
8

而真正发请求是所需要的headers是这样的:

headers: {
  Accept: 'application/json, text/plain, */*',
 'Content-Type':'application/x-www-form-urlencoded'
}
1
2
3
4

所以,我们还需要把合并后的headers扁平化,即把所有的属性提取出来放入headers下。这里要注意的是,对于 common 中定义的 header 字段,我们都要提取,而对于 postget 这类提取,需要和该次请求的方法对应。

OK,那么我们就来实现一个函数,用于将合并后的headers扁平化,在src/helpers/headers.ts中创建flattenHeaders函数,如下:

// src/helpers/headers.ts

export function flattenHeaders(headers: any, method: Method): any {
  if (!headers) {
    return headers;
  }
  headers = deepMerge(headers.common || {}, headers[method] || {}, headers);

  const methodsToDelete = [
    "delete",
    "get",
    "head",
    "options",
    "post",
    "put",
    "patch",
    "common",
  ];

  methodsToDelete.forEach((method) => {
    delete headers[method];
  });

  return headers;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

我们通过 deepMerge 的方式把 commonpost 的属性拷贝到 headers 这一级,然后再把 commonpost 这些属性删掉。最后返回的headers就是我们想要的扁平化后的headers

实现好之后,我们就在src/core/dispatchRequest.ts文件中真正发送请求之前调用它:

function processConfig(config: AxiosRequestConfig): void {
  config.url = transformUrl(config);
  config.headers = transformHeaders(config);
  config.data = transformRequestData(config);
  config.headers = flattenHeaders(config.headers, config.method!);
}
1
2
3
4
5
6

这样确保我们了配置中的 headers 是可以正确添加到请求 header 中的。

OK,终于该合并的已经合并完了,接下来,我们就可以编写demo来测试下效果如何。

# 6. demo 编写

examples 目录下创建 mergeConfig目录,在 mergeConfig目录下创建 index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>mergeConfig demo</title>
  </head>
  <body>
    <script src="/__build__/mergeConfig.js"></script>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10

接着再创建 app.ts 作为入口文件:

import axios from "../../src/axios";
import qs from "qs";

axios.defaults.headers.common["NLRX"] = "Hello NLRX";
axios.defaults.headers.post["NLRX1"] = "post NLRX";
axios.defaults.headers.get["NLRX2"] = "get NLRX";

axios({
  url: "/api/mergeConfig",
  method: "post",
  data: qs.stringify({
    a: 1,
  }),
  headers: {
    test: "321",
  },
}).then((res) => {
  console.log(res.data);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

在该demo中,我们显示的给默认配置对象添加了 postgetcommonheaders,并且我们在请求中的配置对象也配置了headers,另外,我们的默认配置对象默认的会给post请求加上 Content-Type 字段,它的值是 application/x-www-form-urlencoded

我们可以预测下该请求中的headers应该包含哪些内容,由于这个请求时post类型,故axios.defaults.headers.get["NLRX2"] = "get NLRX";不应该生效,所以它的headers至少应该包含如下:

headers = {
  // ...
  Accept: 'application/json, text/plain, */*',
  Content-Type:'application/x-www-form-urlencoded',
  NLRX:"Hello NLRX",
  NLRX1 : "post NLRX",
  test: "321",
  // ...
}
1
2
3
4
5
6
7
8
9

我们可以在demo结果中观察验证是否如此。

接着在 server/server.js 添加新的接口路由:

// 默认配置合并
router.post("/api/mergeConfig", function(req, res) {
  res.json(req.body);
});
1
2
3
4

最后在根目录下的index.html中加上启动该demo的入口:

<li><a href="examples/mergeConfig">mergeConfig</a></li>
1

OK,我们在命令行中执行:

# 同时开启客户端和服务端
npm run server | npm start
1
2

接着我们打开 chrome 浏览器,访问 http://localhost:8000/ (opens new window) 即可访问我们的 demo 了,我们点击 mergeConfig,通过F12network 部分我们可以看到请求已正常发出,并且请求的headers如下:

从结果中我们可以看到,跟我们之前预测的结果完全相符,至此,默认配置合并就已经实现了。