0%

Axios源码解析(二):通用工具方法

上篇 Axios 源码解析(一):模块分解Axios 工程的结构进行了分解,下面来解析通用工具方法部分的源码,包括 utils.js/helpers 目录。

https://github.com/MageeLin/axios-source-code-analysis 中的analysis分支可以看到当前已解析完的文件。

utils.js

utils.js 中包含的方法如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 毕竟是个久经考验的库,所以值得学习下如何手动稳定实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
// 引入绑定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

trimutils.js 中的第一个不以 is 开头的方法。作用很简单,和 String.prototype.trim() 的功能相同,使用正则表达式匹配,将前后的空白剔除掉:

1
2
3
4
5
6
7
8
9
/**
* 修剪字符串前后的空白
*
* @param {String} str 要修剪的字符串
* @returns {String} 去除前后空白后的字符串
*/
function trim(str) {
return str.replace(/^\s*/, '').replace(/\s*$/, '');
}

forEach

forEach 是项目中大量使用的一个方法。它既可以迭代纯对象,也可以迭代数组,fn 参数是迭代过程中的回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 迭代数组或对象,对每一子项都执行一个回调函数
*
* 如果 `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 来实现的可变参数,使用递归来将对象进行了深层次拆分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 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,实现的很巧妙:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 使用对象 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 已经毫无意义,所以删去。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 删除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 目录中包含如下这些文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
└─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 指向的转换,写法非常复古:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @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 中结尾已经有“?”,会不会变成两个问号?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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 实际返回了一个对象,对象中 writereadremove 方法,可以在浏览器环境中对 cookie 进行增删查改。这个方法对非浏览器环境做了兼容,在非浏览器环境中会执行一个空函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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 管理库了:

1
2
3
4
5
import cookies from 'axios/lib/helpers/cookies.js';

cookies.write(name, value, expires, path, domain, secure);
cookies.remove(name);
cookies.read(name);

deprecatedMethod

这段代码很简单,就是有一些 api 可能会被废弃掉,给用户警告下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 警告开发人员:正在使用的方法已被弃用。
*
* @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>://// 开头来判断。

1
2
3
4
5
6
7
8
9
10
11
/**
* 判断给定的地址是否为绝对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 标识。

1
2
3
4
5
6
7
8
9
10
/**
* 判断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 解析,然后判断协议、主机和端口是否相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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,如果不标准就替换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

该方法通过换行符 \nheaders 进行分解,转化为一个对象。

注意:有些 key 重复了就忽略掉,有些 key 重复了就追加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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

将数组形式的参数按次序给回调函数传参并且调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
*
* 用于调用函数和扩展参数数组的语法糖。
*
* 常见的用法是`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 类型的校验器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// 引入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.jsisAxiosError.js 仍然是强耦合的。

总结

Axios 项目的 dependencies 中没有任何依赖,各种基础工具方法都是自己手动实现的。很明显,Axios 长久以来稳定性离不开这些工具方法的支持,所以这些基础的工具方法实现是很值得学习的。

下一篇 Axios 源码解析(三):适配器来解析 /adapters 目录下的文件。

👆 全文结束,棒槌时间到 👇