工作中排查 axios 源码时,发现一个平平无奇的工具箱,responseType 高级类型,拦截器的执行顺序。
axios 做了啥
看 axios 的源码不是很多,顺带就过了一下,对 axios 帮助我们做了什么,多一些了解(仅关键部分哈)
- 适配浏览器端和 node 端
- 根据 requestData 类型,自动设置
Context-Type
,基本上无需开发者手动关心- 有趣的一点,判断 requestData 是 FormData 时,会删除
Context-Type
,让浏览器去设置它
- 有趣的一点,判断 requestData 是 FormData 时,会删除
- 完善的 responseType 支持('arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream')
- responseType 为 text 时,返回 request.responseText,否则返回 request.response 作为 responseData
- 事件的支持
- request 的 progress 事件
- request.upload 的 progress 事件(不是所有浏览器都支持)
- cancelToken 机制
- 完善的中间件机制
拦截器
涉及到源码中相关文件 Axios.js,InterceptorManager.js,InterceptorManager 是一个简单的类,具体执行逻辑在 Axios.js 中。在 Axios 的拦截器机制十分简单。直接看代码
// Hook up interceptors middleware
var chain = [dispatchRequest, undefined];
// config 指的是 AxiosRequestConfig
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);
});
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
解读如下
- dispatchRequest 可以理解成封装 request,返回一个 Promise
- 遍历 requestInterceptors,通过往数组头部插入的方式,添加到 chain 中,由此可知,先添加的 requestInterceptor 后执行
- 遍历 responseInterceptors,通过往数组尾部插入的方式,添加到 chain 中,由此可知,先添加的 responseInterceptor 先执行
项目中会使用拦截器对接口数据蛇形转骆驼,但如果是 dispatchRequest 内部 eject 了,则拦截器会执行不到,导致作为错误处理的 rejectHandle 中拿到的数据是未转换的
responseType
这里比较出乎我的意料,我翻遍了源码,只找到简单 responseType 相关的信息如下
var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
// and here
if (config.responseType) {
try {
request.responseType = config.responseType;
} catch (e) {
// Expected DOMException thrown by browsers not compatible XMLHttpRequest Level 2.
// But, this can be suppressed for 'json' type as it can be parsed by default 'transformResponse' function.
if (config.responseType !== 'json') {
throw e;
}
}
}
这里确实是我的盲区了, 查阅 mdn 发现,这个属性的目的是为了赋值给 request 对象的,有如下取值
- "":responseType 为空字符串时,采用默认类型 DOMString,与设置为 text 相同
- document:response 是一个 HTML Document 或 XML XMLDocument
- json:response 是一个 JavaScript 对象
- text:response 是一个以 DOMString 对象表示的文本
- arraybuffer:response 是一个包含二进制数据的 JavaScript ArrayBuffer
- blob:response 是一个包含二进制数据的 Blob 对象
- stream:mdn 竟然没有提到,仅提到一个 ms-stream,且仅受 IE 支持
工具箱-类型判断、merge、extend
包含丰富的类型判断,以及 forEach 升级版(支持迭代对象),merge 和 extend 等函数。直接看代码吧
'use strict';
// 这里的 bind 其实就是 Function.prototype.bind,不太懂为啥再写一个
var bind = require('./helpers/bind');
/*global toString:true*/
// utils is a library of generic helper functions non-specific to axios
var toString = Object.prototype.toString;
/**
* Determine if a value is an Array
*
* @param {Object} val The value to test
* @returns {boolean} True if value is an Array, otherwise false
*/
function isArray(val) {
return toString.call(val) === '[object Array]';
}
/**
* Determine if a value is undefined
*
* @param {Object} val The value to test
* @returns {boolean} True if the value is undefined, otherwise false
*/
function isUndefined(val) {
return typeof val === 'undefined';
}
/**
* Determine if a value is a Buffer
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a Buffer, otherwise false
*/
function isBuffer(val) {
return val !== null && !isUndefined(val) && val.constructor !== null && !isUndefined(val.constructor)
&& typeof val.constructor.isBuffer === 'function' && val.constructor.isBuffer(val);
}
/**
* Determine if a value is an ArrayBuffer
*
* @param {Object} val The value to test
* @returns {boolean} True if value is an ArrayBuffer, otherwise false
*/
function isArrayBuffer(val) {
return toString.call(val) === '[object ArrayBuffer]';
}
/**
* Determine if a value is a FormData
*
* @param {Object} val The value to test
* @returns {boolean} True if value is an FormData, otherwise false
*/
function isFormData(val) {
return (typeof FormData !== 'undefined') && (val instanceof FormData);
}
/**
* Determine if a value is a view on an ArrayBuffer
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a view on an ArrayBuffer, otherwise 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;
}
/**
* Determine if a value is a String
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a String, otherwise false
*/
function isString(val) {
return typeof val === 'string';
}
/**
* Determine if a value is a Number
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a Number, otherwise false
*/
function isNumber(val) {
return typeof val === 'number';
}
/**
* Determine if a value is an Object
*
* @param {Object} val The value to test
* @returns {boolean} True if value is an Object, otherwise false
*/
function isObject(val) {
return val !== null && typeof val === 'object';
}
/**
* Determine if a value is a plain Object
*
* @param {Object} val The value to test
* @return {boolean} True if value is a plain Object, otherwise false
*/
function isPlainObject(val) {
if (toString.call(val) !== '[object Object]') {
return false;
}
var prototype = Object.getPrototypeOf(val);
return prototype === null || prototype === Object.prototype;
}
/**
* Determine if a value is a Date
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a Date, otherwise false
*/
function isDate(val) {
return toString.call(val) === '[object Date]';
}
/**
* Determine if a value is a File
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a File, otherwise false
*/
function isFile(val) {
return toString.call(val) === '[object File]';
}
/**
* Determine if a value is a Blob
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a Blob, otherwise false
*/
function isBlob(val) {
return toString.call(val) === '[object Blob]';
}
/**
* Determine if a value is a Function
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a Function, otherwise false
*/
function isFunction(val) {
return toString.call(val) === '[object Function]';
}
/**
* Determine if a value is a Stream
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a Stream, otherwise false
*/
function isStream(val) {
return isObject(val) && isFunction(val.pipe);
}
/**
* Determine if a value is a URLSearchParams object
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a URLSearchParams object, otherwise false
*/
function isURLSearchParams(val) {
return typeof URLSearchParams !== 'undefined' && val instanceof URLSearchParams;
}
/**
* Trim excess whitespace off the beginning and end of a string
*
* @param {String} str The String to trim
* @returns {String} The String freed of excess whitespace
*/
function trim(str) {
return str.replace(/^\s*/, '').replace(/\s*$/, '');
}
/**
* Determine if we're running in a standard browser environment
*
* This allows axios to run in a web worker, and react-native.
* Both environments support XMLHttpRequest, but not fully standard globals.
*
* web workers:
* 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'
);
}
/**
* Iterate over an Array or an Object invoking a function for each item.
*
* If `obj` is an Array callback will be called passing
* the value, index, and complete array for each item.
*
* If 'obj' is an Object callback will be called passing
* the value, key, and complete object for each property.
*
* @param {Object|Array} obj The object to iterate
* @param {Function} fn The callback to invoke for each item
*/
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);
}
}
}
}
/**
* Accepts varargs expecting each argument to be an object, then
* immutably merges the properties of each object and returns result.
*
* When multiple objects contain the same key the later object in
* the arguments list will take precedence.
*
* @param {Object} obj1 Object to merge
* @returns {Object} Result of all merge properties
*/
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;
}
/**
* Extends object a by mutably adding to it the properties of object b.
*
* @param {Object} a The object to be extended
* @param {Object} b The object to copy properties from
* @param {Object} thisArg The object to bind function to
* @return {Object} The resulting value of 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;
}
/**
* Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
*
* @param {string} content with BOM
* @return {string} content value without BOM
*/
function stripBOM(content) {
if (content.charCodeAt(0) === 0xFEFF) {
content = content.slice(1);
}
return content;
}
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
};
工具箱-URL
源码中发现 axios 在处理 url 参数的时候,会使用 encodeURIComponent 进行编码,但他并不是直接使用,而是在 encodeURIComponent 的基础上对一些字符做了 replace 操作,目前没搞清楚原因,具体代码如下
function encode(val) {
return encodeURIComponent(val).
replace(/%3A/gi, ':').
replace(/%24/g, '$').
replace(/%2C/gi, ',').
replace(/%20/g, '+').
replace(/%5B/gi, '[').
replace(/%5D/gi, ']');
}
还要一个简单的函数 combineURLs,但也比较有趣,Axios 在针对 baseURL 和 relativeURL 时,会对前者最后的斜杆和后者最前面的斜杆做处理,不得不说,框架考虑的细呀
module.exports = function combineURLs(baseURL, relativeURL) {
return relativeURL
? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')
: baseURL;
};
判断是不是绝对路径
module.exports = function isAbsoluteURL(url) {
// A URL is considered absolute if it begins with "<scheme>://" or "//" (protocol-relative URL).
// RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed
// by any combination of letters, digits, plus, period, or hyphen.
return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url);
};
是不是同域
function isURLSameOrigin(requestURL) {
var parsed = (utils.isString(requestURL)) ? resolveURL(requestURL) : requestURL;
return (parsed.protocol === originURL.protocol &&
parsed.host === originURL.host);
};
工具箱-Cookie
在追加一个 Cookie 读写的工具箱吧
module.exports = (
utils.isStandardBrowserEnv() ?
// Standard browser envs support document.cookie
(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);
}
};
})() :
// Non standard browser env (web workers, react-native) lack needed support.
(function nonStandardBrowserEnv() {
return {
write: function write() {},
read: function read() { return null; },
remove: function remove() {}
};
})()
);
优化接口请求
request 优化
- 增强错误默认处理
- HTTP 状态码非 200 区间时走默认错误处理
- 请求体 success 为 false 时走默认错误处理
- 增加 skipErrorHandler 参数,针对不需要默认处理的接口情况
- 新增 requestConfig 参数
- 新增 loading 参数,可用于设置接口级别的 message.loading
- 新增 getResponse 参与,用于需要获取完整 response 的情况,必须某些信息来源请求头
- 新增 skipErrorHandler 用于不需要进行默认错误处理的情况
- 新增 axios.isCustomError 函数用于判断是不是由于 success 为 false 抛出的错误
- 中间件、拦截器
- 拆分多个拦截器,每个只做一件事
- 全局配置:axios 的全局配置竟然是失效的!!!
- 实例单独配置
- 缓存功能设计
- 缓存开关:useCache
- 缓存时间:ttl
- 缓存 key:url + params + method
- 最大缓存数:maxCache
- 自定义缓存:validateCache