手写一个axios(三)

718 阅读6分钟

回顾

前一篇文章完成了axios的拦截器和取消请求的功能,基本上一个axios的功能就基本完成,但是使用起来还是有些不方便,还不支持一些默认参数的配置,导致我们每次都要添加这些参数,今天就来扩展一下axios,让它支持默认配置的功能,还有我们会完成transformResquest和transformResponse这2个不常用的功能。

修改默认配置

要给axios配置默认参数,有2种办法:

  1. 通过axios.defaults修改axios这个实例的默认配置项

  2. 通过给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,合并的策略是这样的:

  1. 对于url、params、data参数,我们不会采用defaults中的设置的,而总会使用用户传递的

  2. 对headers参数,我们会采用深度合并

  3. 对于除了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的拦截器和取消功能比较感兴趣,所以研究了一遍源码,也学到了组织代码的方式,收获还是满满的。