如何实现一个HTTP请求库?——axios源码阅读与分析

开发 前端
在前端开发过程中,我们经常会遇到需要发送异步请求的情况。而使用一个功能齐全,接口完善的HTTP请求库,能够在很大程度上减少我们的开发成本,提高我们的开发效率。axios是一个在近些年来非常火的一个HTTP请求库,目前在GitHub中已经拥有了超过40K的star,受到了各位大佬的推荐。

[[238375]]

概述

在前端开发过程中,我们经常会遇到需要发送异步请求的情况。而使用一个功能齐全,接口完善的HTTP请求库,能够在很大程度上减少我们的开发成本,提高我们的开发效率。

axios是一个在近些年来非常火的一个HTTP请求库,目前在GitHub中已经拥有了超过40K的star,受到了各位大佬的推荐。

今天,我们就来看下,axios到底是如何设计的,其中又有哪些值得我们学习的地方。我在写这边文章时,axios的版本为0.18.0。我们就以这个版本的代码为例,来进行具体的源码阅读和分析。当前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. })  
  6.   .then(function(response) {  
  7.   response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'))  
  8. }); 

这是一个官方的API示例。从上面的代码中我们可以看到,axios的用法与jQuery的ajax很相似,都是通过返回一个Promise(也可以通过success的callback,不过建议使用Promise或者await)来继续后面的操作。

这个代码示例很简单,我就不过多赘述了,下面让我们来看下如何添加一个过滤器函数。

增加拦截器(Interceptors)函数

 

  1. // 增加一个请求拦截器,注意是2个函数,一个处理成功,一个处理失败,后面会说明这种情况的原因  
  2. axios.interceptors.request.use(function (config) { 
  3.     // 请求发送前处理  
  4.     return config;  
  5.   }, function (error) {  
  6.     // 请求错误后处理  
  7.     return Promise.reject(error);  
  8.   });  
  9.  
  10. // 增加一个响应拦截器  
  11. axios.interceptors.response.use(function (response) {  
  12.     // 针对响应数据进行处理  
  13.     return response;  
  14.   }, function (error) {  
  15.     // 响应错误后处理  
  16.     return Promise.reject(error);  
  17.   }); 

通过上面的示例我们可以知道:在请求发送前,我们可以针对请求的config参数进行数据处理;而在请求响应后,我们也能针对返回的数据进行特定的操作。同时,在请求失败和响应失败时,我们都可以进行特定的错误处理。

取消HTTP请求

在完成搜索相关的功能时,我们经常会需要频繁的发送请求来进行数据查询的情况。通常来说,我们在下一次请求发送时,就需要取消上一次请求。因此,取消请求相关的功能也是一个优点。axios取消请求的示例代码如下:

 

  1. const CancelToken = axios.CancelToken;  
  2. const source = CancelToken.source();  
  3. axios.get('/user/12345', {  
  4.   cancelToken: source.token  
  5. }).catch(function(thrown) {  
  6.   if (axios.isCancel(thrown)) {  
  7.     console.log('Request canceled', thrown.message);  
  8.   } else {  
  9.     // handle error  
  10.   }  
  11. });  
  12.  
  13. axios.post('/user/12345', {  
  14.   name'new name'  
  15. }, {  
  16.   cancelToken: source.token  
  17. })  
  18.  
  19. // cancel the request (the message parameter is optional)  
  20. source.cancel('Operation canceled by the user.'); 

通过上面的示例我们可以看到,axios使用的是基于CancelToken的一个撤回提案。不过,目前该提案已经被撤回,具体详情可以见此处。具体的撤回实现方法我们会在后面的章节源码分析的时候进行说明。

axios的核心模块是如何设计与实现的

通过上面的例子,我相信大家对axios的使用方法都有了一个大致的了解。下面,我们将按照模块来对axios的设计与实现进行分析。下图是我们在这篇博客中将会涉及到的相关的axios的文件,如果读者有兴趣的话,可以通过clone相关代码结合博客进行阅读,这样能够加深对相关模块的理解。

HTTP请求模块

作为核心模块,axios发送请求相关的代码位于core/dispatchReqeust.js文件中。由于篇幅有限,下面我选取部分重点的源码进行简单的介绍:

 

  1. module.exports = function dispatchRequest(config) {  
  2.     throwIfCancellationRequested(config);  
  3.  
  4.     // 其他源码   
  5.     // default adapter是一个可以判断当前环境来选择使用Node还是XHR进行请求发送的模块  
  6.     var adapter = config.adapter || defaults.adapter;  
  7.  
  8.     return adapter(config).then(function onAdapterResolution(response) {  
  9.         throwIfCancellationRequested(config);  
  10.  
  11.         // 其他源码  
  12.  
  13.         return response;  
  14.     }, function onAdapterRejection(reason) {  
  15.         if (!isCancel(reason)) {  
  16.             throwIfCancellationRequested(config);  
  17.  
  18.             // 其他源码  
  19.             return Promise.reject(reason);  
  20.         });  
  21. }; 

通过上面的代码和示例我们可以知道,dispatchRequest方法是通过获取config.adapter来得到发送请求的模块的,我们自己也可以通过传入符合规范的adapter函数来替换掉原生的模块(虽然一般不会这么做,不过也算是一个松耦合扩展点)。

在default.js文件中,我们能够看到相关的adapter选择逻辑,即根据当前容器中特有的一些属性和构造函数来进行判断。

 

  1. function getDefaultAdapter() {  
  2.     var adapter;  
  3.     // 只有Node.js才有变量类型为process的类  
  4.     if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {  
  5.         // Node.js请求模块  
  6.         adapter = require('./adapters/http');  
  7.     } else if (typeof XMLHttpRequest !== 'undefined') {  
  8.         // 浏览器请求模块  
  9.         adapter = require('./adapters/xhr');  
  10.     }  
  11.     return adapter;  

axios中XHR模块较为简单,为XMLHTTPRequest对象的封装,我们在这里就不过多进行介绍了,有兴趣的同学可以自行阅读,代码位于adapters/xhr.js文件中。

拦截器模块

了解了dispatchRequest实现的HTTP请求发送模块,我们来看下axios是如何处理请求和响应拦截函数的。让我们看下axios中请求的统一入口request函数。

 

  1. Axios.prototype.request = function request(config) {  
  2.     // 其他代码  
  3.     var chain = [dispatchRequest, undefined]; 
  4.     var promise = Promise.resolve(config);  
  5.     this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {  
  6.         chain.unshift(interceptor.fulfilled, interceptor.rejected);  
  7.     });  
  8.  
  9.     this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {  
  10.         chain.push(interceptor.fulfilled, interceptor.rejected);  
  11.     });  
  12.  
  13.     while (chain.length) {  
  14.         promise = promise.then(chain.shift(), chain.shift());  
  15.     }  
  16.     return promise;  
  17. }; 

这个函数是axios发送请求的入口,因为函数实现比较长,我就简单说一下相关的设计思路:

  1. chain是一个执行队列。这个队列的初始值,是一个带有config参数的Promise。
  2. 在chain执行队列中,插入了初始的发送请求的函数dispatchReqeust和与之对应的undefined。后面需要增加一个undefined是因为在Promise中,需要一个success和一个fail的回调函数,这个从代码promise = promise.then(chain.shift(), chain.shift());就能够看出来。因此,dispatchReqeust和undefined我们可以成为一对函数。
  3. 在chain执行队列中,发送请求的函数dispatchReqeust是处于中间的位置。它的前面是请求拦截器,通过unshift方法放入;它的后面是响应拦截器,通过push放入。要注意的是,这些函数都是成对的放入,也就是一次放入两个。

通过上面的request代码,我们大致知道了拦截器的使用方法。接下来,我们来看下如何取消一个HTTP请求。

取消请求模块

取消请求相关的模块在Cancel/文件夹中。让我们来看下相关的重点代码。

首先,让我们来看下元数据Cancel类。它是用来记录取消状态一个类,具体代码如下:   

  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.  
  10.     var token = this;  
  11.     executor(function cancel(message) {  
  12.         if (token.reason) {  
  13.             // Cancellation has already been requested  
  14.             return 
  15.         }  
  16.         token.reason = new Cancel(message);  
  17.         resolvePromise(token.reason);  
  18.     });  
  19.  
  20.  
  21. CancelToken.source = function source() {  
  22.     var cancel;  
  23.     var token = new CancelToken(function executor(c) {  
  24.         cancel = c;  
  25.     });  
  26.     return {  
  27.         token: token,  
  28.         cancel: cancel  
  29.     };  
  30. }; 

而在adapter/xhr.js文件中,有与之相对应的取消请求的代码:

 

  1. if (config.cancelToken) {  
  2.     // 等待取消  
  3.     config.cancelToken.promise.then(function onCanceled(cancel) {  
  4.         if (!request) {  
  5.             return 
  6.         } 
  7.         request.abort();  
  8.         reject(cancel);  
  9.         // 重置请求  
  10.         request = null 
  11.     });  

结合上面的取消HTTP请求的示例和这些代码,我们来简单说下相关的实现逻辑:

  1. 在可能需要取消的请求中,我们初始化时调用了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函数时,没有当做一个特殊的函数来对待,而是采用一视同仁的方法,将其放在队列的中间位置,从而保证了队列处理的一致性,提高了代码的可阅读性。

Adapter的处理逻辑

在adapter的处理逻辑中,axios没有把http和xhr两个模块(一个用于Node.js发送请求,另一个则用于浏览器端发送请求)当成自身的模块直接在dispatchRequest中直接饮用,而是通过配置的方法在default.js文件中进行默认引入。这样既保证了两个模块间的低耦合性,同时又能够为今后用户需要自定义请求发送模块保留了余地。

取消HTTP请求的处理逻辑

在取消HTTP请求的逻辑中,axios巧妙的使用了一个Promise来作为触发器,将resolve函数通过callback中参数的形式传递到了外部。这样既能够保证内部逻辑的连贯性,也能够保证在需要进行取消请求时,不需要直接进行相关类的示例数据改动,***程度上避免了侵入其他的模块。

总结

本文对axios相关的使用方式、设计思路和实现方法进行了详细的介绍。读者能够通过上述文章,了解axios的设计思想,同时能够在axios的代码中,学习到关于模块封装和交互等相关的经验。

 

由于篇幅原因,本文仅针对axios的核心模块进行了分解和介绍,如果对其他代码有兴趣的同学,可以去GitHub进行查看。 

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2021-07-27 14:50:15

axiosHTTP前端

2022-04-08 08:26:03

JavaHTTP请求

2021-04-22 05:37:14

Axios 开源项目HTTP 拦截器

2020-10-20 14:01:16

HTTP

2021-11-19 07:54:59

Axios网络源码

2021-01-28 07:21:13

算法虚拟DOM前端

2021-11-22 16:12:34

Axios Axios-Retry前端

2021-10-29 12:01:11

HTTP代码前端

2021-09-09 10:23:08

GinNetHttp

2011-12-26 16:39:43

局部函数

2024-01-08 13:47:00

代码分析工具

2021-07-20 10:30:46

Golanghttp语言

2010-06-29 13:18:31

HTTP协议

2022-03-24 14:49:57

HTTP前端

2019-12-20 09:31:23

TCPHTTP浏览器

2019-04-24 15:06:37

Http服务器协议

2017-12-12 15:24:32

Web Server单线程实现

2019-05-29 15:17:43

TCPHTTPSSL

2021-07-04 10:07:04

Virtual DO阅读源码虚拟DOM

2017-05-02 11:30:44

JavaScript数组惰性求值库
点赞
收藏

51CTO技术栈公众号