# # 创建插件组件

## # 概述

Webclient-Vue-Mapboxgl是针对原始的mapbox-gl脚本的封装。很多额外的高效的插件也是项目开发中常常使用的。因此为了将常用的mapbox-gl的插件也通过Vue的方式使用，下面将介绍如何创建插件组件。

一个插件组件本质上还是通过Vue组件的方式，核心思想是通过获取 根组件`mapbox-map`中的 `mapbox` and `map`对象来进行对应的拓展功能。

Webclient-Vue-Mapboxgl内部使用独立的VUE注入机制provide/inject
(opens new window)。

当`mapbox-map`组件创建的时候，必须等到地图初始化加载完毕后，然后才会渲染子组件 同时通过provide的方式提供下面三个组件:

TIP

1. `mapbox` (Mapbox GL官方脚本),
2. `map` (Map实例
   (opens new window))
3. `actions` (promisified 封装的同步的地图方法).

只要是注入了上面的属性,你就可以添加地图要素或者执行对应的地图方法。 注入方法如下：

    inject: ["mapbox", "map", "actions"],

封装插件的核心思想是： 保持Vue本身的声明规范，这样做可以容易封装额外的控制组件和图层组件.同时也容易封装第三方插件，适应不同的业务需求。

> 这里我在 左侧导航栏 `源码分析`-`注记视图` 里面有详细的描述，请认真阅读。

## # 基本示例

App项目代码

    <template>
      <mapbox-map :accessToken="accessToken" :mapStyle="mapStyle">
        <MyPluginComponent />
      </mapbox-map>
    </template>

    <script>
    import Mapbox from "@mapgis/mapbox-gl";
    import { MapgisWebMap } from "@mapgis/webclient-vue-mapboxgl";

    export default {
      components: {
        MapgisWebMap
      },
      data() {
        return {
          accessToken: ACCESS_TOKEN,
          mapStyle: MAP_STYLE
        };
      }
    };
    </script>

自定义插件组件

    <template>
      <button @click="fly">Fly!</button>
      <div>地图中心是: 经度={{ center.lng }}, 纬度={{ center.lat }}</div>
    </template>

    <script>
    import Mapbox from "@mapgis/mapbox-gl";
    // import { MapgisWebMap } from "@mapgis/webclient-vue-mapboxgl";

    export default {
      name: "MyPluginComponent"
      inject: ["mapbox", "map", "actions"],

      data() {
        return {
          center: null
        };
      },

      created() {
        this.center = this.map.getCenter();
      },

      methods: {
        async fly() {
          const flyResult = await this.actions.flyTo({ center: [10, 10] });
          this.center = flyResult.center;
        }
      }
    };
    </script>

## # 实验性功能 helpers experimental

Experimental

helpers是一个实验性的功能，类似数据库，我们往往在操作数据库的时候很多情况都是通过一个DBHelper、DBLink的方式去连接数据库，因为常见很多SQL语法完全可以封装起来。 这里提出的helpers就是这个目的。 这个试验特性在后续的版本中会不断的改变，我们会尽可能保证长期的向后兼容。这个特性会一直给个`experimental`的提示。 目前他们只是 把`Webclient-Vue-Mapboxgl`框架中的事件机制暴露出去了。

除了基本的地图对象，helpers还提供了一些很有用的注入，这些特性都会包含在$helpers中。代码如下：

    import { $helpers } from "@mapgis/webclient-vue-mapboxgl";

    const { withEvents, withSelfEvents, asControl, asLayer } = $helpers;

### # `withEvents`

Source
(opens new window).
提供了`$_emitEvent` 和 `$_emitMapEvent` 两种方法发送 Vue风格的事件

1. `$_emitEvent` 着重强调发送Vue自身特性的事件可以自定义
2. `$_emitMapEvent` 着重强调发送map自身的事件，一般和地图事件同名

### # `withSelfEvents`

Source
(opens new window)
`$_bindSelfEvents`, `$_unbindSelfEvents` and `$_emitSelfEvent`. 上面三者的核心思想其实就是一个，就是把controls, markers and popups绑定的Dom-Html元素添加对应的事件监听并把该事件传递给Vue组件。

> 通俗的说就是 将地图内部的html的事件对外暴露给Vue组件。 也可以理解为将地图对象的内部事件从map映射到Vue组件上。

### # `asControl`

Source
(opens new window).
提供地图控制视图的主代码结构, 核心是封装了 map.addControl / removeControl(this.control);

### # `asLayer`

Source
(opens new window).
提供地图图层的主代码结构 layers API

## # 从原生插件创建组件

举例说明如何从一个原生mapbox插件创建Vue组件。

VueMaboxGeocoder
(opens new window) — 包装 mapbox-gl-geocoder
(opens new window):

    // 因为该组件没有HTML需要渲染，因此不需要<template>，只用js就可以了。

    import MapboxGeocoder from "@mapbox/mapbox-gl-geocoder";
    import { $helpers } from "@mapgis/webclient-vue-mapboxgl"; //获取 $helpers

    // 定义mapbox-gl-geocoder事件
    const geocoderEvents = {
      clear: "clear",
      loading: "loading",
      results: "results",
      result: "result",
      error: "error"
    };

    export default {
      name: "GeocoderControl",
      mixins: [$helpers.asControl], // 因为是控制组件，所以需要注入asControl

      inject: ["mapbox", "map"], // 获取根组件传递来的对象

      props: {
        //该插件需要秘钥
        accessToken: {
          type: String,
          required: true
        },
        input: {
          type: String,
          default: null
        },
        proximity: {
          type: Object,
          default: null
        }
      },

      data() {
        return {
          initial: true
        };
      },

      // 观察必要的属性
      watch: {
        input: {
          handler(next, prev) {
            if (this.control && next !== prev) {
              this.control.setInput(next);
            }
          },
          immediate: true
        },
        proximity(next, prev) {
          if (this.control && next !== prev) {
            this.control.setProximity(next);
          }
        }
      },

      created() {
        // 特别注意这里的this.control不是使用的 data里面的而是 组件自身的this
        // 核心原因在于 如果使用data或者vuex会使得和Vue自身的响应式机制混乱
        // 因此使用一种非响应式的属性来记录，类似全局变量的形式
        this.control = null;
        if (this.accessToken && !this.mapbox.accessToken) {
          this.mapbox.accessToken = this.accessToken;
        }
        this.control = new MapboxGeocoder(this.$props); // 创建对象
        this.control.on("results", this.$_updateInput); // 当用户输入的时候，更新同步属性 "input"

        // 把组件添加到地图上
        this.$_deferredMount();
      },

      beforeDestroy() {
        this.control.off("results", this.$_updateInput);
        // 由于是注入了asControl，因此控制组件会在asControl的hook周期中执行调用 `地图移除该控制组件`
      },

      methods: {
        $_deferredMount() {
          // 由于该控制组件已经是挂载到<mapbox-map>的子树下面，this.map已经初始化完毕并注入该组件中了
          this.map.addControl(this.control);
          if (this.input) {
            // 设置默认输入数据
            this.control.setInput(this.input);
          }
          // 发送添加事件. `$_emitEvent` 方法来自 `asControl`
          this.$_emitEvent("added", { geocoder: this.control });
          this.$_bindSelfEvents(Object.keys(geocoderEvents)); // 把geocode自身的事件映射成vue的事件
          this.initial = false; // 初始化完毕
        },

        $_bindSelfEvents(events) {
          // asControl 本身是拥有 $_bindSelfEvents 方法的，但是这里的geocoder组件并不是遵循的mapbox-gl.js的事件机制，
          // 因此我们需要覆盖该方法, 按照对应的业务方式实现
          const vm = this;
          // 使用vue的this.$listeners方式来订阅用户指定的事件
          Object.keys(this.$listeners).forEach(eventName => {
            if (events.includes(eventName)) {
              this.control.on(eventName, vm.$_emitControlEvent.bind(vm, eventName));
            }
          });
        },

        // 按照@mapgis/webclient-vue-mapboxgl的规范 发送事件 ，其实就是用{type：eventName}包装事件名
        $_emitControlEvent(eventName, eventData) {
          return this.$_emitSelfEvent({ type: eventName }, eventData);
        },

        $_updateInput(results) {
          if (!this.initial) {
            const input = results.query ? results.query.join("") : "";
            this.$emit("update:input", input); // 更新同步属性 "input"
          }
        }
      }
    };

TIP

可能看完上面的源码分析，还是有点晕晕的，这里的核心思想本质上和封装官方原始对象是一摸一样的。 唯一的不同之处在于，官方的原始对象监听的事件都是遵循的mapboxgl.js的事件机制，所以封装官方组件的_bindSelfEvents通常是不需要重新实现的，就走内部封装的￥_emitMapEvent方法。 而第三方的插件对象监听的事件 `不一定` 遵循mapboxgl.js的事件机制，因此需要重新实现bindSelfEvents和封装emitSelfEvent

一言以蔽之：

官方组件事件:

> bindSelfEvents => emitSelfEvent不变 => emitMapEvent不变 流程

三方插件事件:

> bindSelfEvents新 => emitControlEvent新 => emitSelfEvent不变 => emitMapEvent不变

DANGER

此刻我源代码分析到这里不由得真心感叹一句，这个原始结构设计的真心是 真的即精通mapboxgl又同时精通vue事件,很多GitHub的开源代码往往只是vue/mapboxgl中的某一种进行对应的封装，而这份代码确实是两者都精通，设计上确实高山流水,代码信雅达，因此后面的封装组件强烈建议遵循这一套模式!