axios原理浅析--悄咪咪的带上umi-request

2,441 阅读9分钟

1. axios

今天我们来看一下axios里面一些方法大概的实现方式和执行过程,为什么会有这篇文章呢?因为在我平常开发的项目中都是使用axios来请求的,平常接触很多,同时也看了不少关于它的文章,但是还是解答不了我心中的一些疑惑。比如拦截器的原理啊、如何取消的请求啊等等。所以我就在空闲的时间去看它的源码是怎么实现的。想着肯定有好多人也不理解,所以就想分享一下自己的想法来帮助大家理解。

不啰嗦了,下面我们就进入正题吧。

1. 1 初始化

  • 一般我们通过下面的方法创建一个axios实例
import axios from 'axios';

// 创建一个axios实例
const instance = axios.create({
    baseURL: window.location.origin + '/xxx',
    timeout: 5000,
    headers: {
        post: { 'Content-Type': 'application/json;charset=utf-8' },
    },
});

下面就让我们来看一下,axios.create的时候都发生了什么事情吧。

  • 实现
// 有删减
// https://github.com/axios/axios/blob/94fc4ea7161b1e13c55df102e3177b53d26ef00e/lib/axios.js
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

// Expose Cancel & CancelToken
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');

// Expose all/spread
axios.all = function all(promises) {
  return Promise.all(promises);
};
axios.spread = require('./helpers/spread');
  • createInstance
/**
 * Create an instance of Axios
 *
 * @param {Object} defaultConfig The default config for the instance
 * @return {Axios} A new instance of Axios
 */
function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);

  // 将context的axios.prototype 复制到 instance
  utils.extend(instance, Axios.prototype, context);

  // Copy context to instance
  utils.extend(instance, context);

  return instance;
}

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  // 拦截器
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

function InterceptorManager() {
  this.handlers = [];
}
  • 工具函数
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);
  };
};

/**
 * 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;
}
  • axios.default的实现
    • 就是设置一些默认的配置
var defaults = {
  adapter: getDefaultAdapter(),

  transformRequest: [function transformRequest(data, headers) {
    // 格式化数据,后面会用到
    return data;
  }],

  transformResponse: [function transformResponse(data) {
    // 格式化数据,后面会用到
    return data;
  }],

  timeout: 0, // 单位事ms,0是未超时

  // 安全验证的一些header
  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',

  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300;
  }
};

defaults.headers = {
  common: {
    'Accept': 'application/json, text/plain, */*'
  }
};

module.exports = defaults;
  • getDefaultAdapter这里先需要重点记住
    • 判断使用哪种请求方式
      • 浏览器就用xhr
      • node中就用http
function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  }
  return adapter;
}

上面就是创建初始化的过程,下面我们来看一下发送请求的过程

1. 2 发送请求(get、post

axios.get('/user', {
  params: {
    ID: 12345
  }
})
  .then(function (response) {
  console.log(response);
})
  .catch(function (error) {
  console.log(error);
});

axios.post('/user', {
  firstName: 'Fred',
  lastName: 'Flintstone'
})
  .then(function (response) {
  console.log(response);
})
  .catch(function (error) {
  console.log(error);
});
  • 请求方法初始化
https://github.com/axios/axios/blob/94fc4ea7161b1e13c55df102e3177b53d26ef00e/lib/core/Axios.js
// Provide aliases for supported request methods
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: (config || {}).data
    }));
  };
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, data, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});
  • Axios.prototype.request方法
    • 只列出了异步的处理方法
/**
 * Dispatch a request
 *
 * @param {Object} config The config specific for this request (merged with this.defaults)
 */
Axios.prototype.request = function request(config) {
  // 对config的参数做了一些合并和处理
  // todo
  
  // 获取请求拦截器里面所有的数据
  var requestInterceptorChain = [];
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  
  // 获取响应拦截器里面所有的数据
  var responseInterceptorChain = [];
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });

  var promise;
  var chain = [dispatchRequest, undefined];
  // 拼接请求和响应的拦截器
  Array.prototype.unshift.apply(chain, requestInterceptorChain);
  chain.concat(responseInterceptorChain);
  // [...requestInterceptorChain(请求拦截器), dispatchRequest(请求), undefined, ...responseInterceptorChain(响应拦截器)]
  promise = Promise.resolve(config);
  // 按个执行
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }
  return promise;
};
  • dispatchRequest
module.exports = function dispatchRequest(config) {
  // 如果取消请求了就reject
  throwIfCancellationRequested(config);
  // 对数据进行处理和格式化
  // todo
  
  // 初始化配置时的getDefaultAdapter(),浏览器里也就是xhr
  var adapter = config.adapter || defaults.adapter; 
  
  // 发送请求,成功...,失败...
  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);

    // Transform response data
    response.data = transformData.call(
      config,
      response.data,
      response.headers,
      config.transformResponse
    );
    return response;
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);

      // Transform response data
      if (reason && reason.response) {
        reason.response.data = transformData.call(
          config,
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }
    return Promise.reject(reason);
  });
};

是否cancel

/**
 * Throws a `Cancel` if cancellation has been requested.
 */
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}

function isCancel(value) {
  return !!(value && value.__CANCEL__);
};
  • xhr adapter
    • 其实可以理解为我们平常自己实现一个xhr的过程,代码有点多没有删减,可以大概过一下
module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var requestData = config.data;
    var requestHeaders = config.headers;
    var responseType = config.responseType;

    var request = new XMLHttpRequest();

    var fullPath = buildFullPath(config.baseURL, config.url);
    
    // xhr.open
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);

    // Set the request timeout in MS
    request.timeout = config.timeout;
		
		// 当process结束的时候
    function onloadend() {
      if (!request) {
        return;
      }
      // Prepare the response
      var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
      var responseData = !responseType || responseType === 'text' ||  responseType === 'json' ?
        request.responseText : request.response;
      var response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config: config,
        request: request
      };
      settle(resolve, reject, response);
      // Clean up request
      request = null;
    }

    if ('onloadend' in request) {
      // Use onloadend if available
      request.onloadend = onloadend;
    } else {
      // Listen for ready state to emulate onloadend
      request.onreadystatechange = function handleLoad() {
        if (!request || request.readyState !== 4) {
          return;
        }
        // readystate handler is calling before onerror or ontimeout handlers,
        // so we should call onloadend on the next 'tick'
        setTimeout(onloadend);
      };
    }

    // Handle browser request cancellation (as opposed to a manual cancellation)
    request.onabort = function handleAbort() {
      if (!request) {
        return;
      }
      reject(createError('Request aborted', config, 'ECONNABORTED', request));
      // Clean up request
      request = null;
    };

    // Handle low level network errors
    request.onerror = function handleError() {
      // Real errors are hidden from us by the browser
      // onerror should only fire if it's a network error
      reject(createError('Network Error', config, null, request));

      // Clean up request
      request = null;
    };

    // Handle timeout
    request.ontimeout = function handleTimeout() {
      var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded';
      if (config.timeoutErrorMessage) {
        timeoutErrorMessage = config.timeoutErrorMessage;
      }
      reject(createError(
        timeoutErrorMessage,
        config,
        config.transitional && config.transitional.clarifyTimeoutError ? 'ETIMEDOUT' : 'ECONNABORTED',
        request));

      // Clean up request
      request = null;
    };

    // Add xsrf header
    // This is only done if running in a standard browser environment.
    // Specifically not if we're in a web worker, or react-native.
    if (utils.isStandardBrowserEnv()) {
      // Add xsrf header
      var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
        cookies.read(config.xsrfCookieName) :
        undefined;

      if (xsrfValue) {
        requestHeaders[config.xsrfHeaderName] = xsrfValue;
      }
    }
    
    // Add withCredentials to request if needed
    if (!utils.isUndefined(config.withCredentials)) {
      request.withCredentials = !!config.withCredentials;
    }

    // Add responseType to request if needed
    if (responseType && responseType !== 'json') {
      request.responseType = config.responseType;
    }

    // Handle progress if needed
    if (typeof config.onDownloadProgress === 'function') {
      request.addEventListener('progress', config.onDownloadProgress);
    }

    // Not all browsers support upload events
    if (typeof config.onUploadProgress === 'function' && request.upload) {
      request.upload.addEventListener('progress', config.onUploadProgress);
    }

    if (config.cancelToken) {
      // Handle cancellation
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
          return;
        }

        request.abort();
        reject(cancel);
        // Clean up request
        request = null;
      });
    }

    if (!requestData) {
      requestData = null;
    }

    // Send the request
    request.send(requestData);
  });
};
  • 我们都知道axios是支持防止xsrf攻击的,不懂得可以自己搜一下。下面我们就来看一下它是怎么实现的吧。
    • 防止xsrf
      • 其实就是从cookie里面获取到一个自定义的header,放到请求头里面
    • 流程
      • 如果是浏览器下,并且配置了携带cookie或者是同源的情况下,就去取值是'XSRF-TOKEN', name是xsrfCookieName的cookie
      • 如果能取到,那么就将’X-XSRF-TOKEN‘header设置为该值
    • 原理
      • 其实原理还是通过在请求头中添加自定义属性,
      • 属性值为后台返回的token(一般存放在cookie中)
      • 当后台服务器接受收到浏览器的请求之后,会拿到请求头中的token,进行对比。
// Add xsrf header
// This is only done if running in a standard browser environment.
// Specifically not if we're in a web worker, or react-native.
if (utils.isStandardBrowserEnv()) {
  // Add xsrf header
  var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
      cookies.read(config.xsrfCookieName) :
  undefined;

  if (xsrfValue) {
    requestHeaders[config.xsrfHeaderName] = xsrfValue;
  }
}

function isStandardBrowserEnv() {
  if (typeof navigator !== 'undefined' && 
      (
        navigator.product === 'ReactNative' ||
        navigator.product === 'NativeScript' ||
        navigator.product === 'NS'
      )
  ) {
    return false;
  }
  return (
    typeof window !== 'undefined' &&
    typeof document !== 'undefined'
  );
}
  • 上满用到的settle方法
    • 其实就是判断response的状态
      • 如果是个正常的状态码,例如200<=status<300 || status === 304返回就resolve
      • 否则就reject
/**
 * Resolve or reject a Promise based on response status.
 *
 * @param {Function} resolve A function that resolves the promise.
 * @param {Function} reject A function that rejects the promise.
 * @param {object} response The response.
 */
module.exports = function settle(resolve, reject, response) {
  var validateStatus = response.config.validateStatus;
  if (!response.status || !validateStatus || validateStatus(response.status)) {
    resolve(response);
  } else {
    reject(createError(
      'Request failed with status code ' + response.status,
      response.config,
      null,
      response.request,
      response
    ));
  }
};

1. 3 取消请求

  • 我们先来看一下使用
var CancelToken = axios.CancelToken;
var source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token // 配置标识
}).catch(function (thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error
  }
});

// 取消请求
source.cancel('Operation canceled by the user.');
  • CancelToken的实现
var Cancel = require('./Cancel');

function CancelToken(executor) {
  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    !!!重点
    // 将这promised的resolve交由外界去执行 
    resolvePromise = resolve;
  });

  var token = this;
  executor(function cancel(message) {
    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}

/**
 * 返回一个对象,该对象包含一个新的“CancelToken”和一个函数,调用该函数时,
 * 取消“CancelToken”。
 */
CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

// 其实
cancel = function cancel(message) {
  token.reason = new Cancel(message);
  resolvePromise(token.reason);
}
  • 在请求时,配置的参数
// 如果发现在配置的时候携带着cancelToken,那么就执行下面的函数
if (config.cancelToken) {
  !!! 这里非常重要
  // 待resolve的promise  
  // 当我们执行
  // source.cancel('Operation canceled by the user.');
  // 这句就会执行
  config.cancelToken.promise.then(function onCanceled(cancel) {
    if (!request) {
      return;
    }
    // 取消请求
    request.abort();
    reject(cancel);
    // Clean up request
    request = null;
  });
}
  • 示例
    • 其实你完全可以理解为这样的,当然这是一个很精简的实现
let resolvePromise = null;

let cancelToken = function(){
  return new Promise((resolve)=>{
    resolvePromise = resolve;
  });
};

cancelToken().then(()=>{
  // todo
  console.log('3s之后取消请求');
});

setTimeout(() => {
  resolvePromise();
}, 3000);

1. 4 拦截器

  • 使用
// 请求拦截器1
instance.interceptors.request.use(
  (config) => {
      // todo
      return config;
  }, 
  (err)=>{
    // todo
  }
);

// 请求拦截器2
instance.interceptors.request.use(
  (config) => {
      // todo
      return config;
  }, 
  (err)=>{
    // todo
  }
);

// 响应拦截器1
instance.interceptors.response.use(
    (response) => {
       // todo
    },
    (err) => {
       // todo
    },
);
// 响应拦截器2
instance.interceptors.response.use(
    (response) => {
       // todo
    },
    (err) => {
       // todo
    },
);
  • 其实上面的整个执行是这样的
var promise;
// 如果不是同步的
var chain = [dispatchRequest, undefined];
// 拼接请求和响应的拦截器
Array.prototype.unshift.apply(chain, requestInterceptorChain);
chain.concat(responseInterceptorChain);
promise = Promise.resolve(config);
while (chain.length) {
  promise = promise.then(chain.shift(), chain.shift());
}
return promise;
  • 用上面的示例,其实就是
[
    请求拦截器2的resolve, 请求拦截器2的reject, // 1组
    请求拦截器1的resolve, 请求拦截器1的reject, // 2组
    真实的请求, undefined,                  // 3组 
    响应拦截器1的resolve, 响应拦截器1的reject, // 4组
    响应拦截器2的resolve, 响应拦截器2的reject  // 5组
]
  • 示例图
    • 画的很糙,将就着看看吧 df8e359cc7104a8ca84b6518b3dd60d31487.png

1. 5实例方法

  • axios.all
  • axios.spread
    • 这两个是为了并发请求用的,在实际项目中我反正没用过,不过原理其实还是很简单的
  • axios.all
其实就是promise.all
axios.all = function all(promises) {
  return Promise.all(promises);
};
  • axios.spread
module.exports = function spread(callback) {
  return function wrap(arr) {
    return callback.apply(null, arr);
  };
};

这个大家可能不是很好理解,我一开始也理解了半天,不知道是干什么用的。然后看网上的例子大部分都是和axios.all一起用的

axios.all([getA(), getB()]).then(
  axios.spread(function(resA, resB) {
    console.log(resA.data);
    console.log(resB.data);
  })
);

axios.all的返回值是[getA()的结果, getB()的结果],然后把这个数据的返回值,传入axios.spread的回调里面去。因为axios.spread其实最后运行的是callback.apply(null, arr)。所以其实就是

axios.spread(function(getA()的结果, getB()的结果) {
  console.log(resA.data);
  console.log(resB.data);
})

说白了就是把axios.all返回的参数给展开了,传递给axios.spread的回调。

1. 6 其他

  • 我们从上面看到了axios里面用到了不少promise里面的方法,那么我们接下来就来看看promise是怎么实现的吧。
  • Promise.all
    • 里面的promise全都resolve之后,all才会resolve
    • 有一个rejectall就会reject
Promise.prototype.all = function(promises){
  const data = [];
  let index = 0; // 注意这里是自定义了一个索引,而没有用forEach里面的索引。
  return new Promise((resolve, reject)=>{
    promises.forEach((promise, i)=>{
      Promise.resolve(promise).then((res)=>{
        data[index ++] = res;
        if( index === promises.length ){
          resolve(data);
        }
      }, (err)=>{
        reject(err);
      })
    })
  })
}
Promise.prototype.race = function(promises){
  return new Promise((resolve, reject)=>{
    promises.forEach((promise)=>{
      Promise.resolve(promise).then((res)=>{
        resolve(res);
      }, (err)=>{
        reject(err);
      })
    })
  })
}
  • Promise.allSettled
    • all很类似,但是他是等所有的promise都执行完之后才会返回,而且是resolve
    • 对里面的promise根据不同的状态做了分类
Promise.prototype.allSettled = function(promises){
  const data = [];
  let index = 0;
  return new Promise((resolve, reject)=>{
    promises.forEach((promise, i)=>{
      Promise.resolve(promise).then((res)=>{
        data[index ++] = {status: 'fulfilled', value: res};
      }, (err)=>{
        data[index ++] = {status: 'rejected', reason: err};
      }).finally(()=>{
        if( index === promises.length ){
          resolve(data);
        }
      })
    })
  })
}

2. umi-request

其实以前我没有听说过这个库(好像使用umi创建的时候就请求默认使用的这个库),因为我自己在看axios源码的时候,偶尔看到了这个库,而且两个的实现很相似,再加上有很多axios之外的功能,所以在兴趣和好奇心的驱使下,打算也看一下这个库是怎么实现的。

2. 1 我们先来看一下,它跟其他库/方法的对比

  • fetchaxios的异同? 61f15c11-d6bd-419d-8cbc-6189b7d0a171.jpg 我们下面只介绍这个库的部分功能。比如:取消请求、拦截器、中间件。

2. 2 取消请求

  1. CancelToken
    • 和axios同理,只不过在判断取消的时候逻辑不同,抽成了一个方法
// If request options contain 'cancelToken', reject request when token has been canceled
export function cancel2Throw(opt) {
    // opt就是在配置的时候传入的参数
    // 下面的和axios的写法一致
    return new Promise((_, reject) => {
        if (opt.cancelToken) {
          opt.cancelToken.promise.then(cancel => {
            reject(cancel);
          });
        }
    });
}
  • 在真正发起请求的地方
    • 其实就是真正的请求、取消请求、超时(若有)一起raca,哪个最先执行完取哪个的值
export default function fetchMiddleware(ctx, next) {
  let response;
  // 超时处理、取消请求处理
  if (timeout > 0) {
    response = Promise.race([cancel2Throw(options, ctx), adapter(url, options), timeout2Throw(timeout, ctx.req)]);
  } else {
    response = Promise.race([cancel2Throw(options, ctx), adapter(url, options)]);
  }
}
  1. AbortController
  • 因为umi-request使用的是fetch请求的,所以取消需要借助其他的方法
    • 比如我们取消一个fetch请求
const controller = new AbortController();
const { signal } = controller;

fetch("http://xxx", { signal }).then(response => {
    console.log(`Request 1 is complete!`);
}).catch(e => {
    console.warn(`Fetch 1 error: ${e.message}`);
});

// Abort request
controller.abort();
  • 但是这个AbortController也是umi-request经过polyfill的。
import 'abort-controller/polyfill';

let AbortController = undefined;
let AbortSignal = undefined;

const g = window; // 有删减

if (g) {
  AbortController = typeof g.AbortController !== 'undefined' ? g.AbortController : AcAbortController;
  AbortSignal = typeof g.AbortSignal !== 'undefined' ? g.AbortSignal : AcAbortSignal;
}

export default AbortController;

export { AbortController, AbortSignal };

2.3 拦截器

  • 其实这个库里面同样有
    • 请求拦截器
    • 响应拦截器
  • 例如
// 拦截器
// request interceptor, change url or options.
request.interceptors.request.use((url, options) => {
  return {
    url: `${url}&interceptors=yes`,
    options: { ...options, interceptors: true },
  };
});

// 响应器
// handling error in response interceptor
request.interceptors.response.use(response => {
  const codeMaps = {
    502: '网关错误。',
    503: '服务不可用,服务器暂时过载或维护。',
    504: '网关超时。',
  };
  message.error(codeMaps[response.status]);
  return response;
});
  • 在看拦截器的实现之前,我们先看一下这个库也支持为请求添加前缀和后缀,来看一下是怎么实现的。
    • 其实添加前缀后缀也是一个请求拦截器
// 前后缀拦截
const addfix = (url, options = {}) => {
  const { prefix, suffix } = options;
  if (prefix) {
    url = `${prefix}${url}`;
  }
  if (suffix) {
    url = `${url}${suffix}`;
  }
  return {
    url,
    options,
  };
};

export default addfix;

就是这么简单的字符串拼接,下面让我们进入正题吧。

  • 拦截器的实现
  1. 挂载到实例上
    // 拦截器
umiInstance.interceptors = {
  request: {
    use: Core.requestUse.bind(coreInstance),
  },
  response: {
    use: Core.responseUse.bind(coreInstance),
  },
};
  1. 具体收集封装的类
import addfixInterceptor from './interceptor/addfix';

// core.js
class Core {
  constructor(initOptions) {
    this.onion = new Onion([]);
    this.instanceRequestInterceptors = [];
    this.instanceResponseInterceptors = [];
  }

  // 旧版拦截器为共享 全局的拦截器
  static requestInterceptors = [addfixInterceptor];
  static responseInterceptors = [];

  // 请求拦截器 默认 { global: true } 兼容旧版本拦截器
  static requestUse(handler, opt = { global: true }) {
    if (typeof handler !== 'function') throw new TypeError('Interceptor must be function!');
    if (opt.global) {
      Core.requestInterceptors.push(handler);
    } else {
      this.instanceRequestInterceptors.push(handler);
    }
  }

  // 响应拦截器 默认 { global: true } 兼容旧版本拦截器
  static responseUse(handler, opt = { global: true }) {
    if (typeof handler !== 'function') throw new TypeError('Interceptor must be function!');
    if (opt.global) {
      Core.responseInterceptors.push(handler);
    } else {
      this.instanceResponseInterceptors.push(handler);
    }
  }
}
  • 使用
    1. 在发起请求的时候,将响应拦截器包装,并传给请求拦截器当作参数。
    2. 请求拦截器的整个执行过程其实就是将url和options传给每一个拦截器,使用reduce循环执行。
class Core {
    request(url, options) {
        const { onion } = this;
        const obj = {
          req: { url, options },
          res: null,
          cache: this.mapCache,
          responseInterceptors: [...Core.responseInterceptors, ...this.instanceResponseInterceptors],
    };

    return new Promise((resolve, reject) => {
      this.dealRequestInterceptors(obj)
        .then(() => onion.execute(obj))
          .then(() => {
            resolve(obj.res);
          })
        });
    });
  }
  • 执行请求前拦截器
    • 可能有点绕,但仔细看还是很简单的,就是Array.prototype.reducer循环执行请求拦截器对urloptions处理
  dealRequestInterceptors(ctx) {
    const reducer = (p1, p2) =>
      p1.then((ret = {}) => {
        ctx.req.url = ret.url || ctx.req.url;
        ctx.req.options = ret.options || ctx.req.options;
        return p2(ctx.req.url, ctx.req.options);
      });
    const allInterceptors = [...Core.requestInterceptors, ...this.instanceRequestInterceptors];
    return allInterceptors.reduce(reducer, Promise.resolve()).then((ret = {}) => {
      ctx.req.url = ret.url || ctx.req.url;
      ctx.req.options = ret.options || ctx.req.options;
      return Promise.resolve();
    });
  }
}
  • 下面我们来看一下执行的方法onion.execute
    • Onion
      • 其实看名字了解的人应该就知道是什么意思了,对。就是洋葱模型,因为这个库是支持中间件的。
    • 我们先来介绍一下洋葱模型 f87ab212-18cb-4d9a-a628-7927d80632fa.png

类 koa 的洋葱机制,让开发者优雅地做请求前后的增强处理,支持创建实例、全局、内核中间件。

  • 实例中间件(默认):request.use(fn) 不同实例创建的中间件相互独立不影响;
  • 全局中间件 : request.use(fn, { global: true })全局中间件,不同实例共享全局中间件;
  • 内核中间件 :request.use(fn, { core: true })内核中间件,方便开发者拓展请求内核;
  • 优先级:全局中间件 > 内核中间件 其他的优先级和顺序请详看官方md
  • 下面我们接具体看一下Onion中干了什么吧
class Onion {
	// 有删减
    execute(params = null) {
        const fn = compose([
          ...this.middlewares, // 实例中间件
          ...this.defaultMiddlewares, // []
          ...Onion.globalMiddlewares, // 全局中间件
          ...Onion.coreMiddlewares, // 内核中间件
    ]);
    // 其实从上面的数组中也可以看到执行顺序 全局中间件 > 内核中间件
    return fn(params);
    // fn({
    //     req: { url, options },
    //     res: null,
    //     cache: this.mapCache,
    //     responseInterceptors: [...Core.responseInterceptors, ...this.instanceResponseInterceptors],
    // })
  }
}
// 初始化全局中间件
const globalMiddlewares = [simplePost, simpleGet, parseResponseMiddleware]; // 具体是什么后面在看
// 初始化内核中间件
const coreMiddlewares = [fetchMiddleware]; // 具体是什么后面在看

Onion.globalMiddlewares = globalMiddlewares;
Onion.defaultGlobalMiddlewaresLength = globalMiddlewares.length;
Onion.coreMiddlewares = coreMiddlewares;
Onion.defaultCoreMiddlewaresLength = coreMiddlewares.length;
  • 我们看一下上面的中间件都是处理什么的
    • fetchMiddleware真正发起请求的地方
    • simplePost对请求参数做处理,实现query简化、 post简化
    • simpleGet同上
    • parseResponseMiddleware格式化和解析response
  • 下面我们来看一下compose方法的实现,其实就是整个洋葱模型的执行过程
// 返回一个组合了所有插件的“插件” 洋葱模型的实现
export default function compose(middlewares) {
  const middlewaresLen = middlewares.length;
  // params = {
  //     req: { url, options },
  //     res: null,
  //     cache: this.mapCache,
  //     responseInterceptors: [...Core.responseInterceptors, ...this.instanceResponseInterceptors],
  // })
  return function wrapMiddlewares(params, next) {
    let index = -1;
    function dispatch(i) {
      if (i <= index) {
        return Promise.reject(new Error('next() should not be called multiple times in one middleware!'));
      }
      index = i;
      const fn = middlewares[i] || next;
      if (!fn) return Promise.resolve();
      try {
        // () => dispatch(i + 1) 这个其实就是next方法
        return Promise.resolve(fn(params, () => dispatch(i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }

    return dispatch(0);
  };
}
  • 最后在获取到响应之后,执行响应拦截器
export default function fetchMiddleware(ctx, next) { 
  const { req: { options = {}, url = '' } = {}, cache, responseInterceptors } = ctx;
  // 看这里,请求的方式
  const adapter = fetch;
  
  let response;
  // 超时处理、取消请求处理,上面有讲过
  if (timeout > 0) {
    response = Promise.race([cancel2Throw(options, ctx), adapter(url, options), timeout2Throw(timeout, ctx.req)]);
  } else {
    response = Promise.race([cancel2Throw(options, ctx), adapter(url, options)]);
  }
	
  // 兼容老版本 response.interceptor
  responseInterceptors.forEach(handler => {
    response = response.then(res => {
      // Fix multiple clones not working, issue: https://github.com/github/fetch/issues/504
      let clonedRes = typeof res.clone === 'function' ? res.clone() : res;
      return handler(clonedRes, options);
    });
  });
}

2.4 整体大概流程

  1. request.get('/api/v1/xxx?id=1') 发起请求
  2. 调用Core的request方法
  3. 执行请求拦截器,传入响应拦截器等
const obj = {
  req: { url, options },
  res: null,
  cache: this.mapCache,
  responseInterceptors: [...Core.responseInterceptors, ...this.instanceResponseInterceptors],
};
  1. 按顺序执行中间件
    • 实例中间件(自定义) -> 默认中间件(自定义,默认[]) -> 全局中间件 -> 内核中间件
      • 将上面的obj当作参数分别传给各个中间件
[
  ...this.middlewares, // 实例
  ...this.defaultMiddlewares, // 默认
  ...Onion.globalMiddlewares, // 全局
  ...Onion.coreMiddlewares, // 内核
]
  1. 执行完自定义的之后,执行全局中间件(自定义的和[simplePost, simpleGet, parseResponseMiddleware]
    1. 其实这里有个小的点,中间件执行的顺序是simplePost, simpleGet
      • 在simplePost里判断
if (['post', 'put', 'patch', 'delete'].indexOf(method.toLowerCase()) === -1) {
    return next();
}
  • 如果不是post会直接走向simpleGet,当然最后也会走向simpleGet
    • simplePost里主要判断有没有options.data,有就执行格式化等逻辑
    • simpleGet里主要判断有没有options.params,有就执行格式化等逻辑,否则不做任何格式化,原样返回
    • parseResponseMiddleware里面主要负责格式化response,里面的逻辑其实是等到下个中间件执行完之后才处理,也就是第6步执行之后,对返回结果进行处理
return next()
    .then(() => {
        // ct会传给所有的中间件,也就是上面的obj
        const { res = {}, req = {} } = ctx;
        // todo
    })
  1. 执行内核中间件fetchMiddleware,真正发起请求的地方
  2. fetchMiddleware里面发起请求并且判断是否cancel和执行响应拦截器
  3. 获取到值,并且返回给用户