滑动穿透
一、滑动穿透的本质
滑动穿透指上层元素(如弹窗、浮层)滚动时,下层页面也随之滚动的现象。其核心原因是:
- 事件冒泡:
touchmove
事件从上层元素冒泡到下层。 - 默认行为未被阻止:浏览器默认处理滚动,即使在不需要滚动的区域。
- 滚动容器嵌套:多层可滚动元素导致事件处理冲突。
二、解决方案分类
根据场景和兼容性需求,可选择以下方案:
方案一:全局禁用滚动(简单粗暴)
原理:显示浮层时禁用整个页面滚动,隐藏时恢复。
优点:实现简单,兼容性好。
缺点:滚动位置可能丢失,iOS 体验不佳。
// 显示浮层时
function showOverlay() {
// 保存当前滚动位置
const scrollTop = window.scrollY;
// 禁用页面滚动(关键)
document.body.style.overflow = 'hidden';
document.body.style.position = 'fixed';
document.body.style.top = `-${scrollTop}px`;
document.body.style.width = '100%';
}
// 隐藏浮层时
function hideOverlay() {
const scrollTop = parseInt(document.body.style.top);
// 恢复页面滚动
document.body.style.overflow = '';
document.body.style.position = '';
document.body.style.top = '';
// 恢复滚动位置
window.scrollTo(0, -scrollTop);
}
方案二:局部滚动控制(精细化)
原理:通过事件检测,仅在需要时阻止滚动。
优点:保留页面滚动状态,体验更流畅。
缺点:实现复杂,需处理边界情况。
let startY, scrollTop;
// 显示浮层时
function showOverlay() {
scrollTop = window.scrollY;
// 监听触摸事件
document.addEventListener('touchstart', handleTouchStart);
document.addEventListener('touchmove', handleTouchMove, { passive: false });
}
// 隐藏浮层时
function hideOverlay() {
document.removeEventListener('touchstart', handleTouchStart);
document.removeEventListener('touchmove', handleTouchMove);
}
function handleTouchStart(e) {
startY = e.touches[0].clientY;
}
function handleTouchMove(e) {
const currentY = e.touches[0].clientY;
const diffY = currentY - startY;
// 容器元素(如弹窗内容区)
const container = document.querySelector('.overlay-content');
const containerScrollTop = container.scrollTop;
const containerHeight = container.offsetHeight;
const containerScrollHeight = container.scrollHeight;
// 向上滑动且容器已滑到顶部,阻止继续滑动
if (diffY > 0 && containerScrollTop <= 0) {
e.preventDefault();
}
// 向下滑动且容器已滑到底部,阻止继续滑动
else if (diffY < 0 && containerScrollTop >= containerScrollHeight - containerHeight) {
e.preventDefault();
}
// 容器可滚动时,不阻止
}
方案三:CSS 方案(现代浏览器)
原理:利用 touch-action
属性控制触摸行为。
优点:性能最优,代码简洁。
缺点:iOS Safari 兼容性差(需配合 JS)。
/* 全局样式 */
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
touch-action: none; /* 关键:禁用默认触摸行为 */
}
/* 可滚动内容区 */
.overlay-content {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow-y: auto;
touch-action: pan-y; /* 允许垂直滚动 */
-webkit-overflow-scrolling: touch; /* iOS 滚动优化 */
}
方案四:混合方案(最佳实践)
结合 CSS 和 JS,兼顾性能与兼容性:
// 显示浮层时
function showOverlay() {
// CSS 禁用默认滚动
document.body.classList.add('no-scroll');
// 记录滚动位置
const scrollTop = window.scrollY;
// iOS 特殊处理(防止页面跳回顶部)
if (isIOS()) {
document.body.style.position = 'fixed';
document.body.style.top = `-${scrollTop}px`;
}
}
// 隐藏浮层时
function hideOverlay() {
document.body.classList.remove('no-scroll');
if (isIOS()) {
const scrollTop = parseInt(document.body.style.top);
document.body.style.position = '';
document.body.style.top = '';
window.scrollTo(0, -scrollTop);
}
}
// 判断是否为 iOS
function isIOS() {
return /iPad|iPhone|iPod/.test(navigator.userAgent);
}
/* CSS 样式 */
.no-scroll {
overflow: hidden;
}
/* iOS 滚动优化 */
.overlay-content {
-webkit-overflow-scrolling: touch;
}
三、各方案对比
方案 | 优点 | 缺点 | 兼容性 |
---|---|---|---|
全局禁用滚动 | 实现简单 | 滚动位置丢失,iOS 体验差 | 全兼容 |
局部滚动控制 | 保留滚动状态,体验好 | 实现复杂,需处理边界情况 | 全兼容 |
CSS 方案 | 性能最优,代码简洁 | iOS Safari 兼容性差 | 现代浏览器 |
混合方案 | 兼顾性能与兼容性 | 需区分平台逻辑 | 全兼容(iOS 优化) |
四、常见问题与优化
-
iOS 滚动卡顿:
给可滚动元素添加-webkit-overflow-scrolling: touch
启用硬件加速。 -
滚动位置丢失:
使用position: fixed
和top
属性保存滚动位置(如方案四)。 -
模态框嵌套:
维护滚动状态栈,记录每个模态框的滚动位置。 -
性能优化:
使用requestAnimationFrame
优化滚动事件处理,减少重排。
五、总结:如何选择方案?
- 简单场景:优先使用 CSS 方案(
touch-action
),配合 JS 处理 iOS 兼容。 - 复杂场景:使用 混合方案,结合 CSS 和 JS 精细化控制。
- 性能敏感场景:使用 局部滚动控制,避免全局禁用滚动。
通过合理选择方案,可彻底解决滑动穿透问题,同时保证良好的用户体验和兼容性。