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

概述

在前端开发过程中,我们经常会遇到需要发送异步请求的情况。而使用一个功能齐全,接口完善的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. axios.interceptors.response.use(function (response) {

    11.    // 针对响应数据进行处理

    12.    return response;

    13.  }, function (error) {

    14.    // 响应错误后处理

    15.    return Promise.reject(error);

    16.  });

    通过上面的示例我们可以知道:在请求发送前,我们可以针对请求的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. axios.post('/user/12345', {

    13.  name: 'new name'

    14. }, {

    15.  cancelToken: source.token

    16. })

    17. // cancel the request (the message parameter is optional)

    18. 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.    // default adapter是一个可以判断当前环境来选择使用Node还是XHR进行请求发送的模块

    5.    var adapter = config.adapter || defaults.adapter;

    6.    return adapter(config).then(function onAdapterResolution(response) {

    7.        throwIfCancellationRequested(config);

    8.        // 其他源码

    9.        return response;

    10.    }, function onAdapterRejection(reason) {

    11.        if (!isCancel(reason)) {

    12.            throwIfCancellationRequested(config);

    13.            // 其他源码

    14.            return Promise.reject(reason);

    15.        });

    16. };

    通过上面的代码和示例我们可以知道, 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;

    12. }

    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.    this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {

    9.        chain.push(interceptor.fulfilled, interceptor.rejected);

    10.    });

    11.    while (chain.length) {

    12.        promise = promise.then(chain.shift(), chain.shift());

    13.    }

    14.    return promise;

    15. };

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

  • chain是一个执行队列。这个队列的初始值,是一个带有config参数的Promise。

  • 在chain执行队列中,插入了初始的发送请求的函数 dispatchReqeust和与之对应的undefined。后面需要增加一个 undefined是因为在Promise中,需要一个success和一个fail的回调函数,这个从代码 promise = promise.then(chain.shift(),chain.shift());就能够看出来。因此, dispatchReqeust和 undefined我们可以成为一对函数。

  • 在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.    var token = this;

    10.    executor(function cancel(message) {

    11.        if (token.reason) {

    12.            // Cancellation has already been requested

    13.            return;

    14.        }

    15.        token.reason = new Cancel(message);

    16.        resolvePromise(token.reason);

    17.    });

    18. }

    19. CancelToken.source = function source() {

    20.    var cancel;

    21.    var token = new CancelToken(function executor(c) {

    22.        cancel = c;

    23.    });

    24.    return {

    25.        token: token,

    26.        cancel: cancel

    27.    };

    28. };

    而在 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.    });

    12. }

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

  • 在可能需要取消的请求中,我们初始化时调用了source方法,这个方法返回了一个CancelToken类的实例A和一个函数cancel。

  • 在source方法返回实例A中,初始化了一个在pending状态的promise。我们将整个实例A传递给axios后,这个promise被用于做取消请求的触发器。

  • 当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进行查看。

    如果有任何疑问或者观点,欢迎随时留言讨论。


    作者:hjava

    https://segmentfault.com/a/1190000015747143

    最后编辑于
    ©著作权归作者所有,转载或内容合作请联系作者
    • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
      沈念sama阅读 203,324评论 5 476
    • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
      沈念sama阅读 85,303评论 2 381
    • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
      开封第一讲书人阅读 150,192评论 0 337
    • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
      开封第一讲书人阅读 54,555评论 1 273
    • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
      茶点故事阅读 63,569评论 5 365
    • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
      开封第一讲书人阅读 48,566评论 1 281
    • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
      沈念sama阅读 37,927评论 3 395
    • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
      开封第一讲书人阅读 36,583评论 0 257
    • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
      沈念sama阅读 40,827评论 1 297
    • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
      茶点故事阅读 35,590评论 2 320
    • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
      茶点故事阅读 37,669评论 1 329
    • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
      沈念sama阅读 33,365评论 4 318
    • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
      茶点故事阅读 38,941评论 3 307
    • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
      开封第一讲书人阅读 29,928评论 0 19
    • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
      开封第一讲书人阅读 31,159评论 1 259
    • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
      沈念sama阅读 42,880评论 2 349
    • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
      茶点故事阅读 42,399评论 2 342

    推荐阅读更多精彩内容

    • title: promise总结 总结在前 前言 下文类似 Promise#then、Promise#resolv...
      JyLie阅读 12,215评论 1 21
    • 镜湖的小桥带着雨点的芬芳,那是美。 古老的门楼上杂生的野草,那是美。 1314次列车上邂逅的眼神,那是美。 明媚的...
      文白阅读 201评论 0 3
    • 世上有完全相同的两朵海棠吗? 没有。 就像世上没有完全相同的两个人。 花与花的区别,一般人不易查别,无心查别。植物...
      郭尔同学阅读 150评论 0 0
    • 唯有文字能承载着记忆的永恒,我愿用我这不成熟的文笔来记录我生活中的五味杂粮。 我的第18篇原创日记,相信日积月...
      水墨_画卷阅读 507评论 0 0