概述
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 库很值得借鉴的地方,严格分层。
案例:缓存接口数据
缓存对实时性要求不严格的接口数据,可以减少一次网络请求,页面响应更加及时。针对此需求,我们可以提前设计出指标,做出更健硕的方案。比如:
接口可以缓存数据,也可以不缓存
对业务侵入更少
通过模型设计之美章节,我们知道发起真正 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. 使用文档
实例配置自定义适配器
const http = axios.create({ baseURL: '/', adapter: cacheAdapterEnhancer(axios.defaults.adapter, { enabledByDefault: false, cacheFlag: 'useCache'})});
复制代码
配置说明
enabledByDefault:启用所有请求的缓存,而无需在请求配置中进行明确定义。
cacheFlag:配置标志以明确定义 axios 请求中的缓存使用情况。
defaultCache:默认情况下将用于存储请求的 CacheLike 实例,除非使用请求配置定义自定义 Cache。
总结
至此我们便阅读了 Axios 项目的架构设计亮点、以及拦截器底层实现、适配思想,并通过对默认适配器的增强实现了接口数据缓存功能。当然 Axios 的功能不仅于此,借鉴增强适配器的思路,还可以实现请求节流、请求失败重发机制、取消重复请求此类常用需求。如果你感兴趣可以查找 axios-extension 这个库。
评论 (4 条评论)