上篇 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 var bind = require ('./helpers/bind' );var toString = Object .prototype.toString;function isArray (val ) { return toString.call(val) === '[object Array]' ; } function isUndefined (val ) { return typeof val === 'undefined' ; } function isBuffer (val ) { return ( val !== null && !isUndefined(val) && val.constructor !== null && !isUndefined(val.constructor) && typeof val.constructor.isBuffer === 'function' && val.constructor.isBuffer(val) ); } function isArrayBuffer (val ) { return toString.call(val) === '[object ArrayBuffer]' ; } function isFormData (val ) { return typeof FormData !== 'undefined' && val instanceof FormData; } 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; } function isString (val ) { return typeof val === 'string' ; } function isNumber (val ) { return typeof val === 'number' ; } function isObject (val ) { return val !== null && typeof val === 'object' ; } function isPlainObject (val ) { if (toString.call(val) !== '[object Object]' ) { return false ; } var prototype = Object .getPrototypeOf(val); return prototype === null || prototype === Object .prototype; } function isDate (val ) { return toString.call(val) === '[object Date]' ; } function isFile (val ) { return toString.call(val) === '[object File]' ; } function isBlob (val ) { return toString.call(val) === '[object Blob]' ; } function isFunction (val ) { return toString.call(val) === '[object Function]' ; } function isStream (val ) { return isObject(val) && isFunction(val.pipe); } function isURLSearchParams (val ) { return ( typeof URLSearchParams !== 'undefined' && val instanceof URLSearchParams ); } 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()
的功能相同,使用正则表达式匹配,将前后的空白剔除掉:
1 2 3 4 5 6 7 8 9 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 function forEach (obj, fn ) { if (obj === null || typeof obj === 'undefined' ) { return ; } if (typeof obj !== 'object' ) { 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 function merge (/* obj1, obj2, obj3, ... */ ) { var 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; } } 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 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; }
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 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 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); }; };
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 ) { return encodeURIComponent (val) .replace(/%3A/gi , ':' ) .replace(/%24/g , '$' ) .replace(/%2C/gi , ',' ) .replace(/%20/g , '+' ) .replace(/%5B/gi , '[' ) .replace(/%5D/gi , ']' ); } module .exports = function buildURL (url, params, paramsSerializer ) { if (!params) { return url; } var serializedParams; if (paramsSerializer) { serializedParams = paramsSerializer(params); } else if (utils.isURLSearchParams(params)) { serializedParams = params.toString(); } else { var parts = []; utils.forEach(params, function serialize (val, key ) { if (val === null || typeof val === 'undefined' ) { return ; } if (utils.isArray(val)) { key = key + '[]' ; } else { val = [val]; } utils.forEach(val, function parseValue (v ) { if (utils.isDate(v)) { v = v.toISOString(); } else if (utils.isObject(v)) { v = JSON .stringify(v); } parts.push(encode(key) + '=' + encode(v)); }); }); serializedParams = parts.join('&' ); } if (serializedParams) { var hashmarkIndex = url.indexOf('#' ); if (hashmarkIndex !== -1 ) { url = url.slice(0 , hashmarkIndex); } url += (url.indexOf('?' ) === -1 ? '?' : '&' ) + serializedParams; } return url; };
cookies 这个方法利用 IFFE
实际返回了一个对象,对象中 write
、read
和 remove
方法,可以在浏览器环境中对 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() ? (function standardBrowserEnv ( ) { return { 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('; ' ); }, read: function read (name ) { var match = document .cookie.match( new RegExp ('(^|;\\s*)(' + name + ')=([^;]*)' ) ); return match ? decodeURIComponent (match[3 ]) : null ; }, remove: function remove (name ) { this .write(name, '' , Date .now() - 86400000 ); }, }; })() : (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 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) { } };
isAbsoluteURL 判断 url
是否为绝对路径,判断的依据来源于RFC 3986 标准 ,根据是否以 <scheme>://
或 //
开头来判断。
1 2 3 4 5 6 7 8 9 10 11 module .exports = function isAbsoluteURL (url ) { return /^([a-z][a-z\d\+\-\.]*:)?\/\//i .test(url); };
isAxiosError 判断一个 Error
对象是否为 Axios
抛出,所有由 Axios
抛出的错误都有一个 isAxiosError
标识。
1 2 3 4 5 6 7 8 9 10 module .exports = function isAxiosError (payload ) { 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() ? (function standardBrowserEnv ( ) { var msie = /(msie|trident)/i .test(navigator.userAgent); var urlParsingNode = document .createElement('a' ); var originURL; function resolveURL (url ) { var href = url; if (msie) { urlParsingNode.setAttribute('href' , href); href = urlParsingNode.href; } urlParsingNode.setAttribute('href' , href); 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, }; } originURL = resolveURL(window .location.href); return function isURLSameOrigin (requestURL ) { var parsed = utils.isString(requestURL) ? resolveURL(requestURL) : requestURL; return ( parsed.protocol === originURL.protocol && parsed.host === originURL.host ); }; })() : (function nonStandardBrowserEnv ( ) { return function isURLSameOrigin ( ) { return true ; }; })();
该方法给定了一个标准的 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' );module .exports = function normalizeHeaderName (headers, normalizedName ) { utils.forEach(headers, function processHeader (value, name ) { if ( name !== normalizedName && name.toUpperCase() === normalizedName.toUpperCase() ) { headers[normalizedName] = value; delete headers[name]; } }); };
该方法通过换行符 \n
将 headers
进行分解,转化为一个对象。
注意:有些 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' );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' , ]; module .exports = function parseHeaders (headers ) { var parsed = {}; var key; var val; var i; if (!headers) { return parsed; } utils.forEach(headers.split('\n' ), function parser (line ) { i = line.indexOf(':' ); key = utils.trim(line.substr(0 , i)).toLowerCase(); val = utils.trim(line.substr(i + 1 )); if (key) { if (parsed[key] && ignoreDuplicateOf.indexOf(key) >= 0 ) { return ; } if (key === 'set-cookie' ) { parsed[key] = (parsed[key] ? parsed[key] : []).concat([val]); } else { 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 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 var pkg = require ('./../../package.json' );var validators = {};['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('.' );function isOlderVersion (version, thanVersion ) { var pkgVersionArr = thanVersion ? thanVersion.split('.' ) : currentVerArr; var destVer = version.split('.' ); for (var i = 0 ; i < 3 ; i++) { if (pkgVersionArr[i] > destVer[i]) { return true ; } else if (pkgVersionArr[i] < destVer[i]) { return false ; } } return false ; } 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 : '' ) ); } 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 ; 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 ; }; }; function assertOptions (options, schema, allowUnknown ) { 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 源码解析(三):适配器
来解析 /adapters
目录下的文件。