概述
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 条评论)