写点什么

手撕 83K STAR 的 Axios 设计思想,并进行能力增强

用户头像
梁龙先森
关注
发布于: 2021 年 04 月 11 日
手撕83K STAR的Axios设计思想,并进行能力增强

概述

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。并且,同时具备如下特征:

  • 拦截请求和响应

  • 转换请求数据和响应数据

  • 取消请求

  • 自动转换 JSON 数据

  • 客户端支持防御 XSRF

本文将深入源码、抽离 Axios 核心数据模型,学习它值得借鉴的地方。并通过源码理解拦截器、适配器,以及取消请求等模块如何实现。最后将通过实现 Axios 缓存接口数据的功能,一步步掌握 Axios 库的精髓。看完你一定能对实现接口节流、请求失败重发、取消重复请求等需求手到擒来。

Axios 设计理解

首先我们来看下采用 axios 实例发起一个 URL 请求的大体数据流转流程。

通过追踪 axios 完整的请求流程源码,可以看出项目抽离出了很多的数据模型和转换模块,比如:Axios 构造函数、defaults 默认配置参数、拦截器、适配器,以及数据转换函数等等,具备很高的抽象性。


同时,它具备良好的分层架构,这里指的是底层请求能力、以及上层业务增强能力完美解耦。底层上采用适配器的思想,使得项目完美的兼容浏览器端和 Node.js 服务端。在上层业务实例配置上,依次读取默认配置、实例配置、方法配置,并从后往上进行覆盖,让实例的可扩展性更灵活、细度更细。请求方法的执行链路上,具备清晰流程设计,当然也得益于 axios 对数据模型高度抽象,更方便在流程链路上做业务无伤害侵入。比如在,defaults 默认配置项上配置的 adapter 适配器,适配器属于底层请求上的封装,不存在上层业务对其的侵入,因此往往可以对适配器进行增强,在发起真正 URL 上注入更多的扩展能力,比如:请求失败重发、接口节流,以及我们文末的案例:缓存接口数据。


下面我们从源码上看看 axios 是如何实现拦截器,以及它的适配器思想。

拦截器实现

拦截器赋予了项目开发时注入自定义行为的能力。通常在请求拦截器中实现自定义请求头,在响应拦截器完成对返回数据自定义 code 的解析,异常统一处理等。axios 的拦截器的由注册->编排->执行三部分组成。

下面是项目中请求拦截器的注册执行,那源码是如何实现的呢?

axios.interceptors.request.use(	config=>{    // 自定义操作,通过在此新增自定义请求头token  	return config  },  error=>{  	return Promise.reject(error)  })
复制代码

挂载拦截器

Axios.interceptors 属性挂载拦截器。

function Axios(instanceConfig) {  this.defaults = instanceConfig;  this.interceptors = {    request: new InterceptorManager(),    response: new InterceptorManager()  };}
复制代码

任务管理设计

InterceptorManager 构造函数处理任务注册、注销等。

function InterceptorManager() {  this.handlers = [];}InterceptorManager.prototype.use = function use(fulfilled, rejected){  this.handlers.push({    fulfilled: fulfilled,    rejected: rejected  });  // 返回当前注册  return this.handlers.length - 1;}InterceptorManager.prototype.eject = function eject(id) {  if (this.handlers[id]) {    this.handlers[id] = null;  }};
复制代码

拦截器实例内部维护了 handlers 数组属性存放任务列表,每个任务由 fulfilled、rejected 两种处理函数组成。通过调用 use 函数可以注册任务,并返回当前任务的索引值。拦截器实例可以调用 eject 函数清空对应索引的任务。拦截器还有更多实现,详细可以自行查看代码。

任务编排与执行

拦截器的执行是在 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;};
复制代码

内部通过 chain 数组进行任务的编排,dispatchRequest 是发起 URL 请求封装的方法。遇到请求拦截器时,会遍历其 handlers 任务列表,并插入到 chain 数组的头部,遇到响应拦截器便将其添加到任务编排数组的末尾。每个任务是按 fulfilled 在前、 rejected 在后放置。

存在编排任务时,依次从前往后执行,因此先执行请求拦截器任务,再是发起请求,最后才是响应拦截器。当遇到失败,执行 Promise.reject() ,抛出错误,任务不再往下执行。

适配器思想

axios 广泛应用于浏览器和 Node.js 服务器,那浏览器通常采用 XMLHttpRequest 或者 Fetch 发起请求,而 Node 服务器端则采用 Http 模块,那 axios 是如何兼容的呢?


答案是:适配器 adapter。通过各自环境独有变量判别对象,如浏览器的 XMLHttpRequest ,服务器端的 process,从而返回不同的适配器。

function getDefaultAdapter() {  var adapter;  if (typeof XMLHttpRequest !== 'undefined') {    // For browsers use XHR adapter    adapter = require('./adapters/xhr');  } else if (typeof process !== 'undefined') {    // For node use HTTP adapter    adapter = require('./adapters/http');  }  return adapter;}
复制代码
适配器是什么?

查看 lib/adapters/xhr.js 文件,可以看到适配器只是一个返回 Promise 对象的函数!注意这个,后面我们将自定义扩展适配器!内部请求的实现,不是关注重点,感兴趣可以去看源码。

module.exports = function xhrAdapter(config) {  return new Promise(function dispatchXhrRequest(resolve, reject) {    // 内部URL请求实现  }}
复制代码

取消重复请求

在浏览器中 XMLHttpRequest 请求可以通过 abort 取消请求。在 axios 中,同样提供了取消请求的能力,下面看看 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 {     // 处理错误  }});
// 方式二:let cancel;axios.get('/user/12345', { cancelToken: new CancelToken(function executor(c) { // executor 函数接收一个 cancel 函数作为参数 cancel = c; })});// 取消请求cancel();
复制代码

我们知道 Axios 可以通过 CancelToken 取消请求,并且存在两种取消的方式。那内部的机制是怎样的呢?

CancelToken 是什么?

  function CancelToken(executor) {  if (typeof executor !== 'function') {    throw new TypeError('executor must be a function.');  }
var resolvePromise; // 创建个promise对象 this.promise = new Promise(function promiseExecutor(resolve) { resolvePromise = resolve; });
var token = this; executor(function cancel(message) { if (token.reason) { return; } // 这里的方法,未来某时刻才执行 // reason 存在值,则取消请求 token.reason = new Cancel(message); resolvePromise(token.reason); });}// 取消请求时执行方法CancelToken.prototype.throwIfRequested = function throwIfRequested() { if (this.reason) {throw this.reason;}};
CancelToken.source = function source() { var cancel; var token = new CancelToken(function executor(c) {cancel = c;}); return {token: token,cancel: cancel};};
复制代码

值得借鉴的是 CancelToken 传入了执行器 executor ,并将内部 cancel 函数作为 executor 的参数暴露出去。这里的 cancel 函数可以在未来的某个时刻执行,当它执行后 CancelToken 实例将挂载上 reason 属性,值为 new Cancel。 此时,当前请求就会被取消。

如何取消请求

// CancelToken.js CancelToken.prototype.throwIfRequested = function throwIfRequested() {  if (this.reason) {throw this.reason;}};
// dispatchRequest.js 内部方法function throwIfCancellationRequested(config) { if (config.cancelToken) { config.cancelToken.throwIfRequested(); }}
module.exports = function dispatchRequest(config) { throwIfCancellationRequested(config); // 后续前往发起请求相关逻辑}
复制代码

取消请求是在执行 dispatchRequest 派发请求时判断配置上是否存在 cancelToken,且是否存在reason ,即是否执行了 cancel 函数。若存在,则直接抛出取消请求的 reason,派发请求流程也就不往下走了。不管拦截器、或者取消请求,其实都是在触发适配器前处理,业务层不会响应底层请求适配器。这也是 axios 库很值得借鉴的地方,严格分层。

案例:缓存接口数据

缓存对实时性要求不严格的接口数据,可以减少一次网络请求,页面响应更加及时。针对此需求,我们可以提前设计出指标,做出更健硕的方案。比如:

  1. 接口可以缓存数据,也可以不缓存

  2. 对业务侵入更少

通过模型设计之美章节,我们知道发起真正 URL 请求是在 adapter 适配器。所以可以考虑通过对默认适配器进行增强拦截,若存在缓存数据,则直接缓存数据,否则进入请求。增强适配器的好处,也更利用 axios 的扩展性,我们通过覆盖全局修改适配器,或者仅仅变更单个请求适配器,更加灵活。 下面看看详细设计方案。

1. 判断同个请求

当发起请求的 URL、和参数是一致的时候,我们可以认为他们是同一个请求,因此可以将两参数转换拼接作为缓存接口数据的 key 值。看看具体代码:

import buildURL from 'axios/lib/helpers/buildURL';export default function buildSortedURL(...args: any[]) {	const builtURL = buildURL(...args);	const [urlPath, queryString] = builtURL.split('?');	if (queryString) {		const paramsPair = queryString.split('&');		return `${urlPath}?${paramsPair.sort().join('&')}`;	}	return builtURL;}
复制代码

2. 适配器增强

采用适配器增强

export default function cacheAdapterEnhancer(adapter: AxiosAdapter, options: Options = {}): AxiosAdapter {	const {    // 开启默认缓存		enabledByDefault = true,    // 自定义缓存标志		cacheFlag = 'cache',    // 用于缓存数据		defaultCache = new LRUCache<string, AxiosPromise>({ maxAge: FIVE_MINUTES, max: CAPACITY }),	} = options;
return config => { const { url, method, params, paramsSerializer, forceUpdate } = config; // 是否使用缓存 const useCache = ((config as any)[cacheFlag] !== void 0 && (config as any)[cacheFlag] !== null) ? (config as any)[cacheFlag] : enabledByDefault; // 请求 if (method === 'get' && useCache) { // 如果提供指定的缓存,则使用它 const cache: ICacheLike<AxiosPromise> = isCacheLike(useCache) ? useCache : defaultCache; // 构造缓存的key const index = buildSortedURL(url, params, paramsSerializer); // 获取接口数据缓存 let responsePromise = cache.get(index); // 缓存不存在,或者配置forceUpdate = true不使用缓存,则直接进去请求 if (!responsePromise || forceUpdate) { responsePromise = (async () => { try { return await adapter(config); } catch (reason) { cache.del(index); throw reason; } })(); // 将未转换的响应promise放入缓存 cache.set(index, responsePromise); return responsePromise; } return responsePromise; } // 执行默认适配器 return adapter(config); };}
复制代码

3. 使用文档

  1. 实例配置自定义适配器

const http = axios.create({	baseURL: '/',	adapter: cacheAdapterEnhancer(axios.defaults.adapter,    { enabledByDefault: false, cacheFlag: 'useCache'})});
复制代码
  1. 配置说明

enabledByDefault:启用所有请求的缓存,而无需在请求配置中进行明确定义。

cacheFlag:配置标志以明确定义 axios 请求中的缓存使用情况。

defaultCache:默认情况下将用于存储请求的 CacheLike 实例,除非使用请求配置定义自定义 Cache。

总结

至此我们便阅读了 Axios 项目的架构设计亮点、以及拦截器底层实现、适配思想,并通过对默认适配器的增强实现了接口数据缓存功能。当然 Axios 的功能不仅于此,借鉴增强适配器的思路,还可以实现请求节流、请求失败重发机制、取消重复请求此类常用需求。如果你感兴趣可以查找 axios-extension 这个库。


发布于: 2021 年 04 月 11 日阅读数: 133
用户头像

梁龙先森

关注

无情的写作机器 2018.03.17 加入

vite原理/微前端/性能监控方案...,正在来的路上... 最近太忙...

评论 (4 条评论)

发布
用户头像
认真看了一下,学到了
2021 年 04 月 12 日 15:13
回复
用户头像
写得不错~
2021 年 04 月 12 日 14:36
回复
用户头像
这篇 axios 进阶文章覆盖面很全,值得深入学习。
2021 年 04 月 12 日 14:17
回复
用户头像
手把手教学吗 大佬
2021 年 04 月 12 日 14:13
回复
没有更多了
手撕83K STAR的Axios设计思想,并进行能力增强