Axios 源码解析

本文不会细抠某些功能的具体实现方式,比如 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 源码的解读所做的总结。最主要的就是拦截器和适配器的实现。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容

  • 基类 Axios 跟随入口 index.js 进入/lib/axios.js,第一个方法则是createInsta...
    丶梅边阅读 641评论 0 1
  • 实在来不及自己写了 把读过的文章先转过来 明天再进行编辑 axios项目目录结构 注:因为我们需要要看的代码都是...
    vivianXIa阅读 866评论 0 1
  • Axios是近几年非常火的HTTP请求库,官网上介绍Axios 是一个基于 promise 的 HTTP 库,可以...
    milletmi阅读 3,498评论 0 9
  • axios如何实现多种请求方式 原理: 通过数组循环来批量注册接口,统一调用同一个方法,参数差异:通过until....
    前端的爬行之旅阅读 273评论 0 0
  • 一、拦截器介绍 先看下官方文档[https://axios-http.com/docs/interceptors]...
    前端艾希阅读 801评论 0 1