目录

🌓 实现合并默认配置

# 需求分析

在前面,发送请求的时候可以传入一个配置,决定请求的不同行为。要使 它可以有默认配置,定义一些默认的行为,这样在发送每个请求,用户传递的配置可以和默认配置做一层合并。

axios 库相同,应该给每一个 aixos 对象添加一个如下的 defaults 属性,表示默认配置,并且可以直接修改这些默认配置:

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

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

# 默认配置

# 定义默认配置

创建文件 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
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

定义了 defaults 常量,包含默认请求的方法、超时时间、以及 headers 配置。分别根据请求方法是否会携带数据,对应的情况添加请求头的 Content-Type

# 添加到 axios 对象中

axios 对象添加 defaults 属性,表示默认配置:

src/core/Axios.ts 中:

export default class Axios {
  defaults: AxiosRequestConfig
  interceptors: Interceptors


  constructor(initConfig: AxiosRequestConfig) {
    this.defaults = initConfig
    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

Aixos 类中添加一个 defaults 成员属性,并且让 Axios 的构造函数接受一个 initConfig 初始化配置对象,把 initConfig 赋值给 this.defaults

然后修改 createInstance 的方法,支持传入一个 config 配置对象。

src/axios.ts 中:

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

function createInstance(config: AxiosRequestConfig): AxiosInstance {
  const context = new Axios(config)
  const instance = Axios.prototype.request.bind(context)

  extend(instance, context)

  return instance as AxiosInstance
}

const axios = createInstance(defaults)

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

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

# 配置合并及策略

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

🌰 例子:

// 默认配置
config1 = {
  method: 'get',
  timeout: 0,
  headers: {
    common: {
      Accept: 'application/json, text/plain, */*'
    }
  }
}

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

// 合并后
merged = {
  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

对于新的配置要覆盖掉默认的配置,而没有设置的配置,按照默认配置进行配置。

# 合并方法

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

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

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

# 默认合并策略

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

如果有 val2 则返回 val2 ,否则返回 val1 。意味着如果存在自定义配置就是用自定义配置的字段,否则就是用默认配置;

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

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

function fromVal2Strat(val1: any, val2: any):any{
  if(typeof val2 !== 'undefined') {
    return val2
  }
}

const stratKeysFromVal2 = ['url', 'params', 'data']
stratKeysFromVal2.forEach(key => {
  strats[key] = fromVal2Strat
})
1
2
3
4
5
6
7
8
9
10

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

# 复杂对象合并策略

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

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

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
  }
}

const stratKeysDeepMerge = ['headers']
stratKeysDeepMerge.forEach(key => {
  strats[key] = deepMergeStrat
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

最后 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
  }
}

const stratKeysFromVal2 = ['url', 'params', 'data']
stratKeysFromVal2.forEach(key => {
  strats[key] = fromVal2Strat
})

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
  }
}

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) {
    if (!config[key]) {
      mergeField(key)
    }
  }

  function mergeField(key: string): void {
    const strat = strats[key] || defaultStrat
    config[key] = strat(config)
  }

  return config
}
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

最后在 request 方法中添加合并配置的逻辑。

config = mergeConfig(this.defaults, config)
1

# 扁平化 headers

经过合并之后的配置中, headers 是一个复杂的对象,多了 commonpostget 等属性,而这些属性中的值最终应该要真正添加到请求的 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: {
  Accept: 'application/json, text/plain, */*',
 'Content-Type':'application/x-www-form-urlencoded'
}
1
2
3
4

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

src/helpers/headers.ts 实现 flatterHeaders 方法:

export function flatterHeaders(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

通过 deepMerge 方法,把 commonpost 的属性拷贝到 headers 这一级之后,再把 commonpost 这些属性删掉。

在阵阵发送请求之前执行这个逻辑:

core/dispatchRequest.ts

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

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

# 编写测试 DEMO

examples/config/app.ts

import axios from '../../src'
import qs from 'qs'

axios.defaults.headers.common['test2'] = 123

axios({
  url: '/config/post',
  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

在这个例子中,额外引入 qs 库,用于查询字符串解析和字符串化。比如,对于 {a: 1} 经过字符串化变为 a=1

由于,给默认值添加了 post 、和 commonheaders ,在请求前做配置合并,于是请求就添加了 Content-Type 字段,值为 application/x-www-form-urlencoded ;另外添加了 test2 字段,值为 123

📢 上次更新: 2022/09/02, 10:18:16