原载于 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 的使用。

发送请求

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),然后再进行后续操作。

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

添加拦截器函数


axios.interceptors.request.use(function (config) { return config;
}, function (error) { return Promise.reject(error);
}); axios.interceptors.response.use(function (response) { return response;
}, function (error) { return Promise.reject(error);
});
复制代码

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

撤销 HTTP 请求

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

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('用户撤销了请求');
复制代码

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

axios 核心模块的设计和实现

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

HTTP 请求模块

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

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

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

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

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

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

拦截器模块

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

Axios.prototype.request = function request(config) {

    var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config); this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
}); this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
}); while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
} 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 类。它是一个用来记录撤销状态的类,具体代码如下:

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

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

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

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

if (config.cancelToken) {

    config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
} request.abort();
reject(cancel); 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. hdu 1281 匈牙利算法

    棋盘游戏 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Total Submis ...

  2. js date对象传参获取特定日期的时间戳

    当我们想要通过js获取某一特定时间的时间戳时,会通过给date对象传参再通过getTime函数来获取,传递的参数格式也有不同形式.有些时候,可能会因为自己传入参数的格式不正确而导致date对象inva ...

  3. C#学习资料

    http://www.runoob.com/csharp/csharp-delegate.html

  4. win10下 安装迅雷精简版

    下载链接:https://files-cdn.cnblogs.com/files/del88/ThunderMini_1.5.3.288.zip 他妈的 今天安装迅雷精简版 在win10上 竟然报错, ...

  5. Redis笔记一

    REmote DIctionary Server 是一个开源.内存存储的数据结构服务器,可以用作数据库来存储key-value数据,支持字符串,哈希表,列表,集合,位图,地理空间信息等数据类型,同时也 ...

  6. 在eclipse导入项目的步骤

    1. Import 2. Next 3. 确定  选中copy projects into workspace    Finish 这样项目就导入进来了. 4.导入jar包 Configure Bui ...

  7. 二、MySQL介绍

    目录 一.MySQL背景 二.MySQL的优点 三.MySQL安装 四.MySQL服务的启动和停止 五.MySQL登录和退出 六.MySQL常用命令 (一)常用命令 (二)语法规范 (三)SQL语言细 ...

  8. Kubernetes的初始化容器initContainers

    initContainers是一种专用的容器,在应用程序容器启动之前运行,可以包括一些应用程序镜像中不存在的实用工具和安装脚本,可以完成应用的必要数据初始化等工作.总的来说就是在正式的容器启动之前做一 ...

  9. kubernetes之健康状态检测

    1.说明 容器探针: kubelet 对容器执行的定期诊断 探针执行方式: LivenessProbe: 判断容器是否存活 running状态, 如果不健康kubelet就会杀掉pod,根据重启策略R ...

  10. java—锁的学习研究

    摘抄自博客:https://www.cnblogs.com/qifengshi/p/6831055.html 标题:Java中的锁分类 锁的分类: 公平锁/非公平锁 可重入锁 独享锁/共享锁 互斥锁/ ...