基于uni-app的微信小程序Map事件穿透处理

1,654 阅读3分钟

基于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>

表现

// 下面圈表示点击区域,数字对应列表的序号

  1. 单独点击地图空白区域,会输出 "click map"
  2. 点击marker,会输出"click marker"

  1. 移动marker到button下面,点击button,会输出"click marker; click button; do nothing;"

  1. 移动marker到div的下面,点击marker区域,会输出"click marker; do nothing;"

  1. 移动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的生态。主要目的是为了跟大家讨论一下,我这个比较粗暴如果有优雅一点的方式欢迎提出。