美文网首页
Axios 源码解析

Axios 源码解析

作者: 一蓑烟雨任平生_cui | 来源:发表于2022-10-27 18:55 被阅读0次

    本文不会细抠某些功能的具体实现方式,比如 config 的 merge 方式、utils 中的工具方法。而是抓住主干、梳理脉络,重点介绍经典的、优秀的实现思想。比如 adapter 怎么兼容 browser 和 node、Interceptor 简单而精巧的实现。

    过去八年,axiox 以 github97k+的 star 和 npm2000w+的周下载量占据着网络请求库的绝对地位,但 1.0.0 版本在二十天前才正式发布。具体改动查看 V1.0.0

    Axios 特性

    1. 基于 Promise 封装
    2. 作用于 node 和浏览器,node 创建 http 请求,浏览器创建 XMLHttpRequest
    3. 请求响应拦截器
    4. 数据转换
    5. 成功失败状态码自定义
    6. XSRF 防御
    7. 取消请求

    源码解析

    axios 和 Axios 的关系

    axios 是通过 bind 对 Axios.prototype.request 硬绑定了 Axios 的实例的函数。其上边添加了 Axios、CanceledError、CancelToken、formToJSON、create 等静态方法,又通过 extends 的方式将 Axios.prototype 上的方法扩展到 axios 上。所以可以通过 axios(config)、axios.get()的方式创建请求,也可以通过 new axios.Axios()、axios.create()的方式创建新的 Axios 实例。

    axios 入口

    function createInstance(defaultConfig) {
      const context = new Axios(defaultConfig)
    
      // instance为绑定了context实例的函数,函数内部调用了Axios原型上的request方法
      const instance = bind(Axios.prototype.request, context)
    
      // 将Axios原型上的方法扩展到instance上,包括请求方法等
      utils.extend(instance, Axios.prototype, context, { allOwnKeys: true })
    
      // 将context上的属性扩展到instance上,比如拦截器等
      utils.extend(instance, context, null, { allOwnKeys: true })
    
      // 提供了一个工厂函数,用来生成instance实例
      instance.create = function create(instanceConfig) {
        return createInstance(mergeConfig(defaultConfig, instanceConfig))
      }
    
      return instance
    }
    
    // 对外暴露axios
    const axios = createInstance(defaults)
    
    axios.Axios = Axios
    
    export default axios
    

    Axios

    class Axios {
      constructor(instanceConfig) {
        this.defaults = instanceConfig
        this.interceptors = {
          request: new InterceptorManager(),
          response: new InterceptorManager()
        }
      }
    }
    
    export default Axios
    

    原型上扩展请求方法,分为两类:

    1. 获取数据
    2. 提交数据
      1. 普通提交,格式为 json 或者 FormData 实例
      2. 文件提交,请求方式增加 Form 后缀,设置 Content-Type 为 multipart/form-data

    Multipart/Form-Data是一种编码类型,它允许在将文件传输到服务器进行处理之前将文件包含在表单数据中。

    // 获取数据的方法
    utils.forEach(
      ['delete', 'get', 'head', 'options'],
      function forEachMethodNoData(method) {
        Axios.prototype[method] = function (url, config) {
          return this.request(
            mergeConfig(config || {}, {
              method,
              url,
              data: (config || {}).data
            })
          )
        }
      }
    )
    
    // 提交数据的方法
    utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
      function generateHTTPMethod(isForm) {
        return function httpMethod(url, data, config) {
          return this.request(
            mergeConfig(config || {}, {
              method,
              headers: isForm
                ? {
                    'Content-Type': 'multipart/form-data'
                  }
                : {},
              url,
              data
            })
          )
        }
      }
    
      Axios.prototype[method] = generateHTTPMethod()
      Axios.prototype[method + 'Form'] = generateHTTPMethod(true)
    })
    

    所有的请求都是去调用 Axios 原型上的 request 方法,分析 request 之前先分析拦截器的实现。

    InterceptorManager

    创建拦截器管理器

    class Axios {
      constructor(instanceConfig) {
        this.defaults = instanceConfig
        this.interceptors = {
          request: new InterceptorManager(),
          response: new InterceptorManager()
        }
      }
    }
    

    拦截器构造器

    class InterceptorManager {
      constructor() {
        this.handlers = []
      }
    
      use(fulfilled, rejected, options) {
        this.handlers.push({
          fulfilled,
          rejected,
    
          // 同步执行拦截器
          synchronous: options ? options.synchronous : false,
          runWhen: options ? options.runWhen : null
        })
    
        // 返回拦截器的索引
        return this.handlers.length - 1
      }
    
      // 根据索引移除拦截器
      eject(id) {
        if (this.handlers[id]) {
          this.handlers[id] = null
        }
      }
    
      // 清除所有拦截器
      clear() {
        if (this.handlers) {
          this.handlers = []
        }
      }
    
      forEach(fn) {
        utils.forEach(this.handlers, function forEachHandler(h) {
          if (h !== null) {
            fn(h)
          }
        })
      }
    }
    
    export default InterceptorManager
    

    实例创建时会生成 reques 和 response 两种类型的拦截器。并且每种可以注册多个。每个拦截器接受三个参数:

    1. Fulfilled
    2. Rejected
    3. Options,可选
      1. synchronous,boolean 型
      2. runWhen,函数类型

    fulfilled 为成功时调用
    rejected 为抛出错误时调用

    拦截器的返回值是当前拦截器的索引。由此可以看到当 fulfilled 中出现错误时并不会被 rejected 捕获,request 中的错误会中断后续拦截器的执行,进而中断请求的发起,但是 fulfilled 中的错误不会被 rejected 捕获,会冒泡到全局,通过 promise 的 catch 捕获。比如:

    axios(url)
      .then(res => {})
      .catch(err => {
        // do something...
      })
    
    // OR
    try {
      await axios(url)
    } catch {
      // do something...
    }
    

    拦截器的执行和 Options 的两个属性在 reques 中具体解析。

    eject:根据拦截器在 handlers 中的索引移除特定的拦截器,比如:

    const interceptor = axios.interceptors.request.use(function () {})
    
    axios.interceptors.request.eject(interceptor)
    

    clear:v1.0.0 新增的方法,用来移除所有拦截器

    axios.interceptors.request.clear()
    

    request

    class Axios {
      request(configOrUrl, config) {
        if (typeof configOrUrl === 'string') {
          config = config || {}
          config.url = configOrUrl
        } else {
          config = configOrUrl || {}
        }
    
        config = mergeConfig(this.defaults, config)
    
        // Set config.method 默认 get 请求
        config.method = (
          config.method ||
          this.defaults.method ||
          'get'
        ).toLowerCase()
    
        // Flatten headers
        const defaultHeaders =
          config.headers &&
          utils.merge(config.headers.common, config.headers[config.method])
    
        defaultHeaders &&
          utils.forEach(
            ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
            function cleanHeaderConfig(method) {
              delete config.headers[method]
            }
          )
    
        // 创建请求头
        config.headers = new AxiosHeaders(config.headers, defaultHeaders)
        // 拦截器的 fulfilled 和 rejected 全部平铺到一个数组中
        // 请求拦截器,遵循先进(注册)后出(执行)的原则 栈结构
        const requestInterceptorChain = []
        let synchronousRequestInterceptors = true
    
        this.interceptors.request.forEach(function unshiftRequestInterceptors(
          interceptor
        ) {
          if (
            typeof interceptor.runWhen === 'function' &&
            interceptor.runWhen(config) === false
          ) {
            return
          }
    
          synchronousRequestInterceptors =
            synchronousRequestInterceptors && interceptor.synchronous
    
          requestInterceptorChain.unshift(
            interceptor.fulfilled,
    
            interceptor.rejected
          )
        })
    
        // 响应拦截器 遵循先进先出的原则
        const responseInterceptorChain = []
    
        this.interceptors.response.forEach(function pushResponseInterceptors(
          interceptor
        ) {
          // 同样平铺
          responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected)
        })
    
        let promise
        let i = 0
        let len
    
        if (!synchronousRequestInterceptors) {
          const chain = [dispatchRequest.bind(this), undefined]
    
          chain.unshift.apply(chain, requestInterceptorChain)
          chain.push.apply(chain, responseInterceptorChain)
          len = chain.length
          promise = Promise.resolve(config)
    
          while (i < len) {
            promise = promise.then(chain[i++], chain[i++])
          }
    
          return promise
        }
    
        len = requestInterceptorChain.length
        let newConfig = config
        i = 0
    
        // 同步执行所有请求拦截器
        while (i < len) {
          const onFulfilled = requestInterceptorChain[i++]
          const onRejected = requestInterceptorChain[i++]
    
          try {
            newConfig = onFulfilled(newConfig)
          } catch (error) {
            onRejected.call(this, error)
            break
          }
        }
    
        // 发起网络请求
        try {
          promise = dispatchRequest.call(this, newConfig)
        } catch (error) {
          return Promise.reject(error)
        }
    
        i = 0
        len = responseInterceptorChain.length
    
        // 执行所有响应拦截器
        while (i < len) {
          promise = promise.then(
            responseInterceptorChain[i++],
            responseInterceptorChain[i++]
          )
        }
    
        return promise
      }
    }
    

    request 中主要做了 4 件事:

    1. 初始化 config 配置
    2. 创建请求头
    3. 处理拦截器
    4. 发起网络请求

    具体分析拦截器的处理:

    Request Interceptor

    const requestInterceptorChain = []
    let synchronousRequestInterceptors = true
    
    this.interceptors.request.forEach(function unshiftRequestInterceptors(
      interceptor
    ) {
      if (
        typeof interceptor.runWhen === 'function' &&
        interceptor.runWhen(config) === false
      ) {
        return
      }
    
      synchronousRequestInterceptors =
        synchronousRequestInterceptors && interceptor.synchronous
    
      requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected)
    })
    

    这一步是把请求拦截器的 fulfilled 和 rejected 以先进(注册)后出(执行)的规则全部存储到栈结构。
    如果某个拦截器的配置项定义了 runWhen,则不入栈。

    synchronousRequestInterceptors 则表示请求拦截器是否同步执行。只要有一个拦截器的配置为 false,那么 synchronousRequestInterceptors 的最终结果都是 false。具体执行方式稍后分析。

    最终请求拦截器形成的栈结构结果如下:

    const requestInterceptorChain = [..., requestFulfilled3, requestRejected3, requestFulfilled2, requestRejected2, requestFulfilled1, requestRejected1]
    

    Response Interceptor

    const responseInterceptorChain = []
    
    this.interceptors.response.forEach(function pushResponseInterceptors(
      interceptor
    ) {
      responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected)
    })
    

    响应拦截器遵循先进(注册)先出(执行)的顺序。

    最终的结果如下:

    const responseInterceptorChain = [responseFulfilled1, responseRejected1,  responseFulfilled2, responseRejected2, responseFulfilled3, responseRejected3, ...]
    

    通常情况下请求拦截器的配置项 synchronous 都不会设置,默认为 false,即不是同步调用。所以通过 promise 的 then 异步链式调用。会走到下面逻辑:

    let promise
    let i = 0
    let len
    
    if (!synchronousRequestInterceptors) {
      const chain = [dispatchRequest.bind(this), undefined]
    
      chain.unshift.apply(chain, requestInterceptorChain)
      chain.push.apply(chain, responseInterceptorChain)
      len = chain.length
      promise = Promise.resolve(config)
    
      while (i < len) {
        promise = promise.then(chain[i++], chain[i++])
      }
    
      return promise
    }
    

    chain 最终形成的结构是:

    const chain = [
      requestFulfilled3,
      requestRejected3,
      requestFulfilled2,
      requestRejected2,
      requestFulfilled1,
      requestRejected1,
      dispatchRequest.bind(this),
      undefined,
      responseFulfilled1,
      responseRejected1,
      responseFulfilled2,
      responseRejected2,
      responseFulfilled3,
      responseRejected3
    ]
    

    chain 数组中以 dispatchRequest 为分界点,前面是请求拦截器,后面是响应拦截器,dispatchRequest 为真正发起请求的函数,索引为偶数的是 fulfilled,奇数的是 rejected。最终返回 promise,使得开发者可以链式调用。

    synchronousRequestInterceptors 为 false 时,异步链式调用请求拦截器。如下:

    promise = Promise.resolve(config)
    
    while (i < len) {
      promise = promise.then(chain[i++], chain[i++])
    }
    

    这里真是巧妙。两次 i++,取出来的两个函数正好对应到 then 的两个参数。
    当 synchronousRequestInterceptors 为 true,即同步调用拦截器。步骤:

    1. 按顺序同步调用请求拦截器
    len = requestInterceptorChain.length
    let newConfig = config
    i = 0
    
    while (i < len) {
      const onFulfilled = requestInterceptorChain[i++]
      const onRejected = requestInterceptorChain[i++]
    
      try {
        newConfig = onFulfilled(newConfig)
      } catch (error) {
        onRejected.call(this, error)
        break
      }
    }
    
    1. 发起网络请求
    // 发起网络请求
    try {
      promise = dispatchRequest.call(this, newConfig)
    } catch (error) {
      return Promise.reject(error)
    }
    
    1. 异步链式调用响应拦截器
    i = 0
    len = responseInterceptorChain.length
    
    // 异步链式执行所有响应拦截器
    while (i < len) {
      promise = promise.then(
        responseInterceptorChain[i++],
        responseInterceptorChain[i++]
      )
    }
    

    至此,真正发起网络请求前的工作全部完成。接下来是网络请求环节。

    dispatchRequest

    dispatchRequest 中发起真正的网络请求。

    function throwIfCancellationRequested(config) {
      if (config.cancelToken) {
        config.cancelToken.throwIfRequested()
      }
    
      if (config.signal && config.signal.aborted) {
        throw new CanceledError()
      }
    }
    
    function dispatchRequest(config) {
      throwIfCancellationRequested(config)
    
      config.headers = AxiosHeaders.from(config.headers)
      config.data = transformData.call(config, config.transformRequest)
    
      // 获取请求适配器
      const adapter = config.adapter || defaults.adapter
    
      // 发起请求
      return adapter(config).then(
        function onAdapterResolution(response) {
          throwIfCancellationRequested(config)
    
          response.data = transformData.call(
            config,
            config.transformResponse,
            response
          )
          response.headers = AxiosHeaders.from(response.headers)
    
          return response
        },
    
        function onAdapterRejection(reason) {
          if (!isCancel(reason)) {
            throwIfCancellationRequested(config)
    
            if (reason && reason.response) {
              reason.response.data = transformData.call(
                config,
                config.transformResponse,
                reason.response
              )
              reason.response.headers = AxiosHeaders.from(reason.response.headers)
            }
          }
    
          return Promise.reject(reason)
        }
      )
    }
    

    adapter

    由于 axios 即可在浏览器中也可在 node.js 中使用。不仅会在运行时根据环境区分,而且可以做到应用程序打包构建时根据目标环境只加载对应环境的包。

    运行时适配

    import httpAdapter from './http.js'
    import xhrAdapter from './xhr.js'
    
    const adapters = {
      http: httpAdapter,
      xhr: xhrAdapter
    }
    
    export default {
      getAdapter: nameOrAdapter => {
        const adapter = adapters[nameOrAdapter]
        return adapter
      },
      adapters
    }
    
    // 获取运行时环境
    function getDefaultAdapter() {
      let adapter
    
      if (typeof XMLHttpRequest !== 'undefined') {
        adapter = adapters.getAdapter('xhr')
      } else if (
        typeof process !== 'undefined' &&
        utils.kindOf(process) === 'process'
      ) {
        adapter = adapters.getAdapter('http')
      }
    
      return adapter
    }
    

    xhrAdapter 为浏览器环境,通过创建 XMLHttprequest 请求。
    httpAdapter 为 node.js 环境,创建 http 请求。

    构建时适配

    源码文件:

    image.png

    目标环境为浏览器的项目构建后:

    image.png

    之所以做到这一点是,我们在构建时一般默认目标环境是 web,在 axios 源码包的 package.json 中,配置了 browser 字段。

    image.png

    xhr

    1. 创建 XMLHttpRequest 对象
    2. 设置超时时间、请求头、响应类型、鉴权、跨域携带凭证等
    3. 监听各种事件,比如 onreadystatechange、onabort、onerror、ontimeout、onDownloadProgress、onUploadProgress 等
    4. 发送请求

    默认成功状态码是 status >= 200 & status < 300,也可通过 validateStatus 自行设定。

    http

    1. 一系列初始化工作
    2. http/https/data 等请求

    取消请求

    两种方式可以取消请求:

    1. AbortController, 这种是以 fetch API 方式
    const controller = new AbortController()
    
    axios
      .get('/foo', {
        signal: controller.signal
      })
      .then(function (response) {
        //...
      })
    
    // 取消请求
    controller.abort() // 不支持 message 参数
    
    1. CancelToken
    const CancelToken = axios.CancelToken
    const source = CancelToken.source()
    
    axios
      .get('/user', {
        cancelToken: source.token
      })
      .catch(function (thrown) {
        if (axios.isCancel(thrown)) {
          console.log('Request canceled', thrown.message)
        } else {
          // 处理错误
        }
      })
    
    // 取消请求(message 参数是可选的)
    source.cancel('取消请求~')
    

    也可以通过传递一个 executor 函数到 CancelToken 的构造函数来创建一个 cancel token:

    const CancelToken = axios.CancelToken
    let cancel
    
    axios.get('/user', {
      cancelToken: new CancelToken(function executor(c) {
        // executor 函数接收一个 cancel 函数作为参数
        cancel = c
      })
    })
    
    //取消请求
    cancel()
    

    这种方式将会废弃。不做过多讨论。只分析基于 AbortController 方式取消请求的实现思路。

    image.png

    在配置对象上设置 signal 为 AbortController 的实例,当调用 dispatchRequest 的时候首先判断 config.signal.aborted 的状态,如果是 true,则说明请求已经被取消了,然后抛出错误,阻断请求的发起。

    function throwIfCancellationRequested(config) {
      if (config.signal && config.signal.aborted) {
        throw new CanceledError()
      }
    }
    

    这里为什么调用两次?

    image.png

    因为请求拦截器的执行分为同步和异步。
    如果是异步的,进入到 dispatchRequest 中时取消请求的动作已经完成,所以直接抛出错误阻断请求的发起即可。
    如果是同步,那么从请求拦截器到发起请求的动作都是同步的,所以执行取消的动作在发起请求之后了。所以要拦截本次请求只能在请求结束后 then 中阻断了。
    可能会疑惑,请求都结束了,取消动作的执行还有什么意义,其实细想,作为开发者,或者说在实际业务开发中,我们只是不想要本次请求的结果,比如,页面初始化后,同时并发了三个请求,但是一旦发现没登陆,那么就需要执行 A 操作,如果不做取消的处理,三个请求的结果都是没登陆,那么就需要执行三次 A 操作,大可不必,或者不合理不正确。

    以上就是这三天对 axios 源码的解读所做的总结。最主要的就是拦截器和适配器的实现。

    相关文章

      网友评论

          本文标题:Axios 源码解析

          本文链接:https://www.haomeiwen.com/subject/cvyftdtx.html