上篇 Axios 源码解析(一):模块分解 将 Axios 工程的结构进行了分解,下面来解析通用工具方法部分的源码,包括包括 utils.js 和 /helpers 目录。
在github.com/MageeLin/ax… 中的analysis分支可以看到当前已解析完的文件。
utils.js
utils.js 中包含的方法如下所示:
module.exports = {
isArray: isArray,
isArrayBuffer: isArrayBuffer,
isBuffer: isBuffer,
isFormData: isFormData,
isArrayBufferView: isArrayBufferView,
isString: isString,
isNumber: isNumber,
isObject: isObject,
isPlainObject: isPlainObject,
isUndefined: isUndefined,
isDate: isDate,
isFile: isFile,
isBlob: isBlob,
isFunction: isFunction,
isStream: isStream,
isURLSearchParams: isURLSearchParams,
isStandardBrowserEnv: isStandardBrowserEnv,
forEach: forEach,
merge: merge,
extend: extend,
trim: trim,
stripBOM: stripBOM,
};
可以发现,七成的方法都是 is 开头的,也就是进行判断的工具方法。这些判断方法很多在 lodash 之类的库中都实现过,甚至 JS 已经原生实现了一部分。但是 Axios 毕竟是个久经考验的库,所以值得学习下如何手动稳定实现:
// 引入绑定this指向的bind方法
var bind = require('./helpers/bind');
// utils 是一个通用的辅助函数库,不特定于axios使用
// 借用Object原型上的toString方法
var toString = Object.prototype.toString;
/**
* 判断value是否为数组
*
* @param {Object} val 要检验的值
* @returns {boolean} 是数组返回true,否则返回false
*/
function isArray(val) {
return toString.call(val) === '[object Array]';
}
/**
* 判断value是否为undefined
*
* @param {Object} val 要检验的值
* @returns {boolean} 是undefined返回true,否则返回false
*/
function isUndefined(val) {
return typeof val === 'undefined';
}
/**
* 判断value是否为Buffer
*
* @param {Object} val 要检验的值
* @returns {boolean} 是Buffer返回true,否则返回false
*/
function isBuffer(val) {
return (
val !== null &&
!isUndefined(val) &&
val.constructor !== null &&
!isUndefined(val.constructor) &&
typeof val.constructor.isBuffer === 'function' &&
val.constructor.isBuffer(val)
);
}
/**
* 判断value是否为ArrayBuffer
*
* @param {Object} val 要检验的值
* @returns {boolean} 是ArrayBuffer返回true,否则返回false
*/
function isArrayBuffer(val) {
return toString.call(val) === '[object ArrayBuffer]';
}
/**
* 判断value是否为FormData
*
* @param {Object} val 要检验的值
* @returns {boolean} 是FormData返回true,否则返回false
*/
function isFormData(val) {
return typeof FormData !== 'undefined' && val instanceof FormData;
}
/**
* 判断value是否为ArrayBuffer上的view
*
* @param {Object} val 要检验的值
* @returns {boolean} 是ArrayBuffer上的view返回true,否则返回false
*/
function isArrayBufferView(val) {
var result;
if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView) {
result = ArrayBuffer.isView(val);
} else {
result = val && val.buffer && val.buffer instanceof ArrayBuffer;
}
return result;
}
/**
* 判断value是否为String
*
* @param {Object} val 要检验的值
* @returns {boolean} 是String返回true,否则返回false
*/
function isString(val) {
return typeof val === 'string';
}
/**
* 判断value是否为Number
*
* @param {Object} val 要检验的值
* @returns {boolean} 是Number返回true,否则返回false
*/
function isNumber(val) {
return typeof val === 'number';
}
/**
* 判断value是否为Object
*
* @param {Object} val 要检验的值
* @returns {boolean} 是Object返回true,否则返回false
*/
function isObject(val) {
return val !== null && typeof val === 'object';
}
/**
* 判断value是否为 纯Object
*
* @param {Object} val 要检验的值
* @returns {boolean} 是 纯Object 返回true,否则返回false
*/
function isPlainObject(val) {
if (toString.call(val) !== '[object Object]') {
return false;
}
var prototype = Object.getPrototypeOf(val);
return prototype === null || prototype === Object.prototype;
}
/**
* 判断value是否为 Date
*
* @param {Object} val 要检验的值
* @returns {boolean} 是 Date 返回true,否则返回false
*/
function isDate(val) {
return toString.call(val) === '[object Date]';
}
/**
* 判断value是否为 File
*
* @param {Object} val 要检验的值
* @returns {boolean} 是 File 返回true,否则返回false
*/
function isFile(val) {
return toString.call(val) === '[object File]';
}
/**
* 判断value是否为 Blob
*
* @param {Object} val 要检验的值
* @returns {boolean} 是 Blob 返回true,否则返回false
*/
function isBlob(val) {
return toString.call(val) === '[object Blob]';
}
/**
* 判断value是否为 Function
*
* @param {Object} val 要检验的值
* @returns {boolean} 是 Function 返回true,否则返回false
*/
function isFunction(val) {
return toString.call(val) === '[object Function]';
}
/**
* 判断value是否为 Stream
*
* @param {Object} val 要检验的值
* @returns {boolean} 是 Stream 返回true,否则返回false
*/
function isStream(val) {
return isObject(val) && isFunction(val.pipe);
}
/**
* 判断value是否为 URLSearchParams对象
*
* @param {Object} val 要检验的值
* @returns {boolean} 是 URLSearchParams对象 返回true,否则返回false
*/
function isURLSearchParams(val) {
return (
typeof URLSearchParams !== 'undefined' && val instanceof URLSearchParams
);
}
/**
* 判断是否运行在标准浏览器环境中
*
* 允许 axios 在浏览器工作者线程和react-native中运行。
* 两种环境都支持 XMLHttpRequest,但并不是完全标准的全局变量。
*
* 浏览器工作者线程:
* typeof window -> undefined
* typeof document -> undefined
*
* react-native:
* navigator.product -> 'ReactNative'
* nativescript
* navigator.product -> 'NativeScript' or 'NS'
*/
function isStandardBrowserEnv() {
if (
typeof navigator !== 'undefined' &&
(navigator.product === 'ReactNative' ||
navigator.product === 'NativeScript' ||
navigator.product === 'NS')
) {
return false;
}
return typeof window !== 'undefined' && typeof document !== 'undefined';
}
注意:
bind是在/helpers中实现的一个方法,作用是手动修改this的指向
trim
trim 是 utils.js 中的第一个不以 is 开头的方法。作用很简单,和 String.prototype.trim() 的功能相同,使用正则表达式匹配,将前后的空白剔除掉:
/**
* 修剪字符串前后的空白
*
* @param {String} str 要修剪的字符串
* @returns {String} 去除前后空白后的字符串
*/
function trim(str) {
return str.replace(/^\s*/, '').replace(/\s*$/, '');
}
forEach
forEach 是项目中大量使用的一个方法。它既可以迭代纯对象,也可以迭代数组,fn 参数是迭代过程中的回调函数。
/**
* 迭代数组或对象,对每一子项都执行一个回调函数
*
* 如果 `obj` 是一个数组,将会将子项值、索引和整个数组传给回调函数
* 如果 `obj` 是一个对象,将会将子项值、子项键和整个对象传给回调函数
*
* @param {Object|Array} obj 要迭代的对象
* @param {Function} fn 对每一项执行的回调函数
*/
function forEach(obj, fn) {
// 如果obj没有值,就直接return
if (obj === null || typeof obj === 'undefined') {
return;
}
// 如果obj不是一个对象,就封装成数组
if (typeof obj !== 'object') {
/*eslint no-param-reassign:0*/
obj = [obj];
}
if (isArray(obj)) {
// 迭代数组值
for (var i = 0, l = obj.length; i < l; i++) {
fn.call(null, obj[i], i, obj);
}
} else {
// 迭代对象键
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
fn.call(null, obj[key], key, obj);
}
}
}
}
个人认为这种实现方式不好,迭代数组和迭代对象的方法应分别封装,便于理解和找错。
merge
merge 是个可变参数的函数,期望每个参数都是一个对象,然后把所有的参数的属性合并,返回合并后的新对象。
内部使用了 arguments 来实现的可变参数,使用递归来将对象进行了深层次拆分:
/**
* varargs 期望每个参数都是一个对象,然后合并每个对象的属性并返回新结果(原来的对象不可变)。
*
* 当多个对象包含相同的键时,参数列表中后面的对象将覆盖之前的。
*
* 举例:
*
* ```js
* var result = merge({foo: 123}, {foo: 456});
* console.log(result.foo); // 输出 456
* ```
*
* @param {Object} obj1 要合并的对象
* @returns {Object} 合并所有属性后的结果
*/
function merge(/* obj1, obj2, obj3, ... */) {
var result = {};
// 一个递归方法,合并值到result中去(把引用类型都拆开)
function assignValue(val, key) {
if (isPlainObject(result[key]) && isPlainObject(val)) {
// 递归
result[key] = merge(result[key], val);
} else if (isPlainObject(val)) {
// 递归
result[key] = merge({}, val);
} else if (isArray(val)) {
result[key] = val.slice();
} else {
result[key] = val;
}
}
// 每个参数都需要执行一遍,全都合并到result中去
for (var i = 0, l = arguments.length; i < l; i++) {
forEach(arguments[i], assignValue);
}
return result;
}
extend
extend 其实就是把 b 对象上的属性挨个覆盖在了 a 对象上。但是属性如果为函数,则函数的 this 指向却指向 thisArg,实现的很巧妙:
/**
* 使用对象 b 的属性来扩展对象 a(修改了对象a本身)。
*
* @param {Object} a 要被扩展的对象
* @param {Object} b 扩展属性的来源对象
* @param {Object} thisArg 要绑定的this指向
* @return {Object} 修改后的a对象
*/
function extend(a, b, thisArg) {
forEach(b, function assignValue(val, key) {
// 如果指定了this指向,并且此属性为函数,则重新绑定this指向
if (thisArg && typeof val === 'function') {
a[key] = bind(val, thisArg);
} else {
a[key] = val;
}
});
return a;
}
stripBOM
这里的这个 BOM 不是 Browser Object Model(文档对象模型),而是 Byte Order Mark(字节顺序标记),Unicode标准 允许 UTF8 中有 BOM ,但是 UTF8 中已经不必需并且不建议使用,对 UTF8 已经毫无意义,所以删去。
/**
* 删除Byte Order Mark。捕获 EF BB BF(UTF-8 BOM)
* 主要是处理编码问题
*
* @param {string} content 带有 BOM 的内容
* @return {string} 删除 BOM 后的内容
*/
function stripBOM(content) {
if (content.charCodeAt(0) === 0xfeff) {
content = content.slice(1);
}
return content;
}
/helpers
上篇已经分析过,/helps 目录中包含如下这些文件:
└─helpers
bind.js
buildURL.js
combineURLs.js
cookies.js
deprecatedMethod.js
isAbsoluteURL.js
isAxiosError.js
isURLSameOrigin.js
normalizeHeaderName.js
parseHeaders.js
README.md
spread.js
validator.js
在 README.md 中,介绍了该目录的作用:helpers/ 中的模块是通用模块,不特定于 axios 的内部专门情况。这些模块理论上可以发布到 npm 并由其他模块或应用程序使用。通用模块的一些示例如下:
- 浏览器
polyfills cookie管理- 解析
HTTP请求头
bind.js
bind 方法主要就是用闭包和 Function.prototype.apply 方法实现了 this 指向的转换,写法非常复古:
/**
* @description: 修改fn的this指向为thisArg
* @param {Function} fn
* @param {Object} thisArg
* @return {Function} 返回修改了this指向的fn
*/
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];
}
// wrap函数修改fn的指向为thisArg
return fn.apply(thisArg, args);
};
};
buildURL
这个方法就是 Axios 中如何处理 params 参数的那一部分,看完之后很真切的解答了我的诸多开发过程中的小疑惑,包括经过 axios 处理的 url,什么字符会被转义?为什么数组参数会在后面加一个“[]”?如果 url 中结尾已经有“?”,会不会变成两个问号?
var utils = require('./../utils');
function encode(val) {
// 正常encodeURIComponent不转义的字符: A-Z a-z 0-9 - _ . ! ~ * ' ( )
// 先转义val,再把:$,+[]这几个字符解码回来
// 所以最后A-Z a-z 0-9 - _ . ! ~ * ' ( ) : $ , + [ ]这几个字符不转义,其他都转义
return encodeURIComponent(val)
.replace(/%3A/gi, ':')
.replace(/%24/g, '$')
.replace(/%2C/gi, ',')
.replace(/%20/g, '+')
.replace(/%5B/gi, '[')
.replace(/%5D/gi, ']');
}
/**
* 通过把params加到url的最后面来创建完整url
*
* @param {string} url url的主机名 (例如 http://www.google.com)
* @param {object} [params] 要添加的参数
* @returns {string} 格式化后的参数 http://www.google.com?a=1&b=2
*/
module.exports = function buildURL(url, params, paramsSerializer) {
// 没有参数就直接返回
/*eslint no-param-reassign:0*/
if (!params) {
return url;
}
var serializedParams;
// 如果有params序列方法,就执行下然后返回
if (paramsSerializer) {
serializedParams = paramsSerializer(params);
// 如果是一个URLSearchParams对象,就返回toString()的结果
} else if (utils.isURLSearchParams(params)) {
serializedParams = params.toString();
// 否则就进行普通序列化
} else {
var parts = [];
utils.forEach(params, function serialize(val, key) {
// 值为null或者undefined时,就不添加
if (val === null || typeof val === 'undefined') {
return;
}
// 值如果是个数组就给键封一层[]
if (utils.isArray(val)) {
key = key + '[]';
// 值如果不是数组,就包装成数组
} else {
val = [val];
}
utils.forEach(val, function parseValue(v) {
// 如果是date对象,就变为YYYY-MM-DDTHH:mm:ss.sssZ格式
if (utils.isDate(v)) {
v = v.toISOString();
// 如果是个对象,就stringify
} else if (utils.isObject(v)) {
v = JSON.stringify(v);
}
// 最后把parts中的值变为key=value格式
parts.push(encode(key) + '=' + encode(v));
});
});
// 最后封装为key1=value1&key2=value2的格式
serializedParams = parts.join('&');
}
if (serializedParams) {
// 从url中提取出#之前的部分
var hashmarkIndex = url.indexOf('#');
if (hashmarkIndex !== -1) {
url = url.slice(0, hashmarkIndex);
}
// 如果之前没有params就加个? 如果有params就加个&
url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams;
}
return url;
};
cookies
这个方法利用 IFFE 实际返回了一个对象,对象中 write、read 和 remove 方法,可以在浏览器环境中对 cookie 进行增删查改。这个方法对非浏览器环境做了兼容,在非浏览器环境中会执行一个空函数。
var utils = require('./../utils');
module.exports =
// 先判断是不是标准的浏览器环境
utils.isStandardBrowserEnv()
? // 标准的浏览器环境支持 document.cookie
(function standardBrowserEnv() {
return {
// 把cookie的字段一个个写进去
write: function write(name, value, expires, path, domain, secure) {
var cookie = [];
cookie.push(name + '=' + encodeURIComponent(value));
if (utils.isNumber(expires)) {
cookie.push('expires=' + new Date(expires).toGMTString());
}
if (utils.isString(path)) {
cookie.push('path=' + path);
}
if (utils.isString(domain)) {
cookie.push('domain=' + domain);
}
if (secure === true) {
cookie.push('secure');
}
document.cookie = cookie.join('; ');
},
// 通过正则来读取cookie的值
read: function read(name) {
var match = document.cookie.match(
new RegExp('(^|;\\s*)(' + name + ')=([^;]*)')
);
return match ? decodeURIComponent(match[3]) : null;
},
// 通过设置过期时间来移除cookie
remove: function remove(name) {
this.write(name, '', Date.now() - 86400000);
},
};
})()
: // 非标准的浏览器环境(web workers, react-native)不支持 document.cookie
(function nonStandardBrowserEnv() {
return {
write: function write() {},
read: function read() {
return null;
},
remove: function remove() {},
};
})();
有了这个方法,下次写 cookies 时可以直接从 axios 中引入,不必再专门引入复杂的 cookies 管理库了:
import cookies from 'axios/lib/helpers/cookies.js';
cookies.write(name, value, expires, path, domain, secure);
cookies.remove(name);
cookies.read(name);
deprecatedMethod
这段代码很简单,就是有一些 api 可能会被废弃掉,给用户警告下。
/**
* 警告开发人员:正在使用的方法已被弃用。
*
* @param {string} method 被遗弃的方法
* @param {string} [instead] 替换的新方法
* @param {string} [docs] 更多细节的文档地址
*/
module.exports = function deprecatedMethod(method, instead, docs) {
try {
console.warn(
'DEPRECATED method `' +
method +
'`.' +
(instead ? ' Use `' + instead + '` instead.' : '') +
' This method will be removed in a future release.'
);
if (docs) {
console.warn('For more information about usage see ' + docs);
}
} catch (e) {
/* Ignore */
}
};
isAbsoluteURL
判断 url 是否为绝对路径,判断的依据来源于RFC 3986 标准,根据是否以 <scheme>:// 或 // 开头来判断。
/**
* 判断给定的地址是否为绝对url
*
* @param {string} url 要检查的url
* @returns {boolean} 如果为绝对url返回true,否则返回false
*/
module.exports = function isAbsoluteURL(url) {
// 如果 URL 以“<scheme>://”或“//”开头,则该 URL 被视为绝对url。
// RFC 3986 将 scheme 名称定义为以字母开头且后跟字母、数字、加号、句点或连字符的任意组合的字符序列。
return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url);
};
isAxiosError
判断一个 Error 对象是否为 Axios 抛出,所有由 Axios 抛出的错误都有一个 isAxiosError 标识。
/**
* 判断error是否为Axios抛出的
*
* @param {*} payload 要检测的error
* @returns {boolean} 如果error为Axios抛出的则返回true,否则返回false
*/
module.exports = function isAxiosError(payload) {
// 通过isAxiosError来判断
return typeof payload === 'object' && payload.isAxiosError === true;
};
isURLSameOrigin
判断 location 和给定的 url 是否同源。在这里借助了浏览器的 a 标签进行 url 解析,然后判断协议、主机和端口是否相同。
var utils = require('./../utils');
module.exports = utils.isStandardBrowserEnv()
? // 标准浏览器完全支持给定URL与当前URL是否同源的检测
(function standardBrowserEnv() {
// 判断是否为IE浏览器
var msie = /(msie|trident)/i.test(navigator.userAgent);
var urlParsingNode = document.createElement('a');
var originURL;
/**
* 解析一个URL,将其分解为各个部分
*
* @param {String} url 要解析的URL
* @returns {Object} 返回各个部分组成的对象
*/
function resolveURL(url) {
var href = url;
if (msie) {
// IE浏览器需要设置两次才能标准化属性
urlParsingNode.setAttribute('href', href);
href = urlParsingNode.href;
}
urlParsingNode.setAttribute('href', href);
// urlParsingNode 提供了 UrlUtils 接口 - http://url.spec.whatwg.org/#urlutils
return {
href: urlParsingNode.href,
protocol: urlParsingNode.protocol
? urlParsingNode.protocol.replace(/:$/, '')
: '',
host: urlParsingNode.host,
search: urlParsingNode.search
? urlParsingNode.search.replace(/^\?/, '')
: '',
hash: urlParsingNode.hash
? urlParsingNode.hash.replace(/^#/, '')
: '',
hostname: urlParsingNode.hostname,
port: urlParsingNode.port,
pathname:
urlParsingNode.pathname.charAt(0) === '/'
? urlParsingNode.pathname
: '/' + urlParsingNode.pathname,
};
}
// 最后获得的location的各个部分组成的对象
originURL = resolveURL(window.location.href);
/**
* 判断URL与location是否同源
*
* @param {String} requestURL 要检查的url
* @returns {boolean} 如果同源则返回true,否则返回false
*/
return function isURLSameOrigin(requestURL) {
var parsed = utils.isString(requestURL)
? resolveURL(requestURL)
: requestURL;
// protocol、hostname、port都相等才是同源
return (
parsed.protocol === originURL.protocol &&
parsed.host === originURL.host
);
};
})()
: // 非标准的浏览器环境(web workers, react-native)都默认为同源
(function nonStandardBrowserEnv() {
return function isURLSameOrigin() {
return true;
};
})();
normalizeHeaderName
该方法给定了一个标准的 key,并且检查 headers 中对应的 key,如果不标准就替换。
var utils = require('../utils');
/**
* @description: 标准化报文头部信息
* @param {Object} headers 请求头对象
* @param {String} normalizedName 标准化的请求头key
*/
module.exports = function normalizeHeaderName(headers, normalizedName) {
utils.forEach(headers, function processHeader(value, name) {
// 当请求头的key大小写不标准时,修改为标准的
if (
name !== normalizedName &&
name.toUpperCase() === normalizedName.toUpperCase()
) {
headers[normalizedName] = value;
delete headers[name];
}
});
};
parseHeaders
该方法通过换行符 \n 将 headers 进行分解,转化为一个对象。
注意:有些
key重复了就忽略掉,有些key重复了就追加
var utils = require('./../utils');
// 下面数组中的header,如果出现了重复,就忽略掉
// c.f. https://nodejs.org/api/http.html#http_message_headers
var ignoreDuplicateOf = [
'age',
'authorization',
'content-length',
'content-type',
'etag',
'expires',
'from',
'host',
'if-modified-since',
'if-unmodified-since',
'last-modified',
'location',
'max-forwards',
'proxy-authorization',
'referer',
'retry-after',
'user-agent',
];
/**
* 把报文头字符串解析为对象
*
* ```
* Date: Wed, 27 Aug 2014 08:58:49 GMT
* Content-Type: application/json
* Connection: keep-alive
* Transfer-Encoding: chunked
* ```
*
* @param {String} headers 需要解析的报文头字符串
* @returns {Object} 解析后的对象
*/
module.exports = function parseHeaders(headers) {
var parsed = {};
var key;
var val;
var i;
// 没有header就返回空对象
if (!headers) {
return parsed;
}
// headers字符串首先通过换行符来分割为数组
utils.forEach(headers.split('\n'), function parser(line) {
// 拿到每个header的键和值
i = line.indexOf(':');
key = utils.trim(line.substr(0, i)).toLowerCase();
val = utils.trim(line.substr(i + 1));
if (key) {
// 如果key在“重复则忽略”的名单中,并且重复了,就忽略掉
if (parsed[key] && ignoreDuplicateOf.indexOf(key) >= 0) {
return;
}
// 对“set-cookie”进行专门处理
if (key === 'set-cookie') {
parsed[key] = (parsed[key] ? parsed[key] : []).concat([val]);
} else {
// 普通的header,在值字符串后面追加重复的值
parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
}
}
});
return parsed;
};
spread
将数组形式的参数按次序给回调函数传参并且调用。
/**
*
* 用于调用函数和扩展参数数组的语法糖。
*
* 常见的用法是`Function.prototype.apply`。
*
* ```js
* function f(x, y, z) {}
* var args = [1, 2, 3];
* f.apply(null, args);
* ```
*
* 使用 `spread` 来重写上例
*
* ```js
* spread(function(x, y, z) {})([1, 2, 3]);
* ```
*
* @param {Function} callback
* @returns {Function}
*/
module.exports = function spread(callback) {
return function wrap(arr) {
return callback.apply(null, arr);
};
};
validator
这个文件主要是返回了三个方法:
isOlderVersion:判断Axios的版本相对大小assertOptions:断言对象上的各个属性类型validators:各种JS类型的校验器
// 引入package.json(package.json其实也就是个普通的json)
var pkg = require('./../../package.json');
// validators是个类型校验器组成的对象
var validators = {};
// eslint-disable-next-line func-names
['object', 'boolean', 'number', 'function', 'string', 'symbol'].forEach(
function (type, i) {
validators[type] = function validator(thing) {
return typeof thing === type || 'a' + (i < 1 ? 'n ' : ' ') + type;
};
}
);
var deprecatedWarnings = {};
// 当前版本组成的数组
var currentVerArr = pkg.version.split('.');
/**
* 比较 package.json 的版本
* @param {string} version
* @param {string?} thanVersion 被比较的版本
* @returns {boolean}
*/
function isOlderVersion(version, thanVersion) {
var pkgVersionArr = thanVersion ? thanVersion.split('.') : currentVerArr;
var destVer = version.split('.');
// 通过比较每个位置的版本来确定是否version为thanVersion的老版本
for (var i = 0; i < 3; i++) {
if (pkgVersionArr[i] > destVer[i]) {
return true;
} else if (pkgVersionArr[i] < destVer[i]) {
return false;
}
}
return false;
}
/**
* 过渡的选项的校验器,整个方法是为了提醒用户老版本的某些选项被遗弃
* @param {function|boolean?} validator
* @param {string?} version
* @param {string} message
* @returns {function}
*/
validators.transitional = function transitional(validator, version, message) {
var isDeprecated = version && isOlderVersion(version);
// 格式化信息,
function formatMessage(opt, desc) {
return (
'[Axios v' +
pkg.version +
"] Transitional option '" +
opt +
"'" +
desc +
(message ? '. ' + message : '')
);
}
// eslint-disable-next-line func-names
return function (value, opt, opts) {
if (validator === false) {
throw new Error(formatMessage(opt, ' has been removed in ' + version));
}
if (isDeprecated && !deprecatedWarnings[opt]) {
deprecatedWarnings[opt] = true;
// eslint-disable-next-line no-console
console.warn(
formatMessage(
opt,
' has been deprecated since v' +
version +
' and will be removed in the near future'
)
);
}
return validator ? validator(value, opt, opts) : true;
};
};
/**
* 断言对象的属性类型
* @param {object} options
* @param {object} schema
* @param {boolean?} allowUnknown
*/
function assertOptions(options, schema, allowUnknown) {
// options不是一个对象时,直接报错
if (typeof options !== 'object') {
throw new TypeError('options must be an object');
}
// 挨个校验选项的类型是否符合要求
var keys = Object.keys(options);
var i = keys.length;
while (i-- > 0) {
var opt = keys[i];
var validator = schema[opt];
if (validator) {
var value = options[opt];
var result = value === undefined || validator(value, opt, options);
if (result !== true) {
// 类型不符合要求时直接报错
throw new TypeError('option ' + opt + ' must be ' + result);
}
continue;
}
// 不允许未知选项时,直接报错
if (allowUnknown !== true) {
throw Error('Unknown option ' + opt);
}
}
}
module.exports = {
isOlderVersion: isOlderVersion,
assertOptions: assertOptions,
validators: validators,
};
可以发现,虽然官方说 /helpers 中的方法与 Axios 本身不耦合,但是像 validator.js、isAxiosError.js 仍然是强耦合的。
总结
Axios 项目的 dependencies 中没有任何依赖,各种基础工具方法都是自己手动实现的。很明显,Axios 长久以来稳定性离不开这些工具方法的支持,所以这些基础的工具方法实现是很值得学习的。
下一篇 Axios 源码解析(四):核心工具方法(1)来解析 /core 目录下的文件。