原载于 TutorialDocs 网站的文章《How to Implement an HTTP Request Library with Axios》。译者:zhangbao90shttps://juejin.im/post/5d906269f265da5ba7451b02

概述

前端开发中,经常会遇到发送异步请求的场景。一个功能齐全的 HTTP 请求库可以大大降低我们的开发成本,提高开发效率。

axios 就是这样一个 HTTP 请求库,近年来非常热门。目前,它在 GitHub 上拥有超过 40,000 的 Star,许多权威人士都推荐使用它。

因此,我们有必要了解下 axios 是如何设计,以及如何实现 HTTP 请求库封装的。撰写本文时,axios 当前版本为 0.18.0,我们以该版本为例,来阅读和分析部分核心源代码。axios 的所有源文件都位于 lib 文件夹中,下文中提到的路径都是相对于 lib 来说的。

本文我们主要讨论:

  • 怎样使用 axios。

  • axios 的核心模块(请求、拦截器、撤销)是如何设计和实现的?

  • axios 的设计优点是什么?

如何使用 axios

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

发送请求

  1. axios({
    method:'get',
    url:'http://bit.ly/2mTM3nY',
    responseType:'stream'
    })
    .then(function(response) {
    response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'))
    });
    复制代码

这是一个官方示例。从上面的代码中可以看到,axios 的用法与 jQuery 的 ajax 方法非常类似,两者都返回一个 Promise 对象(在这里也可以使用成功回调函数,但还是更推荐使用 Promise 或 await),然后再进行后续操作。

这个实例很简单,不需要我解释了。我们再来看看如何添加一个拦截器函数。

添加拦截器函数


  1. axios.interceptors.request.use(function (config) {
  2. return config;
    }, function (error) {
  3. return Promise.reject(error);
    });
  4. axios.interceptors.response.use(function (response) {
  5. return response;
    }, function (error) {
  6. return Promise.reject(error);
    });
    复制代码

从上面的代码,我们可以知道:发送请求之前,我们可以对请求的配置参数(config)做处理;在请求得到响应之后,我们可以对返回数据做处理。当请求或响应失败时,我们还能指定对应的错误处理函数。

撤销 HTTP 请求

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

  1. const CancelToken = axios.CancelToken;
    const source = CancelToken.source();
  2. axios.get('/user/12345', {
    cancelToken: source.token
    }).catch(function(thrown) {
    if (axios.isCancel(thrown)) {
    console.log('请求撤销了', thrown.message);
    } else {
  3. }
    });
  4. axios.post('/user/12345', {
    name: '新名字'
    }, {
    cancelToken: source.token
    }).
  5. source.cancel('用户撤销了请求');
    复制代码

从上例中可以看到,在 axios 中,使用基于 CancelToken 的撤销请求方案。然而,该提案现已撤回,详情如 点这里。具体的撤销请求的实现方法,将在后面的源代码分析的中解释。

axios 核心模块的设计和实现

通过上面的例子,我相信每个人都对 axios 的使用有一个大致的了解了。下面,我们将根据模块分析 axios 的设计和实现。下面的图片,是我在本文中会介绍到的源代码文件。如果您感兴趣,最好在阅读时克隆相关的代码,这能加深你对相关模块的理解。

HTTP 请求模块

请求模块的代码放在了 core/dispatchRequest.js 文件中,这里我只展示了一些关键代码来简单说明:

  1. module.exports = function dispatchRequest(config) {
    throwIfCancellationRequested(config);
  2. var adapter = config.adapter || defaults.adapter;
  3. return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);
  4. return response;
    }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
    throwIfCancellationRequested(config);
  5. return Promise.reject(reason);
    });
    };
    复制代码

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

在 defaults.js 文件中,我们可以看到相关适配器的选择逻辑——根据当前容器的一些独特属性和构造函数,来确定使用哪个适配器。

  1. function getDefaultAdapter() {
    var adapter;
  2. if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
  3. adapter = require('./adapters/http');
    } else if (typeof XMLHttpRequest !== 'undefined') {
  4. adapter = require('./adapters/xhr');
    }
    return adapter;
    }
    复制代码

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

拦截器模块

现在让我们看看 axios 是如何处理,请求和响应拦截器函数的。这就涉及到了 axios 中的统一接口 ——request 函数。

  1. Axios.prototype.request = function request(config) {
  2. var chain = [dispatchRequest, undefined];
    var promise = Promise.resolve(config);
  3. this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
    });
  4. this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
    });
  5. while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
    }
  6. return promise;
    };
    复制代码

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

  1. chain 是一个执行队列。队列的初始值是一个携带配置(config)参数的 Promise 对象。

  2. 在执行队列中,初始函数 dispatchRequest 用来发送请求,为了与 dispatchRequest对应,我们添加了一个 undefined。添加 undefined 的原因是需要给 Promise 提供成功和失败的回调函数,从下面代码里的 promise = promise.then(chain.shift(), chain.shift()); 我们就能看出来。因此,函数 dispatchRequest 和 undefiend 可以看成是一对函数。

  3. 在执行队列 chain 中,发送请求的 dispatchReqeust 函数处于中间位置。它前面是请求拦截器,使用 unshift 方法插入;它后面是响应拦截器,使用 push 方法插入,在 dispatchRequest 之后。需要注意的是,这些函数都是成对的,也就是一次会插入两个。

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

撤销请求模块

与撤销请求相关的模块位于 Cancel/ 文件夹下,现在我们来看下相关核心代码。

首先,我们来看下基础 Cancel 类。它是一个用来记录撤销状态的类,具体代码如下:

  1. function Cancel(message) {
    this.message = message;
    }
  2. Cancel.prototype.toString = function toString() {
    return 'Cancel' + (this.message ? ': ' + this.message : '');
    };
  3. Cancel.prototype.__CANCEL__ = true;
    复制代码

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

  1. function CancelToken(executor) {
    if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
    }
  2. var resolvePromise;
    this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
    });
  3. var token = this;
    executor(function cancel(message) {
    if (token.reason) {
  4. return;
    }
  5. token.reason = new Cancel(message);
    resolvePromise(token.reason);
    });
    }
  6. CancelToken.source = function source() {
    var cancel;
    var token = new CancelToken(function executor(c) {
    cancel = c;
    });
    return {
    token: token,
    cancel: cancel
    };
    };
    复制代码

adapters/xhr.js 文件中,撤销请求的地方是这样写的:

  1. if (config.cancelToken) {
  2. config.cancelToken.promise.then(function onCanceled(cancel) {
    if (!request) {
    return;
    }
  3. request.abort();
    reject(cancel);
  4. request = null;
    });
    }
    复制代码

通过上面的撤销  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 会被放在队列的中间位置,以便保证队列处理的一致性和代码的可读性。

适配器的处理逻辑

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

撤销 HTTP 请求的逻辑

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

总结

本文详细介绍了 axios 的用法、设计思想和实现方法。在阅读之后,您可以了解 axios 的设计,并了解模块的封装和交互。

本文只介绍了 axios 的核心模块,如果你对其他模块代码感兴趣,可以到 GitHub 上查看。

欢迎关注公众号,进一步技术:

axios 是如何封装 HTTP 请求的的更多相关文章

  1. 前端MVC Vue2学习总结(六)——axios与跨域HTTP请求、Lodash工具库

    一.axios Vue更新到2.0之后宣告不再对vue-resource更新,推荐使用axios,axios是一个用于客户端与服务器通信的组件,axios 是一个基于Promise 用于浏览器和 no ...

  2. 用Axios Element 实现全局的请求 loading

        Kapture 2018-06-07 at 14.57.40.gif demo in github 背景 业务需求是这样子的,每当发请求到后端时就触发一个全屏的 loading,多个请求合并为 ...

  3. 原生 Ajax 封装 和 Axios 二次 封装

    AJAX 异步的JavaScript与XML技术( Asynchronous JavaScript and XML ) Ajax 不需要任何浏览器插件,能在不更新整个页面的前提下维护数据,但需要用户允 ...

  4. 关于axios的一些封装

    关于Axios的封装 为何需要在封装 应用场景,项目中涉及100个AJAX请求,其中: 1.其中60个需要在请求头header设置token headers: {token: token}用于权限校验 ...

  5. axios 二次封装

    一般项目往往要对 axios 库进行二次封装,添加一些自定义配置和拦截器等 案例 ./service/axios.js 1234567891011121314151617181920212223242 ...

  6. 【vue】axios二次封装,更好的管理api接口和使用

    在现在的前端开发中,前后端分离开发比较主流,所以在封装方法和模块化上也是非常需要掌握的一门技巧.而axios的封装也是非常的多,下面的封装其实跟百度上搜出来的axios封装或者axios二次封装区别不 ...

  7. 基于SqlSugar的开发框架循序渐进介绍(10)-- 利用axios组件的封装,实现对后端API数据的访问和基类的统一封装处理

    在SqlSugar的开发框架的后端,我们基于Web API的封装了统一的返回结果,使得WebAPI的接口返回值更加简洁,而在前端,我们也需要统一对返回的结果进行解析,并获取和Web API接口对应的数 ...

  8. WebApi系列~基于单请求封装多请求的设计

    回到目录 怎么说,单请求封装多请求,这句话确实有点绕了,但还是要看清楚,想明白这到底是怎么一回事,单请求即一次请求(get,post,put,delete),封闭多请求,即在客户端发送的一个请求中可能 ...

  9. [iOS微博项目 - 3.3] - 封装网络请求

    github: https://github.com/hellovoidworld/HVWWeibo   A.封装网络请求 1.需求 为了避免代码冗余和对于AFN框架的多处使用导致耦合性太强,所以把网 ...

随机推荐

  1. .Net C# EF database first connectionstring

    <connectionStrings> <add name="CupCreditCheckDB" connectionString="metadata= ...

  2. 怎样退出mysql命令行

    使用: mysql -u root -p 进入 mysql 命令号以后, 如果想退出, 可以使用: quit 命令, 如下: mysql -u root -p quit;

  3. python 识别图像主题并切割

    两种办法,一种是用百度的API,效果还可以,不过好像每天有50次的调用的限制 from aip import AipImageClassify import cv2 """ ...

  4. 使用lodop.js打印控件打印table并分页等

    import {getLodop} from '@/utils/LodopFuncs.js' //打印表格 export default{ // num 打印还是打印预览 conData 对象形式 传 ...

  5. LLVM使用其他Pass的结果

    之前的工作一直集中在clang中,最近有点空闲时间,又重新熟悉了一下Pass的书写过程.(参考LLVM CookBook和http://llvm.org/docs/WritingAnLLVMPass. ...

  6. VirtualBox使用

    热键:Right Ctrl 串口 端口编号: COM1 -> /dev/ttyS0 COM2 -> /dev/ttyS1 COM3 -> /dev/ttyS2 COM4 -> ...

  7. ssh: Bad configuration option: usedns

    某天突然听到同事说服务器上git用不了了,上去一看,确实用不了了,git pull报出了如下错误: $ git pull /etc/: Bad configuration option: usedns ...

  8. 30K以上的高薪Java程序员所需技能大汇总

    总所周知,Java是目前使用最为广泛的网络编程语言之一. 它具有简单,面向对象,稳定,与平台无关,解释型,多线程,动态等特点. 一般的JAVA程序员或许只需知道一些JAVA的语法结构就可以应付了.但要 ...

  9. mysql tinyint(1) 在java中被转化为boolean

    数据库表字段类型为:tinyint 长度为1 在java中对应的类型是boolean 查询时直接在页面展示成true或false 如果是2,3,4 这样的也是默认成true,非常不友好. 解决方案: ...

  10. shutil:高层文件操作

    介绍 shutil模块包括一些高层文件操作,如赋值和归档 复制文件 import shutil ''' copyfile将源文件的内容复制到目标文件,如果没有权限写目标文件,则会产生一个IOError ...