回顾
前一篇文章完成了axios的拦截器和取消请求的功能,基本上一个axios的功能就基本完成,但是使用起来还是有些不方便,还不支持一些默认参数的配置,导致我们每次都要添加这些参数,今天就来扩展一下axios,让它支持默认配置的功能,还有我们会完成transformResquest和transformResponse这2个不常用的功能。
修改默认配置
要给axios配置默认参数,有2种办法:
-
通过axios.defaults修改axios这个实例的默认配置项
-
通过给axios.create指定一个默认配置参数来产生一个新的实例
生成的这个新axios实例就拥有了新的默认配置,但不会影响原来的那个axios实例的默认配置
axios.defaults
axios内部会维护一个defaults的默认配置,这个配置会暴露给使用者让其可以进行修改,我们新建一个/src/defaults.js文件:
const defaults = {
method: "get",
headers: {
common: {
Accept: "application/json, text/plain, */*"
}
},
timeout: 0
}
const methodsWithData = ["post", "put", "patch"]
methodsWithData.forEach(method => {
defaults.headers[method] = {
"Content-Type": "application/x-www-form-urlencoded"
}
})
export default defaults
这个defaults暴露给使用者后(具体怎么暴露下面会说),使用者通过axios.defaults进行任意修改,比如下面这样:
// 修改请求头,每次发请求都会携带
axios.defaults.headers.common['test'] = '123'
// 修改请求头,只有post请求的时候才会携带
axios.defaults.headers.post = {
'xxx':'1234'
}
// 修改默认超时时间
axios.defaults.timeout = 3000
可以根据自己的需要任意修改,上面的代码是修改了默认的headers和timeout,你当然可以url、data、pramas,只不过这么做意义不大,因为对于每个请求url、data、params肯定不同。
由于我们最终要将我们传递的配置和这个defaults默认配置进行合并产生一个新的config,合并的策略是这样的:
-
对于url、params、data参数,我们不会采用defaults中的设置的,而总会使用用户传递的
-
对headers参数,我们会采用深度合并
-
对于除了url、params、data、headers以外的参数,我们采用的合并策略就是使用用户传递的参数(也就是后面的配置覆盖前面的)
在此之前,我们先实现一个工具方法 deepMerge 来进行深度合并,在src/hepler/utils.js中:
export function deepMerge(...configs) {
const newConfig = {}
configs.forEach(config => {
Object.keys(config).forEach(key => {
if (isPlainObject(config[key])) {
if (newConfig[key]) {
newConfig[key] = deepMerge(newConfig[key], config[key])
} else {
newConfig[key] = deepMerge(config[key])
}
} else {
newConfig[key] = config[key]
}
})
})
return newConfig
}
代码不难理解,根据用户传递的多个配置循环的读取每个配置,然后进行深度合并,最后产生一个合并后的新的对象。
接下来,我们新建一个src/mergeConfig.js类实现我们的合并策略:
import {deepMerge} from "../helper/utils"
import {isPlainObject} from "../helper/utils"
const strats = Object.create(null)
function defaultStrat(val1, val2) {
return typeof val2 !== "undefined" ? val2 : val1
}
function fromVal2Strat(val1, val2) {
if (typeof val2 !== "undefined") {
return val2
}
}
function deepMergeStrat(val1, val2) {
if (isPlainObject(val2)) {
return deepMerge(val1, val2)
} else if (typeof val2 !== "undefined") {
return val2
} else if (isPlainObject(val1)) {
return deepMerge(val1)
} else {
return val1
}
}
const stratKeysFromVal2 = ["url", "data", "params"]
stratKeysFromVal2.forEach(item => {
strats[item] = fromVal2Strat
})
const stratKeysDeepMerge = ["headers"]
stratKeysDeepMerge.forEach(item => {
strats[item] = deepMergeStrat
})
export default function mergeConfig (config1, config2) {
const config = Object.create(null)
for (let key in config1) {
config[key] = merge(key)
}
for (let key in config2) {
if (!config[key]) {
config[key] = merge(key)
}
}
function merge(key) {
const strat = strats[key] || defaultStrat
return strat(config1[key], config2[key])
}
return config
}
最后export default 出来的mergeConfig方法就是我们要调用的方法,它接收2个配置对象,根据我们字段的不同采取不同的合并策略,最终返回一个全新的config。
在大功告成之前,我们还有一件事要做,就是此时我们的这个全新的config里面的headers对象的结构并不是我们想要的,目前它结构大概是这样的:
headers:{
common: {
"Accept": "application/json, text/plain, */*"
},
post:{
"Content-Type": "application/x-www-form-urlencoded"
},
put:{
"Content-Type": "application/x-www-form-urlencoded"
},
patch:{
"Content-Type": "application/x-www-form-urlencoded"
}
}
这种结构肯定是不行的,我们需要的是这样的结构:
headers:{
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/x-www-form-urlencoded"
}
也就是要把数据结构拉平,修改src/hepler/header.js,新增一个flattenHeaders方法:
export function flattenHeaders(headers, method) {
if (!headers) return headers
headers = deepMerge(headers.common, headers[method], headers)
const deleteMethods = [
"delete",
"get",
"head",
"options",
"post",
"put",
"patch",
"common"
]
deleteMethods.forEach(action => {
delete headers[action]
})
return headers
}
紧接着的事情就很简单了,我们只需要在每次发送请求的时候调用一下mergeConfig就行,修改src/core/axios.js:
constructor(defaults) {
this.defaults = defaults
...
...
}
添加一行mergeConfig的代码,把用户配置和默认配置合并
request(config) {
// 只需添加这个合并代码
config = mergeConfig(this.defaults, config)
...
...
}
在src/core/dispatchRequest.js真正的请求发送之前,对headers字段做特殊处理,也就是把字段拉平:
function processConfig(config) {
const {url, method, params, headers = {}, data} = config
// 处理headers拉平
config.headers = flattenHeaders(headers, method)
...
...
}
最后一步,修改我们暴露axios的地方,src/axios.js:
import defaults from "./defaults
function createInstance(defaults) {
const axios = new Axios(defaults)
...
...
}
const axios = createInstance(defaults)
export default axios
在这里我们引入了defaults.js文件,传递给Axios类的构造函数,结合上面修改的src/core/axios.js,整个逻辑就跑通了。
axios.create
axios.create会根据我们传递给它的配置产生一个默认配置,新的axios实例就使用这个默认配置,我们可以少传递一些参数。
有了前面的代码基础,实现起来就很简单了,在src/axios.js中:
function createInstance(defaults) {
const axios = new Axios(defaults)
...
...
}
axios.create = function (defaultConfig) {
return createInstance(mergeConfig(defaults, defaultConfig))
}
export default axios
transformResquest和transformResponse
transformRequest的意义
发送请求的时候,在我们已经实现的代码中,axios帮我们做了一件事,如果data是对象就会添加一个Content-Type:application/json的请求头,并且把data使用JSON.stringify转换一下,这是默认处理的行为,我们不能定制。所以就提供了transformReqeust,让用户可以干预这个过程。
transformResponse的意义
在接收到响应的时候,data如果是字符串的对象,axios会用JSON.parse把字符串转为一个json对象,如果想修改这个行为,可以使用transformResponse。
实现transformRequest和transformResponse
修改src/defaults.js文件,添加transformRequest和transformResponse的默认逻辑:
const defaults = {
...
...
transformRequest: [
function(headers, data) {
processHeaders(headers, data)
return transformRequest(data)
}
],
transformResponse: [
function(data) {
return transformResponse(data)
}
]
}
给默认配置中添加了transformRequest和transformResponse的默认逻辑,如果用户想修改,在调用axios的时候覆盖transformRequest和transformResponse这2个配置项就行。
然后添加一个文件src/core/transform.js:
export default function transform(fns, data, headers) {
if (!fns) return data
fns.forEach(fn => {
data = fn(headers, data)
})
return data
}
因为我们已经把默认逻辑转移在了src/defaults.js中,所以在src/core/dispatchRequest.js中我们要删除原来的处理了,同时调用上面的transform函数完成处理:
function processConfig(config) {
const { url, method, params, headers = {}, data, transformRequest } = config
...
...
config.data = transform(transformRequest, data, headers)
}
export default function dispatchRequest(config) {
...
...
processConfig(config)
return xhr(config).then(res => {
res.data = transform(transformResponse, data)
return res
})
}
总结
至此,已经完成了axios几乎所有的功能,剩下的一些功能并不常用,就不做实现了。
自己这个axios的代码已经写了3次,用ts写了2次,js写了1次,每次写都有不同的体会。ts写的时候虽然麻烦点,但是调试起来真的非常的方便,也加深了对ts的掌握。当时自己主要对axios的拦截器和取消功能比较感兴趣,所以研究了一遍源码,也学到了组织代码的方式,收获还是满满的。