GuardButton:与守护层联动的按钮
一个真实项目沉淀的实践:让按钮的 loading / disabled 自动跟随受守护请求的执行生命周期,业务代码从此不再手写 loading 标志位。
需求背景
一个后台系统里有几百个提交按钮,几乎每个提交处理函数都长这样:
js
async handleSubmit() {
this.submitLoading = true; // 手动开
try {
await submitOrder(this.form);
this.$message.success('提交成功');
} finally {
this.submitLoading = false; // 手动关,忘了就永久转圈
}
}问题很典型:
- 重复代码繁多:每个按钮一个标志位、一套 try/finally,页面一多就是成片的模板代码
- 容易漏:
finally忘了复位、异常分支漏了复位,按钮就永久 loading - 只接入请求守护平台还不够:接入 request-guard 后,双击的重复请求确实被拦下了,但按钮本身没有任何状态流转——它不知道请求在途,既不转圈也不禁用,用户看到的还是一个"没反应"的按钮,只能靠 toast 提示兜底。请求层的防重解决了"数据不出错",没有解决"交互有反馈"
GuardButton 解决的就是最后一公里:把守护层已经掌握的"请求真实执行态"接到按钮上。
特点与特性
- 零业务侵入:通过全局注册替换
<el-button>(也可作为独立组件按需使用),存量模板一行不用改 - 行为等价:
guard关闭时与原生按钮完全等价;根节点就是原按钮的 DOM,按钮组 CSS、指令、slot、属性透传全部不受影响 - loading / disabled 双联动:请求在途时按钮自动转圈并禁用,从物理上消灭双击;外部显式传入的
loading/disabled依然生效(取并集) - 全生命周期覆盖:状态由守护层驱动,包含重试等待、响应收尾——不存在"手写标志位提前复位"的问题
- 实例级隔离:每个按钮实例一把
loadingKey,互不串扰;组件销毁自动退订,无泄漏
与守护平台的联动方式
GuardButton 只用了平台的三个公开 Loading API,没有任何私有依赖:
| 环节 | 用到的公开能力 |
|---|---|
| 按钮挂载 | createLoadingKey() 为该实例创建唯一 key |
| 状态订阅 | subscribeLoading(key, listener),订阅瞬间同步回调一次当前态,销毁时调用返回的退订函数 |
| 请求关联 | 业务发请求时在请求配置带上 requestGuard: { loadingKey },守护层在请求开始/结束(含重试全程)驱动订阅回调 |
也就是说:按钮不感知请求,请求不感知按钮,两者只通过一把 key 在守护层会合。详细语义见 Loading 协作 API。
可以让 AI 直接生成
本组件已收录进包内 AI 接入 Skill 的参考案例。装好 Skill 后直接说"给我的项目生成一个 GuardButton / 守护按钮",AI 会先分析你的技术栈(Vue 2 / Vue 3 / React / 原生小程序等)再产出对应实现,而不是照搬下面的案例代码。
完整代码(Vue 2 + Element UI 参考实现)
vue
<template>
<!--
根节点即 ElementButton,保证与原生 el-button DOM 完全一致:
$el 就是 <button>,.el-button-group > .el-button、class/style 透传均不受影响。
-->
<ElementButton
class="guard-button"
v-bind="$attrs"
:loading="finalLoading"
:disabled="finalDisabled"
v-on="$listeners"
>
<slot />
</ElementButton>
</template>
<script>
import { Button as ElementButton } from 'element-ui';
import { createLoadingKey, subscribeLoading } from '@hydd/request-guard';
// 全局劫持 el-button 的守卫按钮:
// 在原生 el-button 之上叠加“请求守卫 loading 联动”,其余行为与 el-button 保持一致。
export default {
name: 'GuardButton',
components: { ElementButton },
// 关闭属性自动继承,改为显式 v-bind="$attrs",避免属性被同时挂到根组件与内部按钮。
inheritAttrs: false,
props: {
// 是否启用守卫 loading 联动;关闭后完全等价于原生 el-button。
guard: {
type: Boolean,
default: true
},
// 显式声明 loading / disabled,交给 Vue 做 Boolean 归一化,
// 修复裸属性写法 <el-button loading> 时 $attrs.loading === '' 被 Boolean('') 误判为 false 的问题。
loading: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
// 对外暴露:业务通过 this.$refs.xxx.loadingKey 拿到 key 并随请求下发。
loadingKey: '',
guardLoading: false,
unsubscribeLoading: null
};
},
computed: {
// 外部显式 loading 与守卫 loading 取并集,任一为真即处于 loading。
finalLoading() {
return Boolean(this.loading || (this.guard && this.guardLoading));
},
// 守卫 loading 期间禁用,防止在途请求被重复触发;外部 disabled 依然生效。
finalDisabled() {
return Boolean(this.disabled || (this.guard && this.guardLoading));
}
},
watch: {
guard: {
immediate: false,
handler(enabled) {
if (enabled) {
this.setupGuardLoading();
} else {
this.teardownGuardLoading();
}
}
}
},
mounted() {
if (this.guard) {
this.setupGuardLoading();
}
},
beforeDestroy() {
this.teardownGuardLoading();
},
methods: {
setupGuardLoading() {
// 已订阅则跳过,避免 guard 反复切换或重复调用造成的多重订阅。
if (this.unsubscribeLoading) {
return;
}
if (!this.loadingKey) {
this.loadingKey = createLoadingKey();
}
this.unsubscribeLoading = subscribeLoading(this.loadingKey, (loading) => {
this.guardLoading = loading;
});
},
teardownGuardLoading() {
if (this.unsubscribeLoading) {
this.unsubscribeLoading();
this.unsubscribeLoading = null;
}
this.guardLoading = false;
}
}
};
</script>全局注册(在 UI 库之后注册即可覆盖模板级 <el-button>):
js
Vue.use(ElementUI);
Vue.component('ElButton', GuardButton); // 劫持 ElButton使用示例
业务侧只需把按钮的 loadingKey 随请求下发:
vue
<template>
<el-button ref="submitBtn" type="primary" @click="handleSubmit">提交审批</el-button>
</template>
<script>
export default {
methods: {
handleSubmit() {
// 不再写任何 loading 标志位,也不需要 try/finally
auditSubmit(this.form, { loadingKey: this.$refs.submitBtn.loadingKey });
}
}
};
</script>js
// api 层把 loadingKey 合入请求配置
export const auditSubmit = (data, { loadingKey } = {}) =>
request({
url: '/inspectionApproval/auditSubmit',
method: 'POST',
data,
requestGuard: { duplicate: true, loadingKey }
});状态流转效果:

注意事项
- UI 库内部按钮不受全局劫持影响:如 MessageBox、el-dialog 默认 footer 里由库内部直接渲染的按钮,不会经过 GuardButton;这些位置的防重仍由请求层防重(duplicate)兜底
ignore策略与按钮联动是互补关系:请求层防重保证"数据不出错",GuardButton 保证"在途期间点不了",同时接入体验最佳- 想在 Vue 3 / React / 原生小程序里实现同款能力,核心只有"创建 key → 订阅 → 随请求下发 → 销毁退订"四步,见 Loading 协作 API,或直接用 AI 接入 Skill 生成

