Axios 源码解读 —— request 篇

Axios 是一个基于 promise 网络请求库,作用于 node.js 和浏览器中。 它是 isomorphic 的(即同一套代码可以运行在浏览器和 node.js 中)。在服务端它使用原生 node.js http 模块, 而在客户端 (浏览端) 则使用 XMLHttpRequests

从 Axios 的官方介绍可以得知,这是一个可以同时运行在浏览器客户端 + Node 服务端的网络请求库,在浏览器运行时,使用 XMLHttpRequests 构建请求,在 Node 环境时使用 nodehttp 模块构建网络请求。

今天,我们围绕着 axios 的源码实现进行解读,解读完成后,再实现一个简易的 axios 库。

我们先来看看 axios 库的项目目录结构。(如下图)

Axios 源码解读 —— request 篇_第1张图片

从上图可以得到两个信息:

  1. axios 的核心文件是 lib/axios,所以我们如果只关注 axios 运行时的话,只需要看 lib 这个目录下的文件即可。
  2. axios 运行只依赖一个第三方库 follow-redirects,这个库是用于处理 HTTP 重定向请求的,axios 的默认行为是跟随重定向的,可以猜测是用这个库来做重定向跟随的。 —— 如果你不想要自动跟随重定向,需要显式声明 maxRedirects=0

我在百度一直没找到 axios 的官方文档,所以这里贴一份 axios 官方文档,大家可以参考使用。

lib/axios

我们打开 lib/axios.js 文件看看。(如下图)

Axios 源码解读 —— request 篇_第2张图片

重点关注这几行核心就可以了。

| 行数 | 描述 |
| ---- | ---- |
| 第 26 行 | 文档中的 axios.create 调用的是 createInstance 函数,这个函数将会新建一个 Axios 实例 |
| 第 34 行 | 新建了一个默认的 axios 实例 |
| 第 37 ~ 52 行 | 默认的 axios 实例添加了大量的属性和方法 |
| 第 57 行 | 将默认的 axios 实例导出 |

Axios

接下来,我们来看看 Axios 类,这是 axios 源码最核心的部分,位于 lib/core/Axios.js

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

Axios 接收配置,将 instanceConfig 配置存在 axios.defaults 属性中,用于后续的请求。

同时,通过 InterceptorManager 来管理请求拦截器和响应拦截器,在后面我们会展开聊聊这个拦截管理器 —— InterceptorManager

axios 默认的实例将会使用 lib/defaults.js 中的配置进行创建。

Axios 这个设置,我们就知道文档中关于修改配置的这部分内容缘由了。(如下图)

Axios 源码解读 —— request 篇_第3张图片

我们也能看出 Axios 实例是 axios 对于 网络请求-默认配置 的最小单位,而不存在所有实例共享的一套 “全局默认配置”

但是我们可以通过两个方法来实现。

其中一个方法就是我们写两套配置,一套全局默认配置,一套各实例的个性化配置,在创建 axios 实例的时候做手动合并。

当然,还有个更聪明的方法,我们先来看看之前 P1 的 createInstance 方法。

function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);
  
  //...

  instance.create = function create(instanceConfig) {
    // 这里通过闭包继承了 defaultConfig 配置,新创建的实例会继承原实例的配置
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };

  return instance;
}

从代码中可以看出,createInstance 方法内部通过闭包继承了 defaultConfig 配置,新创建的实例会继承原实例的配置。

这样的话,我们还可以通过先用一套全局默认配置创建一个 axios 实例,然后再使用这个 axios 实例,调用 axios/instance.create 方法创建其他的 axios 实例,这样所有的 axios 实例都可以继承全局默认配置。

拦截管理器 —— InterceptorManager

我们现在来看看 axios 内部的拦截管理器 InterceptorManager。(如下图)

Axios 源码解读 —— request 篇_第4张图片

InterceptorManager 的总体实现还是挺简单的,内部有个 handlers 数组,存放了所有通过 use 方法注册的拦截器。

use 方法返回了 handlers.length - 1 作为拦截器 id,在调用 eject 方法时,会将对应 ID 下标的拦截器设置为 null

forEach 方法对所有的拦截器方法进行遍历,执行传入的 fn 回调函数,将拦截器作为参数传入 fn 中。

从这里可以看出,axios 内部的职责划分还是比较清晰的,InterceptorManager 只负责收集管理拦截器,而不关心拦截器的执行逻辑。

不过我感觉 forEach 方法设计的有些冗余,如果是我来设计的话,我可能只会暴露一个 getter 方法让外部获取 handlers。这里作者的设计可能有一些别的考虑,我暂时还没有想到。

request

接下来,我们看看 Axios 类的核心方法,也就是 request 方法,axios 通过 request 方法来发起真实网络请求。

这一段代码比较长,我会对这一大段代码进行逐行解析。request 方法中对于拦截器和请求的处理非常优雅,我会重点介绍。

相对比较简单的部分我直接用表格介绍(如下)

Axios 源码解读 —— request 篇_第5张图片

| 行数 | 描述 |
| ---- | ---- |
| 第 32 ~ 41 行 | 判断首个参数,组装 config 配置,禁止无 url 的请求 |
| 第 46 ~ 52 行 | 设置请求方法,如果未声明则使用默认配置的请求方法,如果未设置默认的请求方法,则使用 get 请求 |

下面我们要着重介绍一下 拦截器 的处理,我们先看 请求拦截器

Axios 源码解读 —— request 篇_第6张图片

这里有两个文档中未说明的参数,这里我们做一下解释:

  • 58 行:使用 use 注册拦截器时,第三个参数中的 options.runWhen 方法将会先被调用,如果该方法返回 false,则跳过该请求拦截器。
  • 62 行:使用 use 注册拦截器时,第三个参数中的 options.synchronous 参数将显式声明该拦截器为 同步,否则默认为 异步 拦截器,将会通过 promise 进行调用。 —— 其实我觉得这个参数没什么意义了,统一 异步 就好了。可能作者还考虑了其他的某些同步场景,我暂时还没有想到。

重点注意:第 64 行使用了 unshift 方法将 请求拦截器 按注册的逆序添加到 requestInterceptorChain 中,供后续执行。

这也就意味着 请求拦截器 对同一份配置的修改,后面加的拦截器是无法覆盖前置拦截器的。

我们看看下面这个请求拦截器案例就知道了。

Axios 源码解读 —— request 篇_第7张图片

后设置的拦截器看起来并未生效,看过源码我们就知道了,其实是执行顺序导致的。

axios 这么设计的原因可能是防止 请求拦截器 滥用导致配置被后续处理人覆盖。—— 但这点没有在文档说明,如果正好碰上这种场景,难免会造成一些困惑。

响应拦截器 的处理就简单多了,相信我应该不用多做解释了。(如下图)

image

稍微值得注意的是,拦截器 将成功处理和错误处理都添加到了内部拦截器数组中,也就是说数组内部是这样的:

['拦截器成功处理处理函数', '拦截器出错处理函数', '拦截器成功处理处理函数', '拦截器出错处理函数', ...]

了解这个数据结构,对最后这一段核心代码的实现理解是有帮助的(如下图)

Axios 源码解读 —— request 篇_第8张图片

我们需要对每一行代码进行逐行解析:

| 行数 | 描述 |
| ---- | ---- |
| 第 74 行 | 判断是否为异步请求拦截器(默认:是) |
| 第 75 行 | 声明 chain 数组,数组第一个元素是发起请求的方法(可以简单理解为 fetch 方法),第二个元素是为了 82 行凑数的 undefined |
| 第 77 行| 将所有的 请求拦截器 添加到 chain 的开头位置 |
| 第 78 行| 将所有的 响应拦截器 添加到 chain 的尾部位置 |
| 第 80 ~ 83 行| 用 config 构建第一个 Promise,然后按顺序依次执行 chain —— 请求拦截器 -> 真实请求 -> 响应拦截器,每次执行传入的就是 成功处理函数(作为 resolve) 和 失败处理函数(作为 reject) |

最后一段 chain 的执行,非常优雅的阐述了 axios 内部的工作流程,也就是 请求拦截器 -> 真实请求 -> 响应拦截器 这一套核心工作流。

建议大家可以仔细再看看最后一段函数的处理,仔细品一品。

下面还有一段关于 同步请求拦截器 的处理,基本上是大同小异的,感兴趣的童鞋可以自行阅读一下。

小结

好了,到这里,axios 的基本结构和核心工作流程就解析完了。

下一章,我会针对 真实请求 —— dispatchRequest 进行详细解析,请大家继续关注。

最后一件事

如果您已经看到这里了,希望您还是点个赞再走吧~

您的点赞是对作者的最大鼓励,也可以让更多人看到本篇文章!

如果觉得本文对您有帮助,请帮忙在 github 上点亮 star 鼓励一下吧!

你可能感兴趣的