当滚动元素滚动到顶/底时, 如果其外层还有可滚动的元素,则会触发外层可滚动元素的滚动.
这是个feature, 但有时这个feature会给开发者带来麻烦.
特别是safari, 如果某个可滚动元素设置了-webkit-overflow-scrolling: touch,滑动到头的时候经常会被卡住.
body-scroll-lock
用body-scroll-lock可以解决这个问题.当滚动到元素边界时,阻止继续滚动.
GitHub - willmcpo/body-scroll-lock: Body scroll locking that just works with everything 😏
See the Pen vMYyQg by aaronbird (@aaronbird) on CodePen.
原理
监控可滚动元素的touchmove事件, 判断手指的滑动方向和元素的滚动距离.
如果元素滚动到顶时用户继续下拉,则将scrollTop重置为零,同时调用event.preventDefault阻止页面滚动/刷新等默认动作.
如果元素滚动到底时用户继续上滑, 则将scrollTop重置为最大可滚动距离, 同时调用preventDefault.
注意: 如果将tochmove事件挂在window document document.body这三个元素上,则必需设置addEventListener的第三个参数的passive属性为false.否则浏览器会忽略事件函数中调用的preventDefault.
代码
const fixScroll = (scrollEl) => {
let startY
scrollEl.addEventListener('touchstart', function (event) {
// 如果多于1根手指点击屏幕,则不处理
if (event.targetTouches.length > 1) {
return
}
// 储存手指的初始位置
startY = event.targetTouches[0].clientY
}, false)
scrollEl.addEventListener('touchmove', function (event) {
if (event.targetTouches.length > 1) {
return
}
// 判断手指滑动方向, y大于0时向下滑动, 小于0时向上滑动
const y = event.targetTouches[0].clientY - startY
// 如果到顶时继续向下拉
if (scrollEl.scrollTop <= 0 && y > 0) {
// 重置滚动距离为最小值
scrollEl.scrollTop = 0
// 阻止滚动
event.preventDefault()
}
// 如果到底时继续上滑
const maxScrollTop = scrollEl.scrollHeight - scrollEl.clientHeight
if (maxScrollTop - scrollEl.scrollTop <= 0 && y < 0) {
scrollEl.scrollTop = maxScrollTop
event.preventDefault()
}
}, {
passive: false
})
}
See the Pen fix scroll 例子 by aaronbird (@aaronbird) on CodePen.
定位元素下的滚动穿透
如果定位元素下有滚动元素,也会发生滚动穿透现象.
如上图所示, 半透明的上层元素是一个fixed元素.即使在触发touchmove事件时调用event.stopPropagation,也不能阻止滚动穿透.
另外要注意,即便用了fixScroll函数, 也必须在touchmove事件中调用event.stopPropagation.否者滑到底时会卡住.
原因是touchmove事件穿透到下层去了
参考
javascript - iOS 10 Safari: Prevent scrolling behind a fixed overlay and maintain scroll position - Stack Overflow
在 iOS 下使用 iframe 的种种问题 - xiaOp的博客