Bootstrap 5 的脚本层全部是原生 JavaScript(无 jQuery),每个交互组件(modal、dropdown、tab、collapse、carousel、toast、tooltip、popover、offcanvas)提供两种接线:data 属性声明式(HTML 里写
data-bs-*即用)与脚本 API 命令式(new bootstrap.Modal实例化后编程控制),外加事件流(show.bs.modal等)供外部联动。
位置:进阶四案第一案。第 3 章用了 data 属性这条「易用线」,本节补上脚本 API 与事件这条「可控线」——动态内容、联动逻辑、单页应用里缺它不可。

// 声明层:3.4 节已用过的按钮,回顾其接线成本为零 // <button data-bs-toggle="modal" data-bs-target="#box">打开</button> // 实例层:命令式控制——适合「表单校验通过后才弹」的场景 const modalEl = document.querySelector('#confirmModal'); const modal = new bootstrap.Modal(modalEl, { backdrop: 'static', // 点遮罩不关闭:危险操作防手滑 keyboard: false // Esc 不关闭:同上 }); // modal.show(); modal.hide(); // 按需调用 // 实例层的另一入口:getOrCreateInstance——单页框架里防重复实例化 const same = bootstrap.Modal.getOrCreateInstance(modalEl); // 事件层:关闭后清空表单 modalEl.addEventListener('hidden.bs.modal', () => { modalEl.querySelector('form')?.reset(); }); // 拦截关闭:校验未过时叫停 modalEl.addEventListener('hide.bs.modal', (e) => { if (!window.formDirty === false) { /* 例:脏表单未确认 */ e.preventDefault(); } });
三层的分工裁决:声明层管「能不能用」,实例层管「怎么控制」,事件层管「何时联动」。动态插入的 HTML(接口返回后渲染的弹窗)data 属性同样生效——框架的事件委托挂在 document 上,后插入的元素也被认领,这是 5.x 架构的一个安静优点。
// tooltip 是唯一的例外:不自动初始化,需逐个实例化 const tipList = [...document.querySelectorAll('[data-bs-toggle="tooltip"]')]; tipList.forEach(el => new bootstrap.Tooltip(el, { delay: { show: 200, hide: 100 } })); // 单页应用卸载页面时销毁实例,防止监听器泄漏 tipList.forEach(el => bootstrap.Tooltip.getOrCreateInstance(el).dispose());
<!-- tooltip 的声明面:title 即内容,placement 即方位 --> <button class="btn btn-secondary" data-bs-toggle="tooltip" data-bs-title="延迟 200 毫秒出现" data-bs-placement="bottom"> 悬停看提示 </button>
dispose 纪律:长生命周期页面里反复创建实例(每次点击 new 一个 Modal)会累积监听器。用 getOrCreateInstance 复用、离场时 dispose 销毁,是脚本层的资源守则。
⚠️ 常见坑:弹层(dropdown、tooltip、popover)依赖 Popper 做定位。引了不含 Popper 的精简脚本又用了弹层,控制台会直接抛错;判给
bootstrap.bundle版或按 6.4 节模块化按需引@popperjs/core。
背景:管理列表的「删除」按钮,要求:点击先弹确认框;确认后按钮转加载态;接口成功才关框,失败留在框内显示错误。
操作:
<button class="btn btn-sm btn-outline-danger" data-bs-toggle="modal" data-bs-target="#delModal" data-bs-id="1042">删除</button> <div class="modal fade" id="delModal" tabindex="-1" aria-hidden="true"> <div class="modal-dialog modal-dialog-centered modal-sm"> <div class="modal-content"> <div class="modal-body">确认删除该记录?</div> <div class="modal-footer"> <button class="btn btn-secondary" data-bs-dismiss="modal">取消</button> <button class="btn btn-danger" id="delConfirm">确认</button> </div> </div> </div> </div>
let pendingId = null; const box = document.getElementById('delModal'); box.addEventListener('show.bs.modal', (e) => { pendingId = e.relatedTarget?.dataset.id; // relatedTarget 即触发按钮 }); document.getElementById('delConfirm').addEventListener('click', async (e) => { const btn = e.currentTarget; btn.disabled = true; // 加载态:按钮禁用防重复提交 const ok = await fakeDelete(pendingId); // 调用删除接口 btn.disabled = false; if (ok) bootstrap.Modal.getOrCreateInstance(box).hide(); else box.querySelector('.modal-body').textContent = '删除失败,请稍后重试'; }); async function fakeDelete(id) { return Math.random() > 0.3; } // 模拟接口,七成成功率
结果:点删除开框并记下 id;确认后按钮 disabled 加转圈;成功 hide,失败在 modal-body 显示错误并保持开框。
解读:这条链用满了三层——声明层开框、show.bs.modal 事件取上下文(relatedTarget 是官方给触发元素的通道,比全局变量干净)、实例层在异步回调里控制关闭时机。异步流程里「何时关框」必须由脚本判,data 属性的自动关闭帮不上忙,这正是实例层存在的意义。
变式:列表改虚拟滚动、DOM 频繁重建——把开框也改判实例层(监听容器点击,modal.show()),声明层完全退场;交互代码集中在一处,重渲染零接线成本。
hide.bs.* 里可 preventDefault 拦截;getOrCreateInstance 防重复、dispose 防泄漏;下一节把交互层欠下的「焦点与语义」账一次结清。