Axios 是一个基于 promise 网络请求库,作用于 node.js
和浏览器中。 它是 isomorphic 的(即同一套代码可以运行在浏览器和 node.js
中)。在服务端它使用原生 node.js
http
模块, 而在客户端 (浏览端) 则使用 XMLHttpRequests
。
从 Axios 的官方介绍可以得知,这是一个可以同时运行在浏览器客户端 + Node 服务端的网络请求库,在浏览器运行时,使用 XMLHttpRequests
构建请求,在 Node
环境时使用 node
的 http
模块构建网络请求。
今天,我们围绕着 axios
的源码实现进行解读,解读完成后,再实现一个简易的 axios
库。
我们先来看看 axios
库的项目目录结构。(如下图)
从上图可以得到两个信息:
- axios 的核心文件是
lib/axios
,所以我们如果只关注axios
运行时的话,只需要看lib
这个目录下的文件即可。 - axios 运行只依赖一个第三方库
follow-redirects
,这个库是用于处理HTTP
重定向请求的,axios
的默认行为是跟随重定向的,可以猜测是用这个库来做重定向跟随的。 —— 如果你不想要自动跟随重定向,需要显式声明maxRedirects=0
。
我在百度一直没找到 axios
的官方文档,所以这里贴一份 axios 官方文档,大家可以参考使用。
lib/axios
我们打开 lib/axios.js
文件看看。(如下图)
重点关注这几行核心就可以了。
行数 | 描述 |
---|---|
第 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
实例是 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
。(如下图)
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
方法中对于拦截器和请求的处理非常优雅,我会重点介绍。
相对比较简单的部分我直接用表格介绍(如下)
行数 | 描述 |
---|---|
第 32 ~ 41 行 |
判断首个参数,组装 config 配置,禁止无 url 的请求 |
第 46 ~ 52 行 |
设置请求方法,如果未声明则使用默认配置的请求方法,如果未设置默认的请求方法,则使用 get 请求 |
下面我们要着重介绍一下 拦截器
的处理,我们先看 请求拦截器
。
这里有两个文档中未说明的参数,这里我们做一下解释:
- 第
58
行:使用use
注册拦截器时,第三个参数中的options.runWhen
方法将会先被调用,如果该方法返回false
,则跳过该请求拦截器。 - 第
62
行:使用use
注册拦截器时,第三个参数中的options.synchronous
参数将显式声明该拦截器为同步
,否则默认为异步
拦截器,将会通过promise
进行调用。 —— 其实我觉得这个参数没什么意义了,统一异步
就好了。可能作者还考虑了其他的某些同步场景,我暂时还没有想到。
重点注意:第 64
行使用了 unshift
方法将 请求拦截器
按注册的逆序添加到 requestInterceptorChain
中,供后续执行。
这也就意味着 请求拦截器
对同一份配置的修改,后面加的拦截器是无法覆盖前置拦截器的。
我们看看下面这个请求拦截器案例就知道了。
后设置的拦截器看起来并未生效,看过源码我们就知道了,其实是执行顺序导致的。
axios
这么设计的原因可能是防止 请求拦截器
滥用导致配置被后续处理人覆盖。—— 但这点没有在文档说明,如果正好碰上这种场景,难免会造成一些困惑。
而 响应拦截器
的处理就简单多了,相信我应该不用多做解释了。(如下图)
稍微值得注意的是,拦截器
将成功处理和错误处理都添加到了内部拦截器数组中,也就是说数组内部是这样的:
['拦截器成功处理处理函数', '拦截器出错处理函数', '拦截器成功处理处理函数', '拦截器出错处理函数', ...]
了解这个数据结构,对最后这一段核心代码的实现理解是有帮助的(如下图)
我们需要对每一行代码进行逐行解析:
行数 | 描述 |
---|---|
第 74 行 |
判断是否为异步请求拦截器(默认:是) |
第 75 行 |
声明 chain 数组,数组第一个元素是发起请求的方法(可以简单理解为 fetch 方法),第二个元素是为了 82 行凑数的 undefined
|
第 77 行 |
将所有的 请求拦截器 添加到 chain 的开头位置 |
第 78 行 |
将所有的 响应拦截器 添加到 chain 的尾部位置 |
第 80 ~ 83 行 |
用 config 构建第一个 Promise ,然后按顺序依次执行 chain —— 请求拦截器 -> 真实请求 -> 响应拦截器 ,每次执行传入的就是 成功处理函数 (作为 resolve ) 和 失败处理函数 (作为 reject ) |
最后一段 chain
的执行,非常优雅的阐述了 axios
内部的工作流程,也就是 请求拦截器
-> 真实请求
-> 响应拦截器
这一套核心工作流。
建议大家可以仔细再看看最后一段函数的处理,仔细品一品。
下面还有一段关于
同步请求拦截器
的处理,基本上是大同小异的,感兴趣的童鞋可以自行阅读一下。
小结
好了,到这里,axios
的基本结构和核心工作流程就解析完了。
下一章,我会针对 真实请求
—— dispatchRequest
进行详细解析,请大家继续关注。
最后一件事
如果您已经看到这里了,希望您还是点个赞再走吧~
您的点赞是对作者的最大鼓励,也可以让更多人看到本篇文章!
如果觉得本文对您有帮助,请帮忙在 github 上点亮 star
鼓励一下吧!