🌓 实现合并默认配置
# 需求分析
在前面,发送请求的时候可以传入一个配置,决定请求的不同行为。要使 它可以有默认配置,定义一些默认的行为,这样在发送每个请求,用户传递的配置可以和默认配置做一层合并。
与 axios
库相同,应该给每一个 aixos
对象添加一个如下的 defaults
属性,表示默认配置,并且可以直接修改这些默认配置:
axios.defaults.headers.common['test'] = 123
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
axios.defaults.timeout = 2000
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
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>()
}
}
// ...
}
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
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'
}
}
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
对于新的配置要覆盖掉默认的配置,而没有设置的配置,按照默认配置进行配置。
# 合并方法
整体思路,对 config1
和 config2
的属性遍历,执行 mereField
方法做合并,这里 config1
代表默认配置, config2
代表自定义配置。
遍历过程中,通过 config2[key]
这种缩阴的方式访问,所以需要给 AxiosRequestConfig
的接口定义添加一个字符串索引签名:
export interface AxiosRequestConfig {
// ...
[propName: string]: any
}
2
3
4
5
# 默认合并策略
function defaultStart(val1: any, val2: any):any {
return typeof val2 !== 'undefined' ? val2 : val1
}
2
3
如果有
val2
则返回val2
,否则返回val1
。意味着如果存在自定义配置就是用自定义配置的字段,否则就是用默认配置;
# 只接受自定义配置合并策略
对于一些 属性: url
、 prams
、 data
,合并策略如下:
function fromVal2Strat(val1: any, val2: any):any{
if(typeof val2 !== 'undefined') {
return val2
}
}
const stratKeysFromVal2 = ['url', 'params', 'data']
stratKeysFromVal2.forEach(key => {
strats[key] = fromVal2Strat
})
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
})
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
}
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)
# 扁平化 headers
经过合并之后的配置中, headers
是一个复杂的对象,多了 common
、 post
、 get
等属性,而这些属性中的值最终应该要真正添加到请求的 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
字段,都要提取,而对于 get
、 post
这类提取,需要和该次请求方法对应。
在 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
}
2
3
4
5
6
7
8
9
10
11
12
13
通过
deepMerge
方法,把common
、post
的属性拷贝到headers
这一级之后,再把common
、post
这些属性删掉。
在阵阵发送请求之前执行这个逻辑:
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!)
}
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)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在这个例子中,额外引入
qs
库,用于查询字符串解析和字符串化。比如,对于{a: 1}
经过字符串化变为a=1
。由于,给默认值添加了
post
、和common
的headers
,在请求前做配置合并,于是请求就添加了Content-Type
字段,值为application/x-www-form-urlencoded
;另外添加了test2
字段,值为123
。