常见业务场景
从真实业务出发,开箱即用的配置参考。每个场景都可独立使用,也可组合。
🛒 场景一:电商下单防重
用默认 ignore 策略 + 自定义 key + 消息提示,防止同一笔订单被重复创建,同时避免触发调用方 then/catch 副作用:
javascript
import axios from 'axios';
import { setupRequestGuard } from '@hydd/request-guard';
setupRequestGuard(axios, {
notify: (payload) => Toast.show(payload.message),
defaults: {
duplicate: { strategy: 'ignore', message: '订单处理中,请勿重复提交' }
}
});
const submitOrder = async (orderData) => {
return axios.post('/api/order/create', orderData, {
requestGuard: {
duplicate: {
strategy: 'ignore',
// 以业务订单 ID 作为判重依据,同一订单 ID 只允许提交一次
keyGenerator(config) {
return `order:create:${config.data?.orderId}`;
},
message: '正在提交订单,请稍候'
}
}
});
};📋 场景二:列表查询复用 + 自动重试
用 reuse 策略 + 重试,相同查询自动复用首次结果,失败自动重试:
javascript
setupRequestGuard(axios, {
rules: [
{
url: /\/api\/(list|search|query)/,
method: 'get',
duplicate: {
strategy: 'reuse',
compareFields: ['method', 'url', 'params']
},
retry: { attempts: 3, delay: 500, backoff: 'exponential' }
}
]
});
// 短时间内多次相同查询,只有第一次真正发出请求
// 失败后自动重试,所有等待方最终拿到相同结果
axios.get('/api/list', { params: { page: 1, size: 20 } });
axios.get('/api/list', { params: { page: 1, size: 20 } });⚡ 场景三:支付服务域级熔断
支付服务整体不可用时,整个域名一起熔断,避免无意义请求打爆服务端;半开试探自动恢复:
javascript
const requestManager = setupRequestGuard(axios, {
rules: [
{
url: /\/api\/(payment|order)/,
circuitBreaker: {
type: 'domain', // 域名级:匹配此 URL 的所有请求共享一个熔断单元
failCount: 3, // 连续 3 次失败就熔断
recoverDelay: 60000, // 1 分钟后试探
statusCodes: [502, 503, 504],
storage: 'session' // 刷新页面后状态保留
}
}
]
});
// 手动查看熔断状态
const state = requestManager.circuitBreaker.getState('POST:/api/payment/create');
// → { state: 'CIRCUIT_BREAKER', openedAt: 1717315200000, failCount: 3, ... }
// 维护后手动重置
requestManager.circuitBreaker.reset('POST:/api/payment/create');跨端持久化
session / indexedDB 只在浏览器环境生效。小程序、RN、SSR 运行时会自动降级为内存存储,详见 熔断守护 - 跨端持久化说明。
🏢 场景四:多租户 / 多门店 header 去重
同一接口在不同租户/门店下应视为不同请求。用 compareHeaders 让指定 header 参与判重:
javascript
setupRequestGuard(axios, {
defaults: {
duplicate: {
strategy: 'ignore',
// 让 tenant-id 和 store-id 参与 key 生成
// 即使 url 和 body 相同,不同租户的请求也不会被判为重复
headerFields: ['X-Tenant-Id', 'X-Store-Id']
}
},
rules: [
{ method: 'post', duplicate: true }
]
});
// 不同租户的相同下单请求互不干扰
axios.post('/api/order', data, {
headers: { 'X-Tenant-Id': 'tenant-a' }
});
axios.post('/api/order', data, {
headers: { 'X-Tenant-Id': 'tenant-b' }
});📱 场景五:小程序表单防重 + 包体优化
小程序对包体积敏感,从 core 按需引入能力,只打包真正用到的部分:
javascript
import { setupRequestGuard } from '@hydd/request-guard/core';
import { duplicate } from '@hydd/request-guard/capabilities/duplicate';
const request = setupRequestGuard(wxRequest, {
capabilities: [duplicate()],
notify: (payload) => wx.showToast({ title: payload.message, icon: 'none' }),
rules: [
{ method: 'post', duplicate: { strategy: 'ignore', message: '正在提交中' } }
]
});
await request({
url: '/order/submit',
method: 'POST',
data: { orderId: '12345' }
});不想手动封装 wx.request 的 Promise 形式?用平台快捷入口:
javascript
import { createWechatRequestGuard } from '@hydd/request-guard/platforms/wechat';
export const request = createWechatRequestGuard(wx.request, {
capabilities: [duplicate()],
baseURL: 'https://api.example.com',
getHeaders() { return { token: wx.getStorageSync('token') }; },
notify(payload) { wx.showToast({ title: payload.message, icon: 'none' }); },
rules: [{ method: 'post', duplicate: true }]
});🔄 场景六:SSR 配置拉取重试
SSR 场景下配置接口失败会导致整页渲染失败,给配置拉取加重试保护:
javascript
setupRequestGuard(axios, {
rules: [
{
// 配置类接口:多试几次,线性退避
url: /\/api\/(config|dictionary|settings)/,
retry: {
attempts: 5,
delay: 300,
backoff: 'linear' // 300ms → 600ms → 900ms → 1200ms
}
},
{
// 幂等的 POST 操作(带幂等 key)也可以重试
url: /\/api\/sync/,
method: 'post',
retry: (config) => {
// 只有带 Idempotency-Key 的请求才重试
if (config.headers?.['Idempotency-Key']) {
return { attempts: 3, delay: 500 };
}
return false;
}
}
]
});📤 场景七:文件上传 FormData 去重
文件上传默认会被视为相同请求(FormData 无法稳定序列化)。用 formDataResolver 自定义判重值:
javascript
setupRequestGuard(axios, {
rules: [
{
url: /\/api\/upload/,
method: 'post',
duplicate: {
strategy: 'ignore',
message: '文件上传中,请勿重复提交',
// 用文件名 + 大小 + 修改时间作为判重依据
formDataResolver(data) {
const entries = [];
data.forEach((value, key) => {
if (value instanceof File) {
entries.push(`${key}:${value.name}:${value.size}:${value.lastModified}`);
} else {
entries.push(`${key}:${value}`);
}
});
return entries.join('|');
}
}
}
]
});🏭 场景八:生产环境完整配置
完整的生产环境配置示例(自定义日志 + 熔断保护):
javascript
import { setupRequestGuard, RequestGuardLogger, ConsoleRequestGuardLogger } from '@hydd/request-guard';
// 自定义 Logger:接入你自己的监控平台(可选)
class RemoteLogger extends RequestGuardLogger {
write(event) {
// event: { level, event, message, namespace, timestamp, data }
monitor.report(event.event, event.data);
}
}
const requestManager = setupRequestGuard(axios, {
notify: (payload) => Toast.show({ content: payload.message, type: 'warning' }),
// 生产环境用自定义日志,开发环境用内置 Console
logger: process.env.NODE_ENV === 'production'
? new RemoteLogger({ devOnly: false, level: 'warn' })
: new ConsoleRequestGuardLogger({ devOnly: true }),
defaults: {
retry: { attempts: 3, delay: 200, backoff: 'exponential' },
circuitBreaker: { failCount: 5, window: 60000, recoverDelay: 30000, storage: 'session' }
},
rules: [
{ method: 'post', duplicate: true },
{ url: /\/api\/(payment|order|user)/, circuitBreaker: true }
]
});🔘 场景九:用 loadingKey 驱动按钮 loading
防重守护已经知道请求在不在途,直接把这个状态复用给按钮 loading,不用自己维护 loading 变量。三个 API 配合使用:
createLoadingKey(label)—— 创建一个唯一的 loading 标识subscribeLoading(key, fn)—— 订阅这个 key 的 loading 状态变化(请求开始变 true,结束变 false)isLoading(key)—— 同步查询当前是否在执行中
javascript
import axios from 'axios';
import {
setupRequestGuard,
createLoadingKey,
subscribeLoading,
isLoading
} from '@hydd/request-guard';
setupRequestGuard(axios, {
dev: true,
rules: [{ method: 'post', duplicate: true }]
});
// 1. 创建一个 loadingKey,label 只是方便调试时辨认
const submitKey = createLoadingKey('submit-order');
// 2. 订阅状态变化,直接驱动 UI
const unsubscribe = subscribeLoading(submitKey, (loading) => {
// loading === true:请求发出中,按钮禁用 + 转圈
// loading === false:请求结束,按钮恢复
submitButton.disabled = loading;
submitButton.classList.toggle('loading', loading);
});
// 3. 发请求时把 loadingKey 绑上去
async function onSubmit() {
await axios.post('/api/order/submit', orderData, {
requestGuard: {
duplicate: { strategy: 'ignore', message: '提交中,请勿重复点击' },
loadingKey: submitKey // 把这次请求的执行态绑定到 submitKey
}
});
}
// 不需要时取消订阅
// unsubscribe();为什么用 loadingKey 而不是自己维护变量?
duplicate 命中重复请求时不会进入新的真实执行态,但你自己的 loading 变量可能已经设成 true 了——会导致按钮一直转圈。loadingKey 只在请求真正进入执行态时才变 true,重复命中的调用不会单独触发,状态天然正确。
适合的场景
- 提交按钮的 loading + 禁用
- 局部骨架屏(请求中显示骨架,结束显示数据)
- 弹窗的提交态(防止用户关闭弹窗)

