Skip to content

常见业务场景

从真实业务出发,开箱即用的配置参考。每个场景都可独立使用,也可组合。

🛒 场景一:电商下单防重

用默认 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 + 禁用
  • 局部骨架屏(请求中显示骨架,结束显示数据)
  • 弹窗的提交态(防止用户关闭弹窗)

🔗 更多

基于 MIT 许可发布