上篇文章​《axios中的参数为啥没被完全编码》​​里我们探讨了axios的一个小的知识点,这篇我们就接着来读读axios的源码,看看他究竟是如何实现的。

初识这个库时,第一反应,这个axios怎么读,万一读错了会不会别人笑;这样的疑问不仅我会有,后面在他的​issues#802​里还看到了一群人在一起讨论。高票赞同的读法是:​acks--ee--oh-ss ​。


用中文的相似音是: ​哎克 C 欧斯 ​ ,常见读的比较多的还有类似:​ 阿克休斯 ​。


回归到文章的主题,如果让我们去写一个网络库,需要考虑哪些点:

1、支持哪些请求方式(get、post等)

2、请求配置(超时、编码)

3、支持异步、并发

4、拦截器(请求发出前拦截、请求返回后拦截)

5、取消请求

6、请求结果回调(正确、其他的)


带着这些问题,我们来一起来看看axios的源码如何做的。


首先参看下axios的目录结构:


├── adapters
│ ├── README.md
│ ├── http.js #node环境http对象
│ └── xhr.js #web环境http对象
├── axios.js #入口
├── cancel
│ ├── Cancel.js #取消的构造对象类
│ ├── CancelToken.js #取消操作的包装类
│ └── isCancel.js #工具类
├── core
│ ├── Axios.js #Axios实例对象
│ ├── InterceptorManager.js #拦截器控制器
│ ├── README.md
│ ├── buildFullPath.js #拼接请求的url、baseURL
│ ├── createError.js #创建异常信息类工具
│ ├── dispatchRequest.js #默认的拦截器(分发完全请求的拦截器)
│ ├── enhanceError.js #异常信息类实体
│ ├── mergeConfig.js #合并配置文件(用户设置的和默认的)
│ ├── settle.js #根据http-code值来resolve/reject状态
│ └── transformData.js #转换请求或相应的数据的工具类
├── defaults.js #默认配置类
├── helpers/ #一些辅助方法
└── utils.js #通用的工具类


各个类的主要功能,这里已经标注出来了,其核心模块主要有两个,一个是adapters 实际请求的发出的模块,另外一个就是core里面实现了一个请求从创建到完成的整个流程控制。adapters模块是对XMLHttpRequest的包装,这里不作过多的概述(其方法细节的实现),主要还是看下core里的做了哪些事情。


首先,我们要从源码的角度来看看一个请求是如何被发出的,下面以get请求发出到收到响应为例(axios.get)。


我们在项目使用axios时,通常是  ​import Axios from "axios" ​,别看简单的这一句导包,他里面可做了好多事:


axios源码简读_拦截器


从axios.js这个入口文件,我们看到其创建了一个axios实例,并添加了一些方法。其关键方是在createInstance()​​

function createInstance(defaultConfig) {
// 创建一个Axios实例,这里理解成包装了请求方法的一个对象
var context = new Axios(defaultConfig);


// 为request方法bind上Axios,一个包装的wrap方法:
// 可以简单理解成创建了一个对象。之所以这样创建为了方便能
// axios('https://m.zz.cn/a')这样用
var instance = bind(Axios.prototype.request, context);


// 把 Axios.prototype 上的方法拓展到instance上
utils.extend(instance, Axios.prototype, context);


// 把context中的defaults、interceptors拓展到instance实例中
utils.extend(instance, context);


return instance;
}


导包成功后,接着调用请求方法,像这样(还有其他的方式):


axios
.get(URL, {
params: {
zz: 'fe'
}
})
.then(successFun, failureFun)
.catch(erorFun);


调用的​​get​​​方法其实是new Axios()中封装的方法,其最终会调到​​Axios.prototype.request​​指向的函数中(这才是所有请求方法调用的合并体):


Axios.prototype.request = function request(config) {
...
// 合并配置参数
config = mergeConfig(this.defaults, config);


// 如果没指定请求方式,默认get
if (config.method) {
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
config.method = 'get';
}
// Hook up interceptors middleware
// ★★★ 这一块是axios的核心部分 ★★★
// 其做的事情是:通过Promise把整个请求、拦截器处理封装到一条链上,按顺序调用
// 这其中就包括最重要的一环:实际请求发送者:dispatchRequest
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);


// 添加用户拦截器到链上:请求前的
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
// 添加用户拦截器到链上:请求前的
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
// 通过promise.then产生关联
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}

return promise;
};


这里面巧妙之处创建了一个请求处理链 ​​china​​​,在通过了​​Promise.resolve()​​​创建一个promise对象,然后通过​​promise.then()​​把请求链串起来。

这种设计在设计模式上叫做:责任链模式(Chain of Responsibility),允许你将发出的请求沿着处理者链进行发送, 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。

axios源码简读_拦截器_02

上面这张网图比较形象的比喻​责任链模​。


我们在回到axios上,看他这一条链是怎样的:


axios源码简读_责任链模式_03


上面代码区里提到这个请求链中有个默认添加的重要的一环​​dispatchRequest​​,他会根据不同的平台(其实在new Axios()时已经确定使用哪个adapter了)来发出网络请求,并承担了对请求前后的数据包装工作。


function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// For browsers use XHR adapter
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// For node use HTTP adapter
adapter = require('./adapters/http');
}
return adapter;
}


接下来就是对后续拦截器的调用了(链中dispatchRequest 之前的是RequestInterceptor,之后的叫ResponseInterceptor),最终promise会回调到业务发起请求的地方。


最后,我们回头再捋一下这整个请求过程,他大致长这样:


axios源码简读_拦截器_04


后续

得益于axios中adapter的设计,后续如果我们有其他的平台的网络请求(eg.小程序),我们只需要实现对应端请求的adapter,就可以像平常一样使用了,抹平了各端的差异。

责任链模式,这种设计模式在封装网络库的时候非常实用,在Android/Java上知名的一个请求库OkHttp,其也使用了责任链模式。大家以后有机会写网络库的时候也可以参考下。

这篇文章没有过多的去细说实现细节,只从整体上讲了其实现框架,后续大家可以顺着整体框架自己去细读源码。