一、滚动锁定的必要性
当模态框(Modal)、侧边抽屉(Drawer)等组件被激活时,为防止背景内容滚动造成的视觉干扰和操作失误,通常需要锁定页面滚动。然而,直接设置 body.style.overflow = 'hidden'
会引入以下问题:
- 页面布局跳动: 滚动条突然消失会导致页面内容宽度瞬间变化,引发布局跳动。
- 多弹窗样式冲突: 在多个弹窗嵌套或依次打开的场景下,对
body
样式的直接修改容易产生覆盖或冲突。 - 浏览器兼容性问题: 不同浏览器对滚动条的处理可能存在细微差异,直接操作
body
样式可能无法在所有环境下完美工作。
二、Ant Design 的解决方案:useScrollLocker
Ant Design 通过 useScrollLocker
钩子函数实现了更智能的滚动控制。其核心思路是:
- 隐藏滚动条: 使用
overflow-y: hidden
阻止背景滚动。 - 保留滚动条空间: 通过计算并补偿滚动条宽度,避免布局跳动。
关键源码解析:
javascript
export default function useScrollLocker(lock?: Ref<boolean>) {
const mergedLock = computed(() => !!lock && !!lock.value); // 确定是否需要锁定
uuid += 1; // 生成唯一ID
const id = `${UNIQUE_ID}_${uuid}`; // 构造样式表唯一标识
watchEffect(
onClear => {
if (!canUseDom()) return; // 确保在DOM环境中执行
if (mergedLock.value) {
const scrollbarSize = getScrollBarSize(); // 获取当前滚动条宽度
const isOverflow = isBodyOverflowing(); // 判断body是否原本有滚动条
// 动态注入CSS规则
updateCSS(
`
html body {
overflow-y: hidden; // 核心:隐藏垂直滚动条
${isOverflow ? `width: calc(100% - ${scrollbarSize}px);` : ''} // 核心:补偿滚动条宽度(仅在需要时)
}`,
id // 使用唯一ID标识样式
);
} else {
removeCSS(id); // 移除注入的CSS规则
}
onClear(() => {
removeCSS(id); // 组件卸载时清理样式
});
},
{ flush: 'post' } // 确保在DOM更新后执行
);
}
方案亮点解析:
- 滚动条空间补偿 (
width: calc(100% - ${scrollbarSize}px)
): 这是解决布局跳动的关键。当页面原本有滚动条时 (isOverflow
为true
),通过将body
的宽度计算为视口宽度减去滚动条宽度 (scrollbarSize
),预留出滚动条消失后的空间,保持页面布局稳定。 - 唯一标识 (
uuid
):uuid += 1;
确保每次调用useScrollLocker
生成的 CSS 规则具有唯一标识 (id
)。这解决了多弹窗场景下的样式冲突问题:每个锁定的弹窗独立添加自己的补偿样式规则;只有当所有锁定的弹窗都关闭(所有对应的样式规则被移除)后,页面滚动才会恢复。 - 按需补偿: 只有
isBodyOverflowing()
为true
(即页面原本有垂直滚动条需要占用空间)时,才会应用calc(100% - ${scrollbarSize}px)
的宽度补偿规则,避免不必要的样式覆盖。 - 资源管理: 使用
onClear
和removeCSS
确保组件卸载或锁定状态解除时,能及时清理注入的 CSS 规则,防止内存泄漏和样式残留。
总结
滚动锁定的核心目标是隐藏滚动条 (通过 overflow-y: hidden
)以阻止背景滚动。其关键在于:隐藏滚动条会导致页面内容宽度瞬间增加(等于滚动条宽度),引发布局跳动。
Ant Design useScrollLocker
的解决方案重点在于动态计算并补偿滚动条宽度:
- 使用
getScrollBarSize()
获取精确的滚动条宽度。 - 在需要锁定滚动时,通过设置
body
的宽度为calc(100% - ${scrollbarSize}px)
预留出滚动条消失后的空间,从而保持页面整体宽度不变,完美消除布局跳动。
补充说明: 固定内容宽度、避免跳动的另一种常见方案是给 body
添加 padding-right: ${scrollbarSize}px
。其原理也是预留滚动条空间,但实现方式与修改 width
不同。useScrollLocker
采用的是 width
计算的方式。