# 源码分析

# 源码结构

整个组件的封装 几乎可以拆分成两部分: 针对mapbox对象的常规基本封装 & 关键事件封装

# 常规基本封装

Vue组件的三大件: props、data、watch我认为几乎是按照要封装的mapbox的对象照本画科,几乎都一个模子,再加上地图开发必需要的map对象和整个脚本对象(这两个通过注入inject就可以了) 如下所示:

<script>
export default {
    props:{
        // 封装原生mapbox的属性
    },
    data() {
        // 记录是否初识化&封装的mapbox对象
    },
    watch:{
        // 观察原生mapbox的属性
    },
    inject: ["mapbox", "map"],  //
}
</script>

# 关键事件机制

由于整个组件是对 marker对象进行封装,有可能自定义插槽的时候渲染HTML元素。 因此,除了处理本身Vue的事件机制以外,还需要处理Map的事件,以及HTML-DOM的事件三种:

  1. _emitEvent 处理Vue事件,告诉Vue,我这个marker已经在地图里面加载完毕了 等于是说 我这个封装的vue-marker组件已经生效加载了, 因此这里发送是的Vue的事件
    // 这里的add是自己定义的,可以随便取
    this.$_emitEvent("added", { marker: this.marker });
    
  2. _emitMapEvent 处理地图事件,由于marker在地图里面是可以拖拽的,查看官方文档 (opens new window)也可以发现marker局有三种地图事件dragstart,drag,dragend因此这三种事件需要处理成地图事件。从事件机制本身来说最后都是采取的vue.emit()机制,但针对地图事件做了封装,将该组件和map对象作为data的一部分封装。
    // 这里的name和 marker本身的事件名[dragstart,drag,dragend]建议一致
    this.$emit(name, {
        map: this.map, //传递map对象让开发者按照自己的业务逻辑做事情
        component: this, //传递组件本身,万网用来获取marker自身的属性
        ...data //事件的真正payload
      });
    
  3. _emitDOMEvent 原生DOM事件在代码里面是藏在bindMarkerDOMEvents内部实现的。

    核心原理是:marker本身的html-element元素不是显式的存在Vue的<template>模板中的,而是map对象自己创建的一个内部html-element元素。 因此不能直接的向Vue的<template>html-element注册事件。而是通过获取内部html-element元素主动添加事件的方式实现。

    this.marker._element.addEventListener(key, event => {
        this.$_emitSelfEvent(event);
    });
    

# 完整代码

完整代码如下:

<template>
  <div style="display: none">
    <!-- slot for custom marker -->
    <slot name="marker" />
    <!-- slot for popup -->
    <slot v-if="marker" />
  </div>
</template>

<script>
// 这两行 = this.$emit(markerEvents, data)
import withEvents from "../../lib/withEvents";
import withSelfEvents from "./withSelfEvents";

const markerEvents = {
  drag: "drag",
  dragstart: "dragstart",
  dragend: "dragend"
};

const markerDOMEvents = {
  click: "click",
  mouseenter: "mouseenter",
  mouseleave: "mouseleave"
};

export default {
  name: "MapMarker",
  mixins: [withEvents, withSelfEvents],

  provide() {
    const self = this;
    return {
      get marker() {
        return self.marker;
      }
    };
  },

  mounted() {
    const markerOptions = {
      ...this.$props
    };
    if (this.$slots.marker) {
      markerOptions.element = this.$slots.marker[0].elm;
    }
    this.marker = new this.mapbox.Marker(markerOptions);

    if (this.$listeners["update:coordinates"]) {
      this.marker.on("dragend", event => {
        let newCoordinates;
        if (this.coordinates instanceof Array) {
          newCoordinates = [event.target._lngLat.lng, event.target._lngLat.lat];
        } else {
          newCoordinates = event.target._lngLat;
        }
        this.$emit("update:coordinates", newCoordinates);
      });
    }

    const eventNames = Object.keys(markerEvents);
    this.$_bindSelfEvents(eventNames, this.marker);

    this.initial = false;
    this.$_addMarker();
  },

  beforeDestroy() {
    if (this.map !== undefined && this.marker !== undefined) {
      this.marker.remove();
    }
  },

  methods: {
    $_addMarker() {
      this.marker.setLngLat(this.coordinates).addTo(this.map);
      this.$_bindMarkerDOMEvents();
      this.$_emitEvent("added", { marker: this.marker });
    },

    $_emitSelfEvent(event) {
      this.$_emitMapEvent(event, { marker: this.marker });
    },

    $_bindMarkerDOMEvents() {
      Object.keys(this.$listeners).forEach(key => {
        if (Object.values(markerDOMEvents).includes(key)) {
          this.marker._element.addEventListener(key, event => {
            this.$_emitSelfEvent(event);
          });
        }
      });
    },

    remove() {
      this.marker.remove();
      this.$_emitEvent("removed");
    },

    togglePopup() {
      return this.marker.togglePopup();
    }
  }
};
</script>