Skip to content

能力:防重复请求(Duplicate)

防止同一请求被重复发送。默认 ignore 静默忽略重复调用,需要显式错误时用 block,查询合并时用 reuse。

策略选择

策略首次请求重复请求适合场景
ignore(忽略,默认)正常发送返回永远 pending 的 Promise,不触发 .then() / .catch() / .finally()表单提交、下单、支付、已有成功/失败副作用的项目
block(阻断)正常发送直接拒绝,抛出 RequestGuardBlockedError需要业务显式感知重复提交
reuse(复用)正常发送等待首次请求结果,共享响应列表查询、数据加载、配置查询

单业务场景配置

ignore 策略:重复请求静默挂起

这是默认策略,适合表单提交、创建订单、支付等写操作。重复调用不会发出请求,也不会触发调用方的成功、失败或 finally 逻辑:

javascript
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 策略:重复请求直接报错

适合需要业务显式感知重复提交的场景:

javascript
await axios.post('/api/order/submit', orderData, {
  requestGuard: {
    duplicate: {
      strategy: 'block',
      message: '订单提交中,请稍候'
    }
  }
});

// 重复调用会 reject RequestGuardBlockedError,请求未发出

reuse 策略:多个请求共享同一响应

适合列表查询、配置加载等读操作:

javascript
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。

javascript
// ❌ 不可靠:短路错误不会进入这里
axios.interceptors.response.use(null, (error) => {
  if (error.name === 'RequestGuardBlockedError') { Toast.show(error.message); }
  return Promise.reject(error);
});

正确的统一处理出口有两个:

  1. notify 出口(推荐做 toast/提示):ignore / block / reuse 命中重复请求都会触发 notify,无需业务侧 catch:
javascript
setupRequestGuard(axios, {
  notify: (payload) => {
    if (payload.capability === 'duplicate') Toast.show(payload.message);
  }
});
  1. 请求级 .catch(需要按错误类型分支时):短路错误会从发起调用的 Promise 直接 reject 出来,在调用处用 error.name 判别:
javascript
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 / number0/false 会转成字符串);返回 undefined/null/'' 时回退到默认序列化
hashAlgorithm + hashThresholdkey 过长时自动压缩(auto 模式超过 hashThresholdcyrb53 哈希;manual 不压缩)
compareHeaders / headerFields让指定 header 参与判重,适合区分租户、门店等上下文
formDataResolver自定义 FormData 参与判断的值,避免默认占位符把不同表单请求都视为相同
blobResolver自定义 Blob 参与判断的值,避免不可读二进制影响判断稳定性
javascript
requestGuard: {
  duplicate: {
    strategy: 'ignore',
    // 以业务订单 ID 作为判重依据
    keyGenerator(config) {
      return `order:${config.data.clientOrderId}`;
    }
  }
}

配置项

请求级配置(requestGuard.duplicate

配置项类型默认值说明
enabledbooleantrue本次请求是否启用防重
strategystring继承全局策略:ignore / block / reuse
compareFieldsstring[]继承全局判重比较字段
compareHeadersstring[][]额外参与判重的 header
hashAlgorithmstring继承全局哈希算法(auto / cyrb53 / manual)
hashThresholdnumber继承全局超过此长度自动压缩 key
maxCacheSizenumber继承全局单策略在途记录硬上限,满额时新 key 拒绝
inFlightTtlnumber继承全局在途记录兜底存活时间,毫秒;0 表示关闭
messagestring继承全局命中时提示文案
showToastboolean继承全局是否触发 notify
captureStackboolean继承全局是否采集调用栈
keyGeneratorfunction继承全局自定义 key 生成函数
formDataResolverfunction继承全局FormData 自定义序列化
blobResolverfunction继承全局Blob 自定义序列化
idempotencyKeyboolean / string / function继承全局幂等 key 生成规则

全局默认配置(defaults.duplicate

配置项类型默认值说明
enabledbooleantrue能力总开关
strategystring'ignore'默认策略
compareFieldsstring[]['method','baseURL','url','params','data']判重比较字段
excludeMethodsstring[]['GET','OPTIONS','HEAD']规则匹配排除的方法
compareHeadersstring[][]进入判重的 header 白名单(headerFields 是其别名,效果相同)
hashAlgorithmstring'auto'哈希算法
hashThresholdnumber1000超过此长度压缩 key
maxCacheSizenumber100单策略在途记录硬上限,满额时新 key 拒绝
inFlightTtlnumber0在途记录兜底存活时间;默认关闭,只在 settle 或 clearState() 时释放
messagestring'请勿重复提交'默认提示文案
showToastbooleantrue是否触发 notify
captureStackbooleantrue是否采集调用栈
keyGeneratorfunctionnull自定义 key 生成函数
formDataResolverfunctionnullFormData 自定义序列化
blobResolverfunctionnullBlob 自定义序列化
idempotencyKeyboolean / string / functiontrue幂等 key 生成规则
idempotencyKeyHeaderobject / boolean{ enabled: false, headerName: 'Idempotency-Key' }幂等 header 注入配置
responseResolverfunction(response) => response复用结果返回给等待方前转换响应
errorResolverfunction(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、重置按钮或展示提示,请显式配置:

javascript
setupRequestGuard(axios, {
  defaults: {
    duplicate: { strategy: 'block' }
  }
});

错误类型

错误名触发时机
RequestGuardBlockedErrorblock 策略命中重复请求
RequestGuardCancelledErrorreuse 等待方被 clearState() 主动取消
RequestGuardCacheEvictedErrorduplicate 容量满拒绝新 key,或启用 inFlightTtl 后后台状态维护清理滞留 reuse 记录时通知等待方结束

ignore 命中重复请求不会抛错,因此不会出现在错误表里。

更多错误处理见 错误类型

基于 MIT 许可发布