合并配置的设计与实现

需求分析

在之前的章节我们了解到,在发送请求的时候可以传入一个配置,来决定请求的不同行为。我们也希望 ts-axios 可以有默认配置,定义一些默认的行为。这样在发送每个请求,用户传递的配置可以和默认配置做一层合并。

和官网 axios 库保持一致,我们给 axios 对象添加一个 defaults 属性,表示默认配置,你甚至可以直接修改这些默认配置:

  1. axios.defaults.headers.common['test'] = 123
  2. axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
  3. axios.defaults.timeout = 2000

其中对于 headers 的默认配置支持 common 和一些请求 method 字段,common 表示对于任何类型的请求都要添加该属性,而 method 表示只有该类型请求方法才会添加对应的属性。

在上述例子中,我们会默认为所有请求的 header 添加 test 属性,会默认为 post 请求的 header 添加 Content-Type 属性。

默认配置

默认配置定义

接下来,我们先实现默认配置

defaults.ts

  1. import { AxiosRequestConfig } from './types'
  2. const defaults: AxiosRequestConfig = {
  3. method: 'get',
  4. timeout: 0,
  5. headers: {
  6. common: {
  7. Accept: 'application/json, text/plain, */*'
  8. }
  9. }
  10. }
  11. const methodsNoData = ['delete', 'get', 'head', 'options']
  12. methodsNoData.forEach(method => {
  13. defaults.headers[method] = {}
  14. })
  15. const methodsWithData = ['post', 'put', 'patch']
  16. methodsWithData.forEach(method => {
  17. defaults.headers[method] = {
  18. 'Content-Type': 'application/x-www-form-urlencoded'
  19. }
  20. })
  21. export default defaults

我们定义了 defaults 常量,它包含默认请求的方法、超时时间,以及 headers 配置。

未来我们会根据新的需求添加更多的默认配置。

添加到 axios 对象中

根据需求,我们要给 axios 对象添加一个 defaults 属性,表示默认配置:

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

我们给 Axios 类添加一个 defaults 成员属性,并且让 Axios 的构造函数接受一个 initConfig 对象,把 initConfig 赋值给 this.defaults

接着修改 createInstance 方法,支持传入 config 对象。

  1. import defaults from './defaults'
  2. function createInstance(config: AxiosRequestConfig): AxiosStatic {
  3. const context = new Axios(config)
  4. const instance = Axios.prototype.request.bind(context)
  5. // extend(instance, Axios.prototype, context)
  6. extend(instance, context)
  7. return instance as AxiosStatic
  8. }
  9. const axios = createInstance(defaults)

这样我们就可以在执行 createInstance 创建 axios 对象的时候,把默认配置传入了。

配置合并及策略

定义了默认配置后,我们发送每个请求的时候需要把自定义配置和默认配置做合并,它并不是简单的 2 个普通对象的合并,对于不同的字段合并,会有不同的合并策略。举个例子:

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

我们在 core/mergeConfig.ts 中实现合并方法。

合并方法

  1. export default function mergeConfig(
  2. config1: AxiosRequestConfig,
  3. config2?: AxiosRequestConfig
  4. ): AxiosRequestConfig {
  5. if (!config2) {
  6. config2 = {}
  7. }
  8. const config = Object.create(null)
  9. for (let key in config2) {
  10. mergeField(key)
  11. }
  12. for (let key in config1) {
  13. if (!config2[key]) {
  14. mergeField(key)
  15. }
  16. }
  17. function mergeField(key: string): void {
  18. const strat = strats[key] || defaultStrat
  19. config[key] = strat(config1[key], config2![key])
  20. }
  21. return config
  22. }

合并方法的整体思路就是对 config1config2 中的属性遍历,执行 mergeField 方法做合并,这里 config1 代表默认配置,config2 代表自定义配置。

遍历过程中,我们会通过 config2[key] 这种索引的方式访问,所以需要给 AxiosRequestConfig 的接口定义添加一个字符串索引签名:

  1. export interface AxiosRequestConfig {
  2. // ...
  3. [propName: string]: any
  4. }

mergeField 方法中,我们会针对不同的属性使用不同的合并策略。

默认合并策略

这是大部分属性的合并策略,如下:

  1. function defaultStrat(val1: any, val2: any): any {
  2. return typeof val2 !== 'undefined' ? val2 : val1
  3. }

它很简单,如果有 val2 则返回 val2,否则返回 val1,也就是如果自定义配置中定义了某个属性,就采用自定义的,否则就用默认配置。

只接受自定义配置合并策略

对于一些属性如 urlparamsdata,合并策略如下:

  1. function fromVal2Strat(val1: any, val2: any): any {
  2. if (typeof val2 !== 'undefined') {
  3. return val2
  4. }
  5. }
  6. const stratKeysFromVal2 = ['url', 'params', 'data']
  7. stratKeysFromVal2.forEach(key => {
  8. strats[key] = fromVal2Strat
  9. })

因为对于 urlparamsdata 这些属性,默认配置显然是没有意义的,它们是和每个请求强相关的,所以我们只从自定义配置中获取。

复杂对象合并策略

对于一些属性如 headers,合并策略如下:

  1. function deepMergeStrat(val1: any, val2: any): any {
  2. if (isPlainObject(val2)) {
  3. return deepMerge(val1, val2)
  4. } else if (typeof val2 !== 'undefined') {
  5. return val2
  6. } else if (isPlainObject(val1)) {
  7. return deepMerge(val1)
  8. } else if (typeof val1 !== 'undefined') {
  9. return val1
  10. }
  11. }
  12. const stratKeysDeepMerge = ['headers']
  13. stratKeysDeepMerge.forEach(key => {
  14. strats[key] = deepMergeStrat
  15. })

helpers/util.ts

  1. export function deepMerge(...objs: any[]): any {
  2. const result = Object.create(null)
  3. objs.forEach(obj => {
  4. if (obj) {
  5. Object.keys(obj).forEach(key => {
  6. const val = obj[key]
  7. if (isPlainObject(val)) {
  8. if (isPlainObject(result[key])) {
  9. result[key] = deepMerge(result[key], val)
  10. } else {
  11. result[key] = deepMerge({}, val)
  12. }
  13. } else {
  14. result[key] = val
  15. }
  16. })
  17. }
  18. })
  19. return result
  20. }

对于 headers 这类的复杂对象属性,我们需要使用深拷贝的方式,同时也处理了其它一些情况,因为它们也可能是一个非对象的普通值。未来我们讲到认证授权的时候,auth 属性也是这个合并策略。

最后我们在 request 方法里添加合并配置的逻辑:

  1. config = mergeConfig(this.defaults, config)

flatten headers

经过合并后的配置中的 headers 是一个复杂对象,多了 commonpostget 等属性,而这些属性中的值才是我们要真正添加到请求 header 中的。

举个例子:

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

我们需要把它压成一级的,如下:

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

这里要注意的是,对于 common 中定义的 header 字段,我们都要提取,而对于 postget 这类提取,需要和该次请求的方法对应。

接下来我们实现 flattenHeaders 方法。

helpers/header.ts

  1. export function flattenHeaders(headers: any, method: Method): any {
  2. if (!headers) {
  3. return headers
  4. }
  5. headers = deepMerge(headers.common || {}, headers[method] || {}, headers)
  6. const methodsToDelete = ['delete', 'get', 'head', 'options', 'post', 'put', 'patch', 'common']
  7. methodsToDelete.forEach(method => {
  8. delete headers[method]
  9. })
  10. return headers
  11. }

我们可以通过 deepMerge 的方式把 commonpost 的属性拷贝到 headers 这一级,然后再把 commonpost 这些属性删掉。

然后我们在真正发送请求前执行这个逻辑。

core/dispatchRequest.ts

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

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

demo 编写

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

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

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

  1. import axios from '../../src/index'
  2. import qs from 'qs'
  3. axios.defaults.headers.common['test2'] = 123
  4. axios({
  5. url: '/config/post',
  6. method: 'post',
  7. data: qs.stringify({
  8. a: 1
  9. }),
  10. headers: {
  11. test: '321'
  12. }
  13. }).then((res) => {
  14. console.log(res.data)
  15. })

这个例子中我们额外引入了 qs 库,它是一个查询字符串解析和字符串化的库。

比如我们的例子中对于 {a:1} 经过 qs.stringify 变成 a=1

由于我们的例子给默认值添加了 postcommonheaders,我们在请求前做配置合并,于是我们请求的 header 就添加了 Content-Type 字段,它的值是 application/x-www-form-urlencoded;另外我们也添加了 test2 字段,它的值是 123

至此,我们合并配置的逻辑就实现完了。我们在前面的章节编写 axios 的基础功能的时候对请求数据和响应数据都做了处理,官方 axios 则把这俩部分逻辑也做到了默认配置中,意味这用户可以去修改这俩部分的逻辑,实现自己对请求和响应数据处理的逻辑。那么下一节我们就来实现这个 feature。