【源码阅读】| 从零到一实现简易版axios (六)

267 阅读4分钟

我正在参加「掘金·启航计划」
在前面几个章节的学习中,我们已经实现了axios库的大部分功能。现在,为了更好的用户体验和代码健壮性,我们需要进一步优化一些细节。
首先,我们希望用户可以自定义配置axios。具体来说,我们需要提供一个配置项对象,其中可以包含用户想要配置的内容,比如设置超时时间、设置请求头等。这些配置项将覆盖默认配置项,以便满足用户的特定需求。
其次,我们希望在用户没有传入对应值时,能够自动应用默认配置。这意味着我们需要预定义一个默认配置项对象,其中包含所有可选项的默认值。当用户未传入对应值时,我们将自动使用默认值。
那么,怎么去完成这个步骤,我们来看下源码:

axios-http.com/docs/config…

github.com/axios/axios… image.png

需求分析

  • 默认预设一些配置
  • 配置具有优先级顺序
    • request config > 构造函数传入config > 默认config

首先,我们来实现default的逻辑

// default.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

axios对象添加default属性

// 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>()
    }
  }
  // 省略...

// axios.ts
function createInstance(config: AxiosRequestConfig): AxiosInstance {
  const context = new Axios(config)

  // 创建instance 指向 Axios.prototype.request 方法,并绑定了上下文 context
  const instance = Axios.prototype.request.bind(context)

  // 通过 extend 方法把 context 中的原型方法和实例方法全部拷贝到 instance 上
  extend(instance, context)

  return instance as AxiosInstance
}

const axios = createInstance(defaults)

通过上述步骤,我们实现了构造函数传入配置和默认配置的合并,接下来,处理request configdefault的合并

mergeConfig

github.com/axios/axios…

// mergeConfig.ts
import { deepMerge, isPlainObject } from '../helpers/utils'
import { AxiosRequestConfig } from '../types'

function mergeConfig(config1: AxiosRequestConfig, config2?: AxiosRequestConfig): AxiosRequestConfig {
  // 对于url,params,data  默认配置没有意义,取传入的config
  const strates: Record<string, any> = {
    url: fromVal2Strate,
    params: fromVal2Strate,
    data: fromVal2Strate,
    headers: deepMergeStrate,
    validateStatus: defaultStrate,
  }

  if (!config2) {
    config2 = Object.create(null)
  }


  return Object.keys({ ...config1, ...config2 }).reduce(
    (mergedConfig, key) => {
      if (strates[key]) {
        mergedConfig[key] = strates[key](config1[key], config2![key])
      } else if (typeof config2![key] !== 'undefined') {
        mergedConfig[key] = config2![key]
      } else {
        mergedConfig[key] = config1[key]
      }
      return mergedConfig
    },
    Object.create(null)
  )
}

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

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

function deepMergeStrate(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 {
    return val1
  }
}

export default mergeConfig

// helpers/util.ts
export function deepMerge(...objs: any[]) {
  const result = Object.create(null);

  objs.forEach(obj => {
    Object.keys(obj).forEach(key => {
      const val = obj[key];
      if (isPlainObject(val)) {
        result[key] = deepMerge(result[key] ?? {}, val);
      } else {
        result[key] = val;
      }
    });
  });

  return result;
}

至此,我们完成了默认配置和用户配置的合并操作。我们来看下输出的header
image.png
可以看出,多了很多无关的属性和需要对对象进行扁平化处理。

对应的源码地址:github.com/axios/axios…

// helpers/headers
export function flattenHeaders(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
}

// dispatchRequest.ts
function processConfig(config: AxiosRequestConfig): void {
  config.url = transformURL(config)
  config.headers = transformHeaders(config)
  config.data = transformRequestData(config)
  // flatten headers
  config.headers = flattenHeaders(config.headers, config.method!)
}

处理requestresponse的配置

接下来,我们来跟文档做对比。
image.png
可以看到,文档中有两个参数,可以帮助我们配置对resquestresponse的数据进行处理

  • transformRequest:一个函数或一个数组,用于在请求发送之前处理请求数据。
  • transformResponse:一个函数或一个数组,用于在响应接收之后处理响应数据。

即将拦截器的逻辑配置到config
首先,我们给AxiosRequestConfig增加定义

export interface AxiosRequestConfig {
  url?: string
  method?: Method
  data?: any
  params?: any
  headers?: any
  responseType?: XMLHttpRequestResponseType
  timeout?: number
  // new
  transformRequest?: AxiosTransformer | AxiosTransformer[]
  transformResponse?: AxiosTransformer | AxiosTransformer[]

  [propName: string]: any
}

// default.ts
const defaults: AxiosRequestConfig = {
  // 省略...
  transformRequest: [
    function (data: any, headers: any): any {
      processHeaders(headers, data)
      return transformRequest(data)
    }],
  transformResponse: [
    function (data: any): any {
      return transformResponse(data)
    }
  ]
}

为了能够调用默认配置上的transform,我们改造一下dispatchRequest

// core/transform.ts
export function transform(data: any, headers: any, fns?: any): any {
  if (!fns) {
    return data
  }
  // 如果fns不是数组,就转换成数组
  if (!Array.isArray(fns)) {
    fns = [fns]
  }
  fns.forEach((fn: any) => {
    data = fn(data, headers)
  })
  return data
}

// dispatchRequest.ts
function processConfig(config: AxiosRequestConfig): void {
  config.url = transformURL(config)
  // new
  config.data = transform(config.data, config.headers, config.transformRequest)
  config.headers = flattenHeaders(config.headers, config.method!)
}

function transformURL(config: AxiosRequestConfig): string {
  const { url, params } = config
  return buildURL(url!, params)
}


function transformResponseData(res: AxiosResponse): AxiosResponse {
  // new
  res.data = transform(res.data, res.headers, res.config.transformResponse)
  return res
}

通过上述逻辑的实现,我们就能够通过默认配置的方式去对响应数据和请求数据进行默认配置的处理了。

Axios.create实现

增加类型

// types/index.ts
export interface AxiosStatic extends AxiosInstance {
  create(config?: AxiosRequestConfig): AxiosInstance
}
// axios.ts
// 省略...
axios.create = function create(config) {
  return createInstance(extend(defaults, config))
}

总结

经过前几章的实现,我们已经完成了axios大部分的功能。在本章中,我们增加了对默认配置的处理,并对配置的合并顺序进行了策略性的优化。