在前端开发过程中, 我们经常会遇到需要发送异步请求的情况. 而使用一个功能齐全, 接口完善的 HTTP 请求库, 能够在很大程度上减少我们的开发成本, 提高我们的开发效率. axios 是一个在近些年来非常火的一个 HTTP 请求库, 目前在 GitHub 中已经拥有了超过 40K 的 star, 受到了各位大佬的推荐.
概述
在前端开发过程中, 我们经常会遇到需要发送异步请求的情况. 而使用一个功能齐全, 接口完善的 HTTP 请求库, 能够在很大程度上减少我们的开发成本, 提高我们的开发效率.
axios 是一个在近些年来非常火的一个 HTTP 请求库, 目前在 GitHub 中已经拥有了超过 40K 的 star, 受到了各位大佬的推荐.
今天, 我们就来看下, axios 到底是如何设计的, 其中又有哪些值得我们学习的地方. 我在写这边文章时, 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'))
- });
这是一个官方的 API 示例. 从上面的代码中我们可以看到, axios 的用法与 jQuery 的 ajax 很相似, 都是通过返回一个 Promise(也可以通过 success 的 callback, 不过建议使用 Promise 或者 await)来继续后面的操作.
这个代码示例很简单, 我就不过多赘述了, 下面让我们来看下如何添加一个过滤器函数.
增加拦截器 (Interceptors) 函数
- // 增加一个请求拦截器, 注意是 2 个函数, 一个处理成功, 一个处理失败, 后面会说明这种情况的原因
- 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('Request canceled', thrown.message);
- } else {
- // handle error
- }
- });
- axios.post('/user/12345', {
- name: 'new name'
- }, {
- cancelToken: source.token
- })
- // cancel the request (the message parameter is optional)
- source.cancel('Operation canceled by the user.');
通过上面的示例我们可以看到, axios 使用的是基于 CancelToken 的一个撤回提案. 不过, 目前该提案已经被撤回, 具体详情可以见此处. 具体的撤回实现方法我们会在后面的章节源码分析的时候进行说明.
axios 的核心模块是如何设计与实现的
通过上面的例子, 我相信大家对 axios 的使用方法都有了一个大致的了解. 下面, 我们将按照模块来对 axios 的设计与实现进行分析. 下图是我们在这篇博客中将会涉及到的相关的 axios 的文件, 如果读者有兴趣的话, 可以通过 clone 相关代码结合博客进行阅读, 这样能够加深对相关模块的理解.
HTTP 请求模块
作为核心模块, axios 发送请求相关的代码位于 core/dispatchReqeust.js 文件中. 由于篇幅有限, 下面我选取部分重点的源码进行简单的介绍:
- module.exports = function dispatchRequest(config) {
- throwIfCancellationRequested(config);
- // 其他源码
- // default adapter 是一个可以判断当前环境来选择使用 Node 还是 XHR 进行请求发送的模块
- 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 来得到发送请求的模块的, 我们自己也可以通过传入符合规范的 adapter 函数来替换掉原生的模块(虽然一般不会这么做, 不过也算是一个松耦合扩展点).
在 default.js 文件中, 我们能够看到相关的 adapter 选择逻辑, 即根据当前容器中特有的一些属性和构造函数来进行判断.
- function getDefaultAdapter() {
- var adapter;
- // 只有 Node.js 才有变量类型为 process 的类
- if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
- // Node.js 请求模块
- adapter = require('./adapters/http');
- } else if (typeof XMLHttpRequest !== 'undefined') {
- // 浏览器请求模块
- adapter = require('./adapters/xhr');
- }
- return adapter;
- }
axios 中 XHR 模块较为简单, 为 XMLHTTPRequest 对象的封装, 我们在这里就不过多进行介绍了, 有兴趣的同学可以自行阅读, 代码位于 adapters/xhr.js 文件中.
拦截器模块
了解了 dispatchRequest 实现的 HTTP 请求发送模块, 我们来看下 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 发送请求的入口, 因为函数实现比较长, 我就简单说一下相关的设计思路:
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 类. 它是用来记录取消状态一个类, 具体代码如下:
- 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) {
- // Cancellation has already been requested
- 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
- };
- };
而在 adapter/xhr.js 文件中, 有与之相对应的取消请求的代码:
- if (config.cancelToken) {
- // 等待取消
- config.cancelToken.promise.then(function onCanceled(cancel) {
- if (!request) {
- return;
- }
- request.abort();
- reject(cancel);
- // 重置请求
- request = null;
- });
- }
结合上面的取消 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 进行查看.
来源: http://developer.51cto.com/art/201807/580025.htm