基于uni-app的微信小程序Map事件穿透处理
背景
业务需要在微信小程序中使用地图组件,上面需要有点位及点位的交互,同时地图上也会有一些悬浮的按钮、弹窗之类的。在微信小程序2.8.x的版本之后,地图这种原生组件是支持同层渲染的,也就是可以通过样式控制层级。在开发者工具中表现正常,但是上了真机后会发现点击事件会穿透(其实主要是marker的事件)。于是在微信社区找解决方案,结果又回到了用cover-image和cover-view的方式。这样又无法满足一些弹层(比如弹层中需要评论功能,需要使用input)的场景。
问题描述
demo
<template><view class="map_page_container"><mapclass="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>
表现
// 下面圈表示点击区域,数字对应列表的序号
1.单独点击地图空白区域,会输出 “click map” 2.点击marker,会输出"click marker"
3.移动marker到button下面,点击button,会输出"click marker; click button; do nothing;"
4.移动marker到div的下面,点击marker区域,会输出"click marker; do nothing;"
5.移动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"><mapclass="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; // 延迟100msthis.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的生态。主要目的是为了跟大家讨论一下,我这个比较粗暴如果有优雅一点的方式欢迎提出。
|