Axios
是一个NB的网络请求库(前后端神器),基于Promise
封装了HTTP请求,用于浏览器和node.js,GitHub 77000+ Star(截至2020年10月18日)。也是前端必备的一个第三方库。
Axios的代码不算复杂,反而清晰易懂、十分优雅(个人觉得特别是请求/响应拦截器的处理和cancelToken的处理),另外它涉及了很多JavaScript的基础知识,非常适合用来巩固基础。
本文在讲源码的同时也会穿插一些涉及前端的知识。
本文使用的axios版本: v0.20.0。
axios的项目结构如下,省略了和本文无关的目录或文件和细节目录。它的源代码在lib
下:
axios
├─ lib // 项目源码目录
│ ├─ adapters // 请求适配器
│ │ ├─ http.js // http适配器
│ │ └─ xhr.js // xhr适配器
│ ├─ axios.js // axios的实例
│ ├─ cancel // 请求取消模块
│ ├─ core // 核心模块
│ │ ├─ Axios.js // Axios对象
│ │ ├─ buildFullPath.js // url构建
│ │ ├─ createError.js // 自定义异常相关
│ │ ├─ dispatchRequest.js // 请求封装
│ │ ├─ enhanceError.js // 自定义异常相关
│ │ ├─ InterceptorManager.js // 拦截器类
│ │ ├─ mergeConfig.js // 配置合并工具
│ │ ├─ settle.js // promise处理工具
│ │ └─ transformData.js // 数据转换工具
│ ├─ defaults.js // 默认配置
│ ├─ helpers // 各种工具函数 │ └─ utils.js
Axios的一次基本流程如下:
执行下面的代码,我们创建了一个axios实例,我们接下来按照代码顺序来阐述整个执行过程。
const axios = require('../../lib/axios');
模块的代码如下:
// 创建一个Axios实例
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig);
var instance = bind(Axios.prototype.request, context);
// 将axios.prototype复制到实例(继承)
utils.extend(instance, Axios.prototype, context);
// 将上下文复制到实例(继承)
utils.extend(instance, context);
return instance;
}
// 创建要导出的默认实例
var axios = createInstance(defaults);
// 公开Axios类以允许类继承
axios.Axios = Axios;
// 用于创建新实例的工厂
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
// Expose Cancel & CancelToken
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
// Expose all/spread
axios.all = function all(promises) {
return Promise.all(promises);
};
axios.spread = require('./helpers/spread');
module.exports = axios;
// Allow use of default import syntax in TypeScript
module.exports.default = axios;
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig);
var instance = bind(Axios.prototype.request, context);
// 将axios.prototype复制到实例(继承)
utils.extend(instance, Axios.prototype, context);
// 将上下文复制到实例(继承)
utils.extend(instance, context);
return instance;
}
见名知义,createInstance
便是创建实例的核心函数了,它返回了instance
这个变量,内部通过extend
、bind
这两个工具来进行加工。
第二行:var instance = bind(Axios.prototype.request, context)
, 这里的bind
函数如下:
'use strict';
module.exports = function bind(fn, thisArg) {
return function wrap() {
var args = new Array(arguments.length);
for (var i = 0; i < args.length; i++) {
args[i] = arguments[i];
}
return fn.apply(thisArg, args);
};
};
bind()
最终返回一个function
,这个function
的作用:以thisArg
为函数调用上下文(this),调用fn
。
最终,instance
变成了一个函数,即Axios.prototype.request
,函数调用上下文(this)为context
, 也就是new Axios(defaultConfig)
, 它来自在前一行新建的一个axios对象。
关于 apply()
给函数传参,并拥有控制函数调用上下文即函数体内 this 值的能力 ,在上面的例子中,函数的this为 thisArg,参数为args(一个数组),它通过一个简单的循环遍历得到。
类似的,ECMAScript 中的函数还有一个方法:call(),只不过
call()
的参数需要一个一个列出来。
关于 arguments
函数内部存在的一个特殊对象,它是一个类数组对象,包含调用函数时传入的所有参数。这个对象只有以 function 关键字定义函数(相对于使用箭头语法创建函数)时才会有。
在上面代码出现的
arguments
是wrap()
函数的参数。
值得注意的是,虽然在ECMAScript5 中已经内置了这个方法:Function.prototype.bind()
, 但由于兼容性问题(iE9+),不直接使用。
utils.extend()
也是一个工具函数,内容如下:
/**
* 通过可变地添加对象b的属性来扩展对象a。
*
* @param {Object} a 要扩展的对象
* @param {Object} b 要复制属性的对象
* @param {Object} thisArg 要绑定功能的对象
* @return {Object} 对象a的结果值
*/
function extend(a, b, thisArg) {
forEach(b, function assignValue(val, key) {
if (thisArg && typeof val === 'function') {
a[key] = bind(val, thisArg);
} else {
a[key] = val;
}
});
return a;
}
里面又出现了一个foreach()
,内容如下:
function forEach(obj, fn) {
// Don't bother if no value provided
if (obj === null || typeof obj === 'undefined') {
return;
}
// Force an array if not already something iterable
if (typeof obj !== 'object') {
/*eslint no-param-reassign:0*/
obj = [obj];
}
if (isArray(obj)) {
// Iterate over array values
for (var i = 0, l = obj.length; i < l; i++) {
fn.call(null, obj[i], i, obj);
}
} else {
// Iterate over object keys
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
fn.call(null, obj[key], key, obj);
}
}
}
}
我们自上而下解读:
特殊情况的判断,obj
为空,或者obj
为 undefined
,我们不处理,直接 return
if (obj === null || typeof obj === 'undefined') {
return;
}
对于不可迭代的对象,我们强制转换成一个array
。
if (typeof obj !== 'object') {
obj = [obj];
}
对于可迭代的数组,我们执行 fn
for (var i = 0, l = obj.length; i < l; i++) {
fn.call(null, obj[i], i, obj);
}
对于可迭代的对象,也是差不多的思路。
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
fn.call(null, obj[key], key, obj);
}
}
综上所述,foreach
遍历一个(可迭代的)数组或一个对象,为每个项执行传入的函数。
回到我们的extend()
,看看我们传入的函数 assignValue
,这个函数的作用便是在迭代b的过程中为a赋值。
function extend(a, b, thisArg) {
forEach(b, function assignValue(val, key) {
// 对于函数,利用了上面说到的bind()
if (thisArg && typeof val === 'function') {
a[key] = bind(val, thisArg);
} else {
a[key] = val;
}
});
return a;
}
所以最终我们的instance
被如何扩展?很明显:
extend
扩展了Axios.prototype
, 即Axios对象的原型。extend
扩展了context
, 即Axios对象,这个对象含有用户传入的一系列配置信息(这个对象的详细内容会在下面详细说明)。这里有一个小问题,为什么我们在生成实例的时候不直接生成Axios
对象,而是先以Axios.prototype.request
为基础,然后基于上面说到的两部分进行扩展呢?个人认为是方便调用者调用,不用额外再手动新建对象。
在axios实例被export
前,它被添加了几个静态方法。
axios.create
给用户提供了自定义配置的接口,通过调用mergeConfig()
来合并用户配置和默认配置。从而我们可以得到一个自定义的instance
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
如果你了解过Promise.all()
, 那么你一定可以猜出axios.all
是个啥了。
axios.all = function all(promises) {
return Promise.all(promises);
};
axios.spread
常和 axios.all
配合使用,例如:
function getUserAccount() {
return axios.get('/user/12345');
}
function getUserPermissions() {
return axios.get('/user/12345/permissions');
}
axios.all([getUserAccount(), getUserPermissions()])
.then(axios.spread(function (acct, perms) {
// 两个请求现在都执行完成
}));
我们来看看axios.spread()
的实现:
'use strict';
module.exports = function spread(callback) {
return function wrap(arr) {
return callback.apply(null, arr);
};
};
axios.spread()
要求我们传入一个函数,最终promise.all()
的结果会作为它的参数,并通过callback.apply()
执行。
这个功能非常有意思,用户可以调用这个接口来随时取消请求,像这样:
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('xxxxxxxxx', {
cancelToken: source.token
}).catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// 处理错误
}
});
source.cancel('请求被用户取消!');
CancelToken.source()
是一个工厂方法,它生产了一个对象,包含CancelToken()
和一个默认的cancel()
,于是调用者就可以用下面的方法注册CancelToken()
。
// 方法1 - 默认的工厂方法
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('xxxxxxxxxxxx', {
cancelToken: source.token
}).catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// 处理错误
}
});
// 方法2 - 自定义cancel
let cancel;
axios