需求
我们在发送请求的时候,可以传入配置,来决定请求的不同行为。我们也希望ts-axios可以有默认配置,定义一些默认的行为。这样在用户发送每个请求时,用户传递的配置可以和默认配置做一次合并。
和官网的axios库保持一致,我们给axios对象添加一个defaults属性,表示默认配置,你甚至可以直接修改这些默认配置:
axios.defaults.headers.common['test'] = 123
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
axios.defaults.timeout = 2000
其中对于headers的默认配置支持common和一些请求method字段,common表示对于任何类型的请求都要添加该属性,而method表示只有该类型请求方法才会添加对应的属性。
在上述的例子中,我们会默认为所有请求的header添加test属性,会默认为post请求的header添加Content-Type属性。
默认配置
- 默认配置定义
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 = ['put', 'post', 'patch']
methodsWithData.forEach(method => {
defaults.headers[method] = {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
export default defaults
我们定义了defaults常量,它包含默认请求的方法、超时时间以及headers配置,之后我们会根据新的需求添加更多的默认配置。
- 添加到axios对象中
export default class Axios {
defaults: AxiosRequestConfig
interceptors: Interceptors
constructor(initConfig: AxiosRequestConfig) {
this.interceptors = {
request: new InterceptorManager<AxiosRequestConfig>(),
response: new InterceptorManager<AxiosResponse>()
}
this.defaults = initConfig
}
// ...
}
接着修改createInstance方法,支持传入config对象
import { AxiosInstance, AxiosRequestConfig } from "./types";
import Axios from './core/Axios'
import { extend } from "./helpers/util";
import defaults from './core/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
这样我们就可以在执行createInstance的时候,传入默认配置了。
默认配置合并策略
定义了默认配置之后,我们发送每个请求的时候,需要把自定义配置和默认配置进行合并,它并不是简单的2个普通对象的合并,对于不通的字段合并,会有不同的合并策略。如:
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'
}
}
合并配置
import { AxiosRequestConfig, Method } from "../types";
import { isPlainObject } from "../helpers/util";
export default function mergeConfig(config1: AxiosRequestConfig, config2?: AxiosRequestConfig): AxiosRequestConfig {
if (!config2) {
config2 = {}
}
console.log('config1', config1)
const config = Object.create(null)
const strats = Object.create(null)
const stratKeysFromVal2 = ['url', 'params', 'data']
stratKeysFromVal2.forEach(key => {
strats[key] = fromVal2Strat
})
const stratKeysDeepMerge = ['headers']
stratKeysDeepMerge.forEach(key => {
strats[key] = deepMergeStrat
})
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])
}
config.headers = flattenHeaders(config.headers, config.method)
return config
}
function deepMergeStrat(val1: any, val2: any): any {
val1 = val1 || {}
val2 = val2 || {}
return deepMerge(val1, val2)
}
// 复杂合并策略,比如headers
function deepMerge(...objs: any[]) {
const result = Object.create(null)
objs.forEach(obj => {
if (!obj) {
return
}
Object.keys(obj).forEach(key => {
const val = obj[key]
if (isPlainObject(val)) {
if (isPlainObject(result[key])) {
result[key] = deepMerge(result[key], val)
} else {
result[key] = deepMerge({}, val)
}
} else {
result[key] = val
}
})
})
return result
}
// 对于url, params, data默认配置是没有意义的,只要config2中配置过,就使用
function fromVal2Strat(val1: any, val2: any): any {
return val2
}
// 默认合并策略,即config2中有,则以config2中的为准
function defaultStrat(val1: any, val2: any): any {
return typeof val2 !== 'undefined' ? val2 : val1
}
因为在headers中会出现
headers: {
common: {
Accept: 'application/json, text/plain, */*'
},
post: {
'Content-Type':'application/x-www-form-urlencoded'
}
}
我们需要把它转化成这样
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type':'application/x-www-form-urlencoded'
}
所以我们需要下面的方法,把它压成一级
function flattenHeaders(headers: any, method: Method): any {
if (!headers) {
return headers
}
headers = deepMerge(headers.common || {}, headers[method] || {}, headers)
const keysToDelete = ['delete', 'get', 'head', 'options', 'post', 'put', 'patch', 'common']
keysToDelete.forEach(key => {
delete headers[key]
})
return headers
}
在request中增加mergeConfig
request(url: string | AxiosRequestConfig, config?: AxiosRequestConfig): AxiosPromise {
// 这里要定义一个新的newConfig变量,是因为AxiosPromise接收的泛型类型T是any,与config的类型不一致
let newConfig: any
newConfig = config || {}
if (typeof url === 'string') {
newConfig.url = url
} else {
newConfig = url
}
newConfig = mergeConfig(this.defaults, newConfig)
// ...
}
demo
import axios from '../../src/index'
import qs from 'qs'
axios.defaults.headers.common['test2'] = 123
axios.interceptors.request.use(config => {
config.headers.test += '1'
return config
})
axios.interceptors.request.use(config => {
config.headers.test += '2'
return config
})
axios.interceptors.request.use(config => {
config.headers.test += '3'
return config
})
axios.interceptors.response.use(res => {
res.data += '1'
return res
})
let interceptor = axios.interceptors.response.use(res => {
res.data += '2'
return res
})
axios.interceptors.response.use(res => {
res.data += '3'
return res
})
axios.interceptors.response.eject(interceptor)
axios({
url: '/api/extend/post',
method: 'post',
data: qs.stringify({
a: 1
}),
headers: {
test: '321'
}
}).then((res) => {
console.log(res.data)
})
这个例子中我们额外引入了qs库,它是一个查询字符串解析和字符串化的库。
比如我们例子中的{a: 1}经过qs.stringify变成a=1。
因为我们的例子给默认值添加了post和common的headers,我们在请求前做了配置合并,于是我们请求的headers中就添加了content-type字段,它的值是application/x-www-form-urlencoded,另外我们也加了test2字段,它的值是123.
至此,我们的合并配置的逻辑就实现完了。我们在前面的章节编写axios的基础功能的时候,对请求数据和响应数据都做了处理,官方axios则把这2部分的逻辑也做到了默认配置中,以为这用户可以去修改这2部分的逻辑,实现自己对请求和响应数据处理的逻辑。接下来,我们会来实现这部分feature。