基于uni-app的微信小程序Map事件穿透处理
背景
业务需要在微信小程序中使用地图组件,上面需要有点位及点位的交互,同时地图上也会有一些悬浮的按钮、弹窗之类的。在微信小程序2.8.x的版本之后,地图这种原生组件是支持同层渲染的,也就是可以通过样式控制层级。在开发者工具中表现正常,但是上了真机后会发现点击事件会穿透(其实主要是marker的事件)。于是在微信社区找解决方案,结果又回到了用cover-image和cover-view的方式。这样又无法满足一些弹层(比如弹层中需要评论功能,需要使用input)的场景。
问题描述
demo
<template>
<view class="map_page_container">
<map
class="map"
@tap="handleClickMap"
@markertap="handleClickMarker"
latitude="30.654228"
longitude="104.066558"
:markers="markersList"
></map>
<!-- 测试弹层穿透情况 -->
<div class="float_container" @tap="handleClickPrevent">
<!-- input用于测试focus时的情况 -->
<input />
<!-- 按钮用于测试点击穿透情况 -->
<button @tap="handleClickButton">测试按钮</button>
</div>
</view>
</template>
<script>
export default {
data() {
return {
markersList: [
{
id: '1',
latitude: '30.654228',
longitude: '104.066558',
iconPath: '/static/images/ge/guide.png',
},
],
};
},
methods: {
handleClickMap() {
console.log('click map');
},
handleClickButton() {
console.log('click button');
},
handleClickMarker(event) {
console.log('click marker', event);
},
handleClickPrevent() {
console.log('do nothing');
},
},
};
</script>
<style lang="scss" scoped>
.map_page_container {
width: 100%;
height: 100vh;
position: relative;
overflow: hidden;
.map {
width: 100%;
height: 100%;
}
.float_container {
width: 100%;
height: 500rpx;
position: fixed;
bottom: 0;
left: 0;
z-index: 1;
opacity: 0.5; // 方便看marker的位置
background-color: red;
}
}
</style>
表现
// 下面圈表示点击区域,数字对应列表的序号
- 单独点击地图空白区域,会输出 "click map"
- 点击marker,会输出"click marker"
- 移动marker到button下面,点击button,会输出"click marker; click button; do nothing;"
- 移动marker到div的下面,点击marker区域,会输出"click marker; do nothing;"
- 移动marker到input的位置,选中input,会输出"do nothing"
从上面的表现可以看出,marker的层级是最高的,marker的事件不会穿透到map上,其他的dom会按冒泡的顺序执行事件(这里的事件指click或者tap,其他的还没有测试)。选中input不会触发marker的事件。
解决方案
思路
为了避免事件穿透,首先想到的肯定是preventDefault或者stopPropagation去阻止事件冒泡或者捕获(或者vue中的修饰符.stop之类的,道理一样就不说了)。但是从上面的表现也可以看出来,marker本身并不在冒泡或者捕获过程中,它就是最先触发的。经过验证也确实不能阻止它的tap事件触发。
起初我是想通过target来判定执行事件(事件代理),在通过给dom添加data-来判断执行顺序。但是发现marker与常规dom并不同。最后,我就想通过JS逻辑去限制事件函数的执行。注意,这里并不是阻止事件执行,我们没有办法阻止tap事件的执行。
基本逻辑就是通过执行优先级来决定执行谁,只要执行了某一个,其他的就不执行了。具体的我通过代码来表示吧。
demo
<template>
<view class="map_page_container">
<map
class="map"
@tap="handleClickMap"
@markertap="handleClickMarker"
latitude="30.654228"
longitude="104.066558"
:markers="markersList"
></map>
<!-- Weex在view组件中增加了eventPenetrationEnabled属性,当值为true(默认为false)时,View的子View仍能正常响应事件,但View自身将不会响应事件。 ——摘自uniapp文档 -->
<div class="float_container" @tap="handleClickPrevent" eventPenetrationEnabled="true">
<input />
<!--.stop只是处理dom的事件捕获 和map本身没有关系 -->
<button @tap.stop="handleClickButton">测试按钮</button>
</div>
</view>
</template>
<script>
export default {
data() {
return {
clickEventList: [], // 记录点击事件
timer: null, // 定时器
markersList: [
{
id: '1',
latitude: '30.654228',
longitude: '104.066558',
iconPath: '/static/images/ge/guid.png',
},
],
};
},
watch: {
clickEventList(c, p) {
if (c.length > 0) {
const temp = [...c];
// 根据level进行排序,执行优先级最高的
const cur = temp.sort((a, b) => a.level - b.level)[0];
this.timerGenerator(cur.func);
}
},
},
methods: {
// 处理定时器
timerGenerator(func) {
// 存在则清空定时器
this.timer && clearTimeout(this.timer);
// 存在func则100ms后执行
if (func) {
const TIMEOUT = 100; // 延迟100ms
this.timer = setTimeout(func, TIMEOUT);
}
},
handleClickMap() {
// 点击事件push进数组
this.clickEventList.push({
func: () => {
// 实际的执行函数
console.log('click map');
// 执行完成后清空点击事件列表
// TODO 这个部分可以封装起来对所有事件函数进行处理
this.clickEventList = [];
},
level: 3, // 优先级 越小则优先级越高
});
},
handleClickButton() {
this.clickEventList.push({
func: () => {
console.log('click button');
this.clickEventList = [];
},
level: 1,
});
},
handleClickMarker(event) {
this.clickEventList.push({
func: () => {
console.log('click marker', event);
this.clickEventList = [];
},
level: 2, // TODO marker的level应该为尽可能大的n比如10000,然后map的level则为n+1
});
},
handleClickPrevent() {
// 用于一些div view之类的没有实际执行逻辑的dom上 主要是阻止遮盖marker的时候的事件
this.clickEventList.push({
func: () => {
console.log('do nothing');
this.clickEventList = [];
},
level: 0,
});
},
},
destroyed() {
// 清空定时器
this.timerGenerator();
},
};
</script>
// ...css一样就不写了
经过验证目前可以满足当前业务需求,也就是阻止了map的事件穿透。
TODO
对地图进行封装,入参一个clickEventList,达到逻辑复用的目的。我理解正常情况下,map的tap层级应该是最低的,其次是marker的tap层级(也就是上面注释里的内容)。不过还没有验证,只是有个大概的思路,等验证了再来更新。
写在最后
这里只提供一下我的思路,目前来讲虽然实现了效果,但是不太好用。第一个问题就是需要人为设定执行优先级,就不太友好,最好是能通过dom层级来生成这个优先级,但是在uniapp或者说小程序环境中获取dom一直都不太好用。第二个问题就是这里是通过定时器进行延时,多少还是有点不稳定,比如两个事件触发超过100ms了就拉胯了,虽然正常情况下这种点击事件的穿透不会超过100ms,但是依然是有风险的。
本身是React栈,业务需要才来维护这个uniapp的项目,并不是很熟悉uniapp的生态。主要目的是为了跟大家讨论一下,我这个比较粗暴如果有优雅一点的方式欢迎提出。