Better

Ethan的博客,欢迎访问交流

从 axios 拾得平平无奇一工具箱

工作中排查 axios 源码时,发现一个平平无奇的工具箱,responseType 高级类型,拦截器的执行顺序。

axios 做了啥

看 axios 的源码不是很多,顺带就过了一下,对 axios 帮助我们做了什么,多一些了解(仅关键部分哈)

  • 适配浏览器端和 node 端
  • 根据 requestData 类型,自动设置 Context-Type,基本上无需开发者手动关心
    • 有趣的一点,判断 requestData 是 FormData 时,会删除 Context-Type,让浏览器去设置它
  • 完善的 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 读写的工具箱吧

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


留言