xhr-ajax-fetch-axios 发展历程

axios 是如何封装 HTTP 请求的?

概述

前端开发中,经常会遇到发送异步请求的场景。一个功能齐全的 HTTP 请求库可以大大降低我们的开发成本,提高开发效率。
axios 就是这样一个 HTTP 请求库,近年来非常热门。目前,它在 GitHub 上拥有超过 40,000 的 Star,许多权威人士都推荐使用它。
因此,我们有必要了解下 axios 是如何设计,以及如何实现 HTTP 请求库封装的。撰写本文时,axios 当前版本为 0.21.1,我们以该版本为例,来阅读和分析部分核心源代码。axios 的所有源文件都位于 lib 文件夹中,下文中提到的路径都是相对于 lib 来说的。
本文我们主要讨论:

  • 怎样使用 axios。
  • axios 的核心模块(请求、拦截器、撤销)是如何设计和实现的?
  • axios 的设计优点是什么?

    如何使用 axios

    要理解 axios 的设计,首先需要看一下如何使用 axios。我们举一个简单的例子来说明下 axios API 的使用

    发送请求

    1. axios({
    2. method:'get',
    3. url:'http://bit.ly/2mTM3nY',
    4. responseType:'stream'
    5. }).then(function(response) {
    6. response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'))
    7. })
    这是一个官方示例。从上面的代码中可以看到,axios 的用法与 jQuery 的 ajax 方法非常类似,两者都返回一个 Promise 对象(在这里也可以使用成功回调函数,但还是更推荐使用 Promiseawait),然后再进行后续操作。

    添加拦截器函数

    1. axios.interceptors.request.use(function (config) {
    2. return config;
    3. }, function (error) {
    4. return Promise.reject(error);
    5. });
    6. axios.interceptors.response.use(function (response) {
    7. return response;
    8. }, function (error) {
    9. return Promise.reject(error);
    10. });
    从上面的代码,我们可以知道:发送请求之前,我们可以对请求的配置参数(config)做处理;在请求得到响应之后,我们可以对返回数据做处理。当请求或响应失败时,我们还能指定对应的错误处理函数。

    撤销 HTTP 请求

    在开发与搜索相关的模块时,我们经常要频繁地发送数据查询请求。一般来说,当我们发送下一个请求时,需要撤销上个请求。因此,能撤销相关请求功能非常有用。axios 撤销请求的示例代码如下: ```javascript const CancelToken = axios.CancelToken; const source = CancelToken.source();

axios.get(‘/user/12345’, { cancelToken: source.token }).catch(function(thrown) { if (axios.isCancel(thrown)) { console.log(‘请求撤销了’, thrown.message); } else {

} });

axios.post(‘/user/12345’, { name: ‘新名字’ }, { cancelToken: source.token }).

source.cancel(‘用户撤销了请求’);

  1. 从上例中可以看到,在 axios 中,使用基于 `CancelToken` 的撤销请求方案。然而,该提案现已撤回,详情如 点这里。具体的撤销请求的实现方法,将在后面的源代码分析的中解释。
  2. <a name="x1Q4r"></a>
  3. ## axios 核心模块的设计和实现
  4. 通过上面的例子,我相信每个人都对 axios 的使用有一个大致的了解了。下面,我们将根据模块分析 axios 的设计和实现。下面的图片,是我在本文中会介绍到的源代码文件。如果您感兴趣,最好在阅读时克隆相关的代码,这能加深你对相关模块的理解。
  5. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/465727/1618138284781-bc8934f3-a41d-4d38-b94d-fb03248f2af4.png#height=499&id=IKPaV&margin=%5Bobject%20Object%5D&name=image.png&originHeight=499&originWidth=300&originalType=binary&ratio=1&size=146280&status=done&style=none&width=300)![image.png](https://cdn.nlark.com/yuque/0/2021/png/465727/1618139548377-3aa66c72-44de-48ec-9dbc-e0c2f92dc9e2.png#height=1254&id=EYmS4&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1254&originWidth=564&originalType=binary&ratio=1&size=136011&status=done&style=none&width=564)
  6. <a name="orUC7"></a>
  7. ### HTTP 请求模块
  8. 请求模块的代码放在了 `[core/dispatchRequest.js](https://github1s.com/axios/axios/blob/HEAD/lib/core/dispatchRequest.js)` 文件中,这里我只展示了一些关键代码来简单说明:
  9. ```javascript
  10. module.exports = function dispatchRequest(config) {
  11. throwIfCancellationRequested(config);
  12. // Ensure headers exist
  13. config.headers = config.headers || {};
  14. // Transform request data
  15. config.data = transformData(
  16. config.data,
  17. config.headers,
  18. config.transformRequest
  19. );
  20. // Flatten headers
  21. config.headers = utils.merge(
  22. config.headers.common || {},
  23. config.headers[config.method] || {},
  24. config.headers
  25. );
  26. utils.forEach(
  27. ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
  28. function cleanHeaderConfig(method) {
  29. delete config.headers[method];
  30. }
  31. );
  32. var adapter = config.adapter || defaults.adapter;
  33. return adapter(config).then(function onAdapterResolution(response) {
  34. throwIfCancellationRequested(config);
  35. // Transform response data
  36. response.data = transformData(
  37. response.data,
  38. response.headers,
  39. config.transformResponse
  40. );
  41. return response;
  42. }, function onAdapterRejection(reason) {
  43. if (!isCancel(reason)) {
  44. throwIfCancellationRequested(config);
  45. // Transform response data
  46. if (reason && reason.response) {
  47. reason.response.data = transformData(
  48. reason.response.data,
  49. reason.response.headers,
  50. config.transformResponse
  51. );
  52. }
  53. }
  54. return Promise.reject(reason);
  55. });
  56. };

上面的代码中,我们能够知道 dispatchRequest 方法是通过 config.adapter ,获得发送请求模块的。我们还可以通过传递,符合规范的适配器函数来替代原来的模块(一般来说,我们不会这样做,但它是一个松散耦合的扩展点)

[defaults.js](https://github1s.com/axios/axios/blob/HEAD/lib/defaults.js) 文件中,我们可以看到相关适配器的选择逻辑——根据当前容器的一些独特属性和构造函数,来确定使用哪个适配器。

  1. function getDefaultAdapter() {
  2. var adapter;
  3. if (typeof process !== 'undefined' &&
  4. Object.prototype.toString.call(process) === '[object process]') {
  5. // For node use HTTP adapter
  6. adapter = require('./adapters/http');
  7. } else if (typeof XMLHttpRequest !== 'undefined') {
  8. // For browsers use XHR adapter
  9. adapter = require('./adapters/xhr');
  10. }
  11. return adapter;
  12. }

axios 中的 XHR 模块相对简单,它是对 XMLHTTPRequest 对象的封装,这里我就不再解释了。有兴趣的同学,可以自己阅读源源码看看,源码位于 adapters/xhr.js 文件中。

拦截器模块

现在让我们看看 axios 是如何处理,请求和响应拦截器函数的。这就涉及到了 axios 中的统一接口 ——[request](https://github1s.com/axios/axios/blob/HEAD/lib/core/Axios.js) 函数。

  1. /**
  2. * Dispatch a request
  3. *
  4. * @param {Object} config The config specific for this request (merged with this.defaults)
  5. */
  6. Axios.prototype.request = function request(config) {
  7. var chain = [dispatchRequest, undefined];
  8. var promise = Promise.resolve(config);
  9. this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
  10. chain.unshift(interceptor.fulfilled, interceptor.rejected);
  11. });
  12. this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
  13. chain.push(interceptor.fulfilled, interceptor.rejected);
  14. });
  15. while (chain.length) {
  16. promise = promise.then(chain.shift(), chain.shift());
  17. }
  18. return promise;
  19. };

这个函数是 axios 发送请求的接口。因为函数实现代码相当长,这里我会简单地讨论相关设计思想:

  1. chain 是一个执行队列。队列的初始值是一个携带配置(config)参数的 Promise 对象。
  2. 在执行队列中,初始函数 dispatchRequest 用来发送请求,为了与 dispatchRequest对应,我们添加了一个 undefined。添加 undefined 的原因是需要给 Promise 提供成功和失败的回调函数,从下面代码里的 promise = promise.then(chain.shift(), chain.shift()); 我们就能看出来。因此,函数 dispatchRequestundefiend 可以看成是一对函数。
  3. 在执行队列 chain 中,发送请求的 dispatchReqeust 函数处于中间位置。它前面是请求拦截器,使用 unshift 方法插入;它后面是响应拦截器,使用 push 方法插入,在 dispatchRequest 之后。需要注意的是,这些函数都是成对的,也就是一次会插入两个。

浏览上面的 request 函数代码,我们大致知道了怎样使用拦截器。下一步,来看看怎样撤销一个 HTTP 请求。

撤销请求模块

与撤销请求相关的模块位于 Cancel/ 文件夹下,现在我们来看下相关核心代码。
首先,我们来看下基础 [Cancel](https://github1s.com/axios/axios/blob/HEAD/lib/cancel/Cancel.js) 类。它是一个用来记录撤销状态的类,具体代码如下:

  1. function Cancel(message) {
  2. this.message = message;
  3. }
  4. Cancel.prototype.toString = function toString() {
  5. return 'Cancel' + (this.message ? ': ' + this.message : '');
  6. };
  7. Cancel.prototype.__CANCEL__ = true;

使用 CancelToken 类时,需要向它传递一个 Promise 方法,用来实现 HTTP 请求的撤销,具体代码如下:

  1. function CancelToken(executor) {
  2. if (typeof executor !== 'function') {
  3. throw new TypeError('executor must be a function.');
  4. }
  5. var resolvePromise;
  6. this.promise = new Promise(function promiseExecutor(resolve) {
  7. resolvePromise = resolve;
  8. });
  9. var token = this;
  10. executor(function cancel(message) {
  11. if (token.reason) {
  12. return;
  13. }
  14. token.reason = new Cancel(message);
  15. resolvePromise(token.reason);
  16. });
  17. }
  18. CancelToken.source = function source() {
  19. var cancel;
  20. var token = new CancelToken(function executor(c) {
  21. cancel = c;
  22. });
  23. return {
  24. token: token,
  25. cancel: cancel
  26. };
  27. };

[adapters/xhr.js](https://github1s.com/axios/axios/blob/HEAD/lib/adapters/xhr.js) 文件中,撤销请求的地方是这样写的:

  1. if (config.cancelToken) {
  2. config.cancelToken.promise.then(function onCanceled(cancel) {
  3. if (!request) {
  4. return;
  5. }
  6. request.abort();
  7. reject(cancel);
  8. request = null;
  9. });
  10. }

通过上面的撤销 HTTP请求的例子,让我们简要地讨论一下相关的实现逻辑:

  1. 在需要撤销的请求中,调用 CancelToken 类的 source 方法类进行初始化,会得到一个包含 CancelToken 类实例 A 和 cancel 方法的对象。
  2. 当 source 方法正在返回实例 A 的时候,一个处于 pending 状态的 promise 对象初始化完成。在将实例 A 传递给 axios 之后,promise 就可以作为撤销请求的触发器使用了。
  3. 当调用通过 source 方法返回的 cancel 方法后,实例 A 中 promise 状态从 pending 变成 fulfilled,然后立即触发 then 回调函数。于是 axios 的撤销方法——request.abort() 被触发了。

    axios 这样设计的好处是什么?

    发送请求函数的处理逻辑

    如前几章所述,axios 不将用来发送请求的 dispatchRequest 函数看做一个特殊函数。实际上,dispatchRequest 会被放在队列的中间位置,以便保证队列处理的一致性和代码的可读性。

    适配器的处理逻辑

    在适配器的处理逻辑上,httpxhr 模块(一个是在 Node.js 中用来发送请求的,一个是在浏览器里用来发送请求的)并没有在 dispatchRequest 函数中使用,而是各自作为单独的模块,默认通过 defaults.js 文件中的配置方法引入的。因此,它不仅确保了两个模块之间的低耦合,而且还为将来的用户提供了定制请求发送模块的空间。

    撤销 HTTP 请求的逻辑

    在撤销 HTTP 请求的逻辑中,axios 设计使用 Promise 来作为触发器,将 resolve 函数暴露在外面,并在回调函数里使用。它不仅确保了内部逻辑的一致性,而且还确保了在需要撤销请求时,不需要直接更改相关类的样例数据,以避免在很大程度上入侵其他模块。

    总结

    本文详细介绍了 axios 的用法、设计思想和实现方法。
    在阅读之后,您可以了解 axios 的设计,并了解模块的封装和交互。
    本文只介绍了 axios 的核心模块,如果你对其他模块代码感兴趣,可以到 GitHub 上查看。