主题
请求库封装
背景
axios 虽然很成熟,但它只是一个基础库,没有提供诸多的上层功能,比如:
- 请求重试
- 请求缓存
- 请求幂等
- 请求串行
- 请求并发
- …
VueRequest(vue) / SWR(react) 它们虽然提供的功能很多,但仍然存在诸多问题:
- 与上层框架过度绑定导致开发场景受限,也无法提供统一的 API
- 成熟度不够,issue 的回复也难以做到及时,存在一定风险
- 它们没有聚合基础请求库,仍然需要手动整合
除此之外更重要的是:
公共库不包含公司内部制定的协议规范,即便使用公共库,也必须针对它们做二次封装。
综上,需要自行封装一套适配公司业务的前端请求库
方案和实现
库结构的宏观设计
整个库结构包含三层,从下往上 依次是:
- 请求实现层:提供请求基本功能
- request-core:提供网络上层控制,比如请求串行、请求并行、请求重试、请求防重等功能
- request-bus:为请求绑定业务功能,该层接入公司内部协议规范和接口文档,向外提供业务接口 API

优化设计:
在三层中,请求实现层的实现有多种方式:
- 基于
XHR原生 - 基于
fetch原生 - 基于
axios等第三方库 - ...
这种实现的多样性可能导致这一次层的不稳定,而 request-imp 是基础层,它的不稳性会传导到上一层。
所以必须寻求一种方案来隔离这种不稳定性。
我们可以基于 DIP(Dependence Inversion Principle,依赖倒置原则),彻底将 request-core 和请求的实现解耦,而 typescript 的类型系统让这一切的落地成为了可能。

请求缓存
请求缓存是指创建一个带有缓存的请求,当没有命中缓存时发送请求并缓存结果,当有缓存时直接返回缓存。
js
const req = createCacheRequestor()
req.get('/a') // 请求
req.get('/a') // 使用缓存
req.get('/b') // 请求
req.get('/b') // 使用缓存
// ...要实现此功能需要考虑几个核心问题:
- 请求结果怎么存?
- 存在哪?
- 缓存键是什么?
我们希望用户能够指定缓存方案(内存/持久化),同时也能够指定缓存键:
js
const req = createCacheRequestor({
key(config) {
// config 为某次请求的配置
return config.pathname // 使用 pathname 作为缓存键
},
persist: true, //是否开启持久化缓存
})存储有多种方案:
- 内存
- storage
- cookie
- indexDB
- SW
- WebSQL
不同的方案,存储的格式不同,支持的功能不同,兼容性也不同。为了抹平这种差异,避免将来存储方案变动时对其他代码造成影响,需要设计一个稳定的接口来屏蔽方案间的差异。

ts
export interface CahceStore {
has(key: string): Promise<boolean>
get(key: string): Promise<T>
set(key: string, ...value: T[]): Promise<void>
// ...
}
export function useCacheStore(isPersist): CacheStore{
if(!isPersist) {
return createMemoryStore();
} else {
return createStoragestore():
}
}请求幂等
幂等性是一个数学概念,常见于抽象代数
在网络请求中,很多接口都要求幂等性,比如支付,同一订单多次支付和一次支付对用户余额的影响应该是一样的。
要解决这一问题,就必须保证:要求幂等的请求不能重复提交
这里的关键问题就在于定义什么是 重复?
我们可以把重复定义为:请求方法、请求头、请求体完全一致
