必须手动控制 disabled 属性、替换内容、并在 finally 块中统一恢复;bootstrap 仅提供 spinner-border 样式,不处理状态流转。

必须手动控制 disabled 属性、替换内容、并在 finally 块中统一恢复——Bootstrap 本身不处理状态流转,只提供 spinner-border 样式和基础类。
点击后按钮还能重复提交?检查是否漏设 disabled
常见错误是只加 spinner-border 类或只改文字,但没真正禁用按钮。浏览器原生 disabled 是唯一能拦截鼠标点击、键盘回车、屏幕阅读器操作的手段。
-
button.disabled = true是唯一可靠方式;setAttribute('disabled', 'true')在部分场景下不触发样式重绘 - 别信
opacity: 0.6或pointer-events: none单独使用——前者纯视觉,后者不阻断键盘焦点和回车提交 - 移动端 WebView 对
disabled样式支持不一,建议额外加style="pointer-events: none;"防穿透 - 必须同步设置
aria-disabled="true"和aria-busy="true",否则读屏器无法感知状态变化
按钮高度突变、文字上移?用 position: absolute 居中 spinner
spinner-border 是行内元素,默认参与流式布局,会撑高按钮容器,导致原有文字被“顶上去”。这不是 bug,是 CSS 渲染机制决定的。
- 给按钮加
position: relative和overflow: hidden - 创建
spinner-border元素后,用position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)居中 - 原始文字建议包在
<span class="btn-text"></span>里,加载时设display: none,避免堆叠 - 尺寸别写死(如
2rem),改用1.5em(对应btn-md)或1.2em(对应btn-sm)
怎么安全地禁用 + 插入 + 恢复?三步必须同步且进 finally
漏掉任意一种分支(成功、失败、取消、超时),按钮就永远卡在 Loading 或 disabled 状态。90% 的 loading 按钮 bug 出在这里。
- 点击瞬间就要执行:
btn.disabled = true、btn.querySelector('.btn-text').style.display = 'none'、再appendChild(spinnerEl) - 提前存原始内容:
btn.dataset.originalHtml = btn.innerHTML(保留图标、data-*等结构) - 恢复动作必须放在
fetch().finally()或try/catch/finally中:btn.disabled = false、移除spinnerEl、还原innerHTML - 别用
innerHTML += '...'拼接,多次点击会导致 DOM 节点层层嵌套;清空再赋值更安全
用 spinner-border 还是 spinner-grow?看交互优先级
不是看哪个“好看”,而是看用户能否在 0.3 秒内无意识识别出“正在加载”——这是交互效率问题。
-
spinner-border有明确旋转方向 + 环形轮廓,识别率高,适合主操作按钮(提交、保存、下单) -
spinner-grow是中心脉冲缩放,动画轻,在弱光屏、小尺寸按钮或色弱场景下容易被忽略,更适合工具栏里的次要动作(如刷新列表) - 两者都必须加
role="status"和aria-hidden="true",否则读屏器会重复读 “loading loading” - 大尺寸不用
spinner-border-lg(不存在),改用内联样式:style="width: 1.5rem; height: 1.5rem; border-width: 0.1875rem;"
最容易被忽略的是:恢复逻辑不在 finally 里,或没清空 spinner 节点导致多次点击后 DOM 越叠越多;还有就是用 <a></a> 标签代替 <button></button>,disabled 完全无效。











