解决模态窗穿透,整个页面内容区域滑动问题

tua-body-scroll-lock

解决模态窗滑动时候,穿透到背景,背景被滑动的问题

WX20191227-123602

示例:使用的 vue

1
2
3
4
5
6
7
8
9
10
11
12
import { lock, unlock } from "tua-body-scroll-lock";

...
mounted() {
// 禁止滑动后还需要内部可以滚动的元素
console.warn(this.$refs["rule-list"]);
lock(this.$refs["rule-list"]);
},
beforeDestroy() {
unlock(this.$refs["rule-list"]);
}
...

看一下 tua-body-scroll-lock 库做了什么

首先是 lock 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const lock = (targetElement?: HTMLElement) => {
if (isServer()) return // typeof window === 'undefined' 判断是否在浏览器环境

checkTargetElement(targetElement) // 看起来没做什么事情

if (detectOS().ios) {
// 判断 ua 是否是 ios

if (targetElement && lockedElements.indexOf(targetElement) === -1) {
// 如果有不想被锁定的元素,则为元素添加 touchstart 和 touchmove 事件
targetElement.ontouchstart = event => {
initialClientY = event.targetTouches[0].clientY
initialClientX = event.targetTouches[0].clientX
}

targetElement.ontouchmove = event => {
if (event.targetTouches.length !== 1) return
// 其实就是阻止默认行为
handleScroll(event, targetElement)
}
// 添加到 lock 数组里,表示处理完了
lockedElements.push(targetElement)
}

if (!documentListenerAdded) {
// 为 document 添加 touchmove 阻止默认行为,参数有 passive: false ,就禁止了 touchmove 的默认行为了
document.addEventListener(
'touchmove',
preventDefault,
eventListenerOptions
)
documentListenerAdded = true
}
} else if (lockedNum <= 0) {
unLockCallback = detectOS().android
? setOverflowHiddenMobile()
: setOverflowHiddenPc()
}

lockedNum += 1
}

看一下 unlock 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const unlock = (targetElement?: HTMLElement) => {
if (isServer()) return // typeof window === 'undefined' 判断是否在浏览器环境

checkTargetElement(targetElement)
lockedNum -= 1

if (lockedNum > 0) return
if (!detectOS().ios && typeof unLockCallback === 'function') {
unLockCallback()
return
}

// 移除元素的 touchmove 和 touchstart
if (targetElement) {
const index = lockedElements.indexOf(targetElement)

if (index !== -1) {
targetElement.ontouchmove = null
targetElement.ontouchstart = null
lockedElements.splice(index, 1)
}
}

if (documentListenerAdded) {
// 移除 document 的监听
document.removeEventListener(
'touchmove',
preventDefault,
eventListenerOptions
)
documentListenerAdded = false
}
}

问题产生的原因

滚动穿透不是浏览器的 bug。我们在页面上加了一个遮罩层并不会影响 document 滚动事件的产生。也就是说遮罩层虽然不可滚动,但是这个时候浏览器会去触发 document 的滚动从而导致了下方文档的滚动。也就是说如果 document 也不可滚动了,也就不会有这个问题了。

简述 tua-body-scroll-lock 中的实现方式

ios

  1. 禁止 document 默认行为
1
2
3
4
5
6
7
8
9
document.addEventListener(
"touchmove",
function preventDefault(event) {
event.preventDefault();
},
{
passive: false
}
);
  1. 模态窗内还需要可滑动的元素,需要阻止其冒泡(如果没有阻止冒泡会触发到 document 的 touchmove,之后被禁止默认行为,导致页面无法滚动)
1
2
3
target.ontouchmove = function (event) {
event.stopPropagation();
};

满足上面 2 个条件以后,虽然可以禁止背景滚动,但是当模态窗内容区域滑动到底部、或者顶部的时候,还是可以导致背景滑动。

  1. 所以还得在内容区域里的内容滚动到底部或者顶部的时候,就禁止默认行为
1
2
3
4
5
6
7
target.ontouchmove = function (event) {
...
if (满足条件) {
event.preventDefault();
}
event.stopPropagation();
};

安卓

给 body 和 html 加样式,超出隐藏,并且在 unlock 的时候,主动滚动到之前的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var setOverflowHiddenMobile = function setOverflowHiddenMobile() {
var $html = $('html');
var $body = $('body');
var scrollTop = $html.scrollTop || $body.scrollTop;
var htmlStyle = Object.assign({}, $html.style);
var bodyStyle = Object.assign({}, $body.style);
$html.style.height = '100%';
$html.style.overflow = 'hidden';
$body.style.top = "-".concat(scrollTop, "px");
$body.style.width = '100%';
$body.style.height = 'auto';
$body.style.position = 'fixed';
$body.style.overflow = 'hidden';
return function () {
$html.style.height = htmlStyle.height || '';
$html.style.overflow = htmlStyle.overflow || '';
['top', 'width', 'height', 'overflow', 'position'].forEach(function (x) {
$body.style[x] = bodyStyle[x] || '';
});
window.scrollTo(0, scrollTop);
};
};

##参考资料

解析滑动穿透

Unable to preventDefault inside passive event listener due to target being treated as passive 报错

翻译一下:chrome 监听 touch 类事件报错:无法被动侦听事件 preventDefault,是新版本 chrome 浏览器报错。
说明:说一下这个 preventDefault()是个什么鬼,这个是取消默认事件的,如果这个函数起作用的,比如默认的表单提交,a 链接的点击跳转,就不好用了。
原因:google 浏览器为了最快速的相应 touch 事件,做出的改变。
历史:当浏览器首先对默认的事件进行响应的时候,要检查一下是否进行了默认事件的取消。这样就在响应滑动操作之前有那么一丝丝的耽误时间。
现在:google 就决定默认取消了对这个事件的检查,默认时间就取消了。直接执行滑动操作。这样就更加的顺滑了。
解决方案:

1、注册处理函数时,用如下方式,明确声明为不是被动的
window.addEventListener(‘touchmove’, func, { passive: false })

2、应用 CSS 属性 touch-action: none; 这样任何触摸事件都不会产生默认行为,但是 touch 事件照样触发。
touch-action 还有很多选项,