上篇文章《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.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上,看他这一条链是怎样的:
上面代码区里提到这个请求链中有个默认添加的重要的一环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中adapter的设计,后续如果我们有其他的平台的网络请求(eg.小程序),我们只需要实现对应端请求的adapter,就可以像平常一样使用了,抹平了各端的差异。
责任链模式,这种设计模式在封装网络库的时候非常实用,在Android/Java上知名的一个请求库OkHttp,其也使用了责任链模式。大家以后有机会写网络库的时候也可以参考下。
这篇文章没有过多的去细说实现细节,只从整体上讲了其实现框架,后续大家可以顺着整体框架自己去细读源码。