能力:防重复请求(Duplicate)
防止同一请求被重复发送。默认 ignore 静默忽略重复调用,需要显式错误时用 block,查询合并时用 reuse。
策略选择
| 策略 | 首次请求 | 重复请求 | 适合场景 |
|---|---|---|---|
| ignore(忽略,默认) | 正常发送 | 返回永远 pending 的 Promise,不触发 .then() / .catch() / .finally() | 表单提交、下单、支付、已有成功/失败副作用的项目 |
| block(阻断) | 正常发送 | 直接拒绝,抛出 RequestGuardBlockedError | 需要业务显式感知重复提交 |
| reuse(复用) | 正常发送 | 等待首次请求结果,共享响应 | 列表查询、数据加载、配置查询 |
单业务场景配置
ignore 策略:重复请求静默挂起
这是默认策略,适合表单提交、创建订单、支付等写操作。重复调用不会发出请求,也不会触发调用方的成功、失败或 finally 逻辑:
const submitOrder = async (orderData) => {
const res = await axios.post('/api/order/submit', orderData, {
requestGuard: {
duplicate: {
strategy: 'ignore', // 可省略,ignore 是默认策略
message: '订单提交中,请稍候' // 用户看到的提示
}
}
});
return res.data;
};
// 用户快速双击按钮
submitOrder(data); // ✅ 正常发送,请求发出
submitOrder(data); // ⏳ 请求不发出,返回的 Promise 永远 pending
// 用户看到:弹出"订单提交中,请稍候"提示(由 notify 触发)
// 开发日志:控制台输出 duplicate.triggered,含 requestKey、repeatCount 等ignore 的 Promise 不会结束
ignore 命中的重复请求会返回一个永远 pending 的 Promise。request-guard 内部不会保存这份 Promise,因此调用方释放引用后可被 GC 回收;但如果业务代码 await 了这个重复请求,该 async 执行流会一直挂起。组件卸载、页面离开和业务取消应由调用方自己的生命周期处理。
自定义 transport 的兜底释放
默认 inFlightTtl: 0 表示在途 key 只会在原请求 settle 或 clearState() 时释放。如果你的自定义 transport 可能返回永不 settle 的 Promise,可显式配置 inFlightTtl 作为兜底清理。
开启 TTL 后,超时的在途 key 会被状态维护任务释放;如果原请求其实仍未结束,后续同 key 请求可能重新进入真实 transport。这是可用性兜底,不是默认防重窗口。
block 策略:重复请求直接报错
适合需要业务显式感知重复提交的场景:
await axios.post('/api/order/submit', orderData, {
requestGuard: {
duplicate: {
strategy: 'block',
message: '订单提交中,请稍候'
}
}
});
// 重复调用会 reject RequestGuardBlockedError,请求未发出reuse 策略:多个请求共享同一响应
适合列表查询、配置加载等读操作:
const fetchList = (params) => axios.get('/api/list', {
params,
requestGuard: {
duplicate: {
strategy: 'reuse', // 复用策略
compareFields: ['method', 'url', 'params'] // 按这些字段判断是否重复
}
}
});
// 组件 A 和组件 B 同时发起相同查询
const promiseA = fetchList({ page: 1 });
const promiseB = fetchList({ page: 1 });
// 实际只发出 1 次网络请求,两者拿到相同的响应
const [resA, resB] = await Promise.all([promiseA, promiseB]);
console.log(resA === resB); // true — 共享同一响应短路错误不经过响应拦截器
重要
block 命中时请求根本没发出去(在请求真正发出前就被拦截),所以你写在 axios.interceptors.response.use(null, onRejected) 里的统一错误处理不会执行。这对 duplicate block、容量硬上限、以及 circuitBreaker 熔断打开这类"请求未发出"的短路错误都成立。ignore 命中不会产生错误,它返回的是永远 pending 的 Promise。
// ❌ 不可靠:短路错误不会进入这里
axios.interceptors.response.use(null, (error) => {
if (error.name === 'RequestGuardBlockedError') { Toast.show(error.message); }
return Promise.reject(error);
});正确的统一处理出口有两个:
notify出口(推荐做 toast/提示):ignore / block / reuse 命中重复请求都会触发notify,无需业务侧 catch:
setupRequestGuard(axios, {
notify: (payload) => {
if (payload.capability === 'duplicate') Toast.show(payload.message);
}
});- 请求级
.catch(需要按错误类型分支时):短路错误会从发起调用的 Promise 直接 reject 出来,在调用处用error.name判别:
try {
await axios.post('/api/order', data);
} catch (error) {
if (error.name === 'RequestGuardBlockedError') Toast.show(error.message);
}Key 生成配置
判重依赖请求指纹(requestKey)。相同请求产出相同 key,不同请求产出不同 key。你通常只需配置 compareFields,复杂场景可用 keyGenerator 完全接管:
| 配置方式 | 适用场景 |
|---|---|
compareFields | 从 config 中取指定字段拼接成 key(默认 ['method','baseURL','url','params','data']) |
keyGenerator(config, options) | 完全自主决定 key 格式,返回 string / number(0/false 会转成字符串);返回 undefined/null/'' 时回退到默认序列化 |
hashAlgorithm + hashThreshold | key 过长时自动压缩(auto 模式超过 hashThreshold 用 cyrb53 哈希;manual 不压缩) |
compareHeaders / headerFields | 让指定 header 参与判重,适合区分租户、门店等上下文 |
formDataResolver | 自定义 FormData 参与判断的值,避免默认占位符把不同表单请求都视为相同 |
blobResolver | 自定义 Blob 参与判断的值,避免不可读二进制影响判断稳定性 |
requestGuard: {
duplicate: {
strategy: 'ignore',
// 以业务订单 ID 作为判重依据
keyGenerator(config) {
return `order:${config.data.clientOrderId}`;
}
}
}配置项
请求级配置(requestGuard.duplicate)
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| enabled | boolean | true | 本次请求是否启用防重 |
| strategy | string | 继承全局 | 策略:ignore / block / reuse |
| compareFields | string[] | 继承全局 | 判重比较字段 |
| compareHeaders | string[] | [] | 额外参与判重的 header |
| hashAlgorithm | string | 继承全局 | 哈希算法(auto / cyrb53 / manual) |
| hashThreshold | number | 继承全局 | 超过此长度自动压缩 key |
| maxCacheSize | number | 继承全局 | 单策略在途记录硬上限,满额时新 key 拒绝 |
| inFlightTtl | number | 继承全局 | 在途记录兜底存活时间,毫秒;0 表示关闭 |
| message | string | 继承全局 | 命中时提示文案 |
| showToast | boolean | 继承全局 | 是否触发 notify |
| captureStack | boolean | 继承全局 | 是否采集调用栈 |
| keyGenerator | function | 继承全局 | 自定义 key 生成函数 |
| formDataResolver | function | 继承全局 | FormData 自定义序列化 |
| blobResolver | function | 继承全局 | Blob 自定义序列化 |
| idempotencyKey | boolean / string / function | 继承全局 | 幂等 key 生成规则 |
全局默认配置(defaults.duplicate)
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| enabled | boolean | true | 能力总开关 |
| strategy | string | 'ignore' | 默认策略 |
| compareFields | string[] | ['method','baseURL','url','params','data'] | 判重比较字段 |
| excludeMethods | string[] | ['GET','OPTIONS','HEAD'] | 规则匹配排除的方法 |
| compareHeaders | string[] | [] | 进入判重的 header 白名单(headerFields 是其别名,效果相同) |
| hashAlgorithm | string | 'auto' | 哈希算法 |
| hashThreshold | number | 1000 | 超过此长度压缩 key |
| maxCacheSize | number | 100 | 单策略在途记录硬上限,满额时新 key 拒绝 |
| inFlightTtl | number | 0 | 在途记录兜底存活时间;默认关闭,只在 settle 或 clearState() 时释放 |
| message | string | '请勿重复提交' | 默认提示文案 |
| showToast | boolean | true | 是否触发 notify |
| captureStack | boolean | true | 是否采集调用栈 |
| keyGenerator | function | null | 自定义 key 生成函数 |
| formDataResolver | function | null | FormData 自定义序列化 |
| blobResolver | function | null | Blob 自定义序列化 |
| idempotencyKey | boolean / string / function | true | 幂等 key 生成规则 |
| idempotencyKeyHeader | object / boolean | { enabled: false, headerName: 'Idempotency-Key' } | 幂等 header 注入配置 |
| responseResolver | function | (response) => response | 复用结果返回给等待方前转换响应 |
| errorResolver | function | (error) => error | 复用结果返回给等待方前转换错误 |
showToast 与 notify
showToast: true 只是"允许触发 notify",真正的 UI 提示由你配置的 notify 回调负责。若没配 notify,重复请求仍会被正常拦截(并打 duplicate.triggered 日志,开发环境默认 logger 会在控制台显示),但不会有任何 toast。要弹提示,请在 setupRequestGuard 时配 notify(见消息与日志系统)。
hashAlgorithm 非法值静默降级
hashAlgorithm 等枚举型配置传了非法值(如拼写错误)时会静默降级为默认值,但开发环境下会通过内部错误通道输出一条告警,帮助定位拼写问题;生产环境保持静默。
从旧版默认 block 升级
本次迭代后,duplicate 默认策略是 ignore。如果你的项目依赖旧默认 block 的 .catch(RequestGuardBlockedError) / .finally() 来关闭 loading、重置按钮或展示提示,请显式配置:
setupRequestGuard(axios, {
defaults: {
duplicate: { strategy: 'block' }
}
});错误类型
| 错误名 | 触发时机 |
|---|---|
RequestGuardBlockedError | block 策略命中重复请求 |
RequestGuardCancelledError | reuse 等待方被 clearState() 主动取消 |
RequestGuardCacheEvictedError | duplicate 容量满拒绝新 key,或启用 inFlightTtl 后后台状态维护清理滞留 reuse 记录时通知等待方结束 |
ignore 命中重复请求不会抛错,因此不会出现在错误表里。
更多错误处理见 错误类型。

