手撕83K STAR的Axios设计思想,并进行能力增强
概述
拦截请求和响应 转换请求数据和响应数据 取消请求 自动转换 JSON 数据 客户端支持防御 XSRF
本文将深入源码、抽离Axios核心数据模型,学习它值得借鉴的地方。并通过源码理解拦截器、适配器,以及取消请求等模块如何实现。最后将通过实现Axios缓存接口数据的功能,一步步掌握Axios库的精髓。看完你一定能对实现接口节流、请求失败重发、取消重复请求等需求手到擒来。
Axios设计理解
首先我们来看下采用axios实例发起一个URL请求的大体数据流转流程。

通过追踪 axios 完整的请求流程源码,可以看出项目抽离出了很多的数据模型和转换模块,比如:Axios构造函数、defaults 默认配置参数、拦截器、适配器,以及数据转换函数等等,具备很高的抽象性。
同时,它具备良好的分层架构,这里指的是底层请求能力、以及上层业务增强能力完美解耦。底层上采用适配器的思想,使得项目完美的兼容浏览器端和 Node.js 服务端。在上层业务实例配置上,依次读取默认配置、实例配置、方法配置,并从后往上进行覆盖,让实例的可扩展性更灵活、细度更细。请求方法的执行链路上,具备清晰流程设计,当然也得益于axios对数据模型高度抽象,更方便在流程链路上做业务无伤害侵入。比如在,defaults 默认配置项上配置的
下面我们从源码上看看 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()
};
}
任务管理设计
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;
}
};
拦截器实例内部维护了 数组属性存放任务列表,每个任务由 fulfilled、rejected 两种处理函数组成。通过调用 函数可以注册任务,并返回当前任务的索引值。拦截器实例可以调用 函数清空对应索引的任务。拦截器还有更多实现,详细可以自行查看代码。
任务编排与执行
拦截器的执行是在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;
};
内部通过 数组进行任务的编排, 是发起URL请求封装的方法。遇到请求拦截器时,会遍历其 任务列表,并插入到 数组的头部,遇到响应拦截器便将其添加到任务编排数组的末尾。每个任务是按 在前、 在后放置。
存在编排任务时,依次从前往后执行,因此先执行请求拦截器任务,再是发起请求,最后才是响应拦截器。当遇到失败,执行 ,抛出错误,任务不再往下执行。
适配器思想
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;
}
适配器是什么?
查看 文件,可以看到适配器只是一个返回 对象的函数!注意这个,后面我们将自定义扩展适配器!内部请求的实现,不是关注重点,感兴趣可以去看源码。
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);
// 后续前往发起请求相关逻辑
}
取消请求是在执行 派发请求时判断配置上是否存在 ,且是否存在 ,即是否执行了 函数。若存在,则直接抛出取消请求的 ,派发请求流程也就不往下走了。不管拦截器、或者取消请求,其实都是在触发适配器前处理,业务层不会响应底层请求适配器。这也是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({ 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 = 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这个库。