vue 将百度地图或者高德地图组件化

发布时间 2023-12-29 10:09:36作者: Sebastian·S·Pan

一、前言

百度地图已经有了 react 相关的组件库,本人用的百度地图 v3.0 和 vue3
我仅仅是抛砖引玉,百度地图 webgl、高德地图都是一样的,因为底层都是通过 js 控制地图
如果用组件的方式开发,比如我将 BMap.Marker 作为一个组件,我暴露一个参数position,其目的是控制 BMap.Marker 位置
那么只要我修改positionBMap.Marker 在地图中的就会自动改变
和用js相比,无疑会减少很多步骤,方便省心(同理,其实用vue开发,比用原生html、js 更快!),以下是我自己在使用的代码截图

二、实现

这里我主要讲讲 BMap、BMap.Marker、自定义 Overlay
其实其他的覆盖物,相信只要你看了 BMap.Marker,就知道如何生成了,原理一样的
因为我只写了我自己需要的,有很多没用到的操作,我懒就没写了
核心其实就一个,如何给子组件注入已经生成的地图对象,就没了!

2.1 BMap实现

注意控制地图的生命周期,以及如何暴露 map 元素给子组件

<template>
    <div class="c-BMap" ref="targetDom"></div>
    <!-- 等地图挂载后,再加载子组件 -->
    <slot v-if="isMapMount"></slot>
</template>

<script lang="ts" setup>
import { onMounted, onUnmounted, provide, ref, shallowRef, watch } from 'vue';

const props = withDefaults(defineProps<{
    zoom?: number,
    center?: BMap.Point | BMap.Point[],
}>(), {
    zoom: 15,
    center: () => new BMap.Point(114.13581398675089, 22.61457979837459)
});
const isMapMount = ref(false);
const emits = defineEmits(['onMapMount']);

let map = shallowRef<BMap.Map | null>(null);
const targetDom = ref<HTMLDivElement>();

// 暴露地图给子组件
provide('map', map);

watch(() => props.zoom, (zoom) => {
    if (map.value) {
        map.value.setZoom(zoom);
    }
})

watch(() => props.center, (position) => {
    if (map.value) {
        if (Array.isArray(position)) {
            map.value.setViewport(position);
        } else {
            map.value.setViewport({
                center: position,
                zoom: props.zoom
            })
        }
    }
})
// 初始化地图
onMounted(() => {
    if (targetDom.value) {
        const _map = new BMap.Map(targetDom.value);
        if (Array.isArray(props.center)) {
            _map.setViewport(props.center);
        } else {
            _map.setViewport({
                center: props.center,
                zoom: props.zoom
            })
        }
        _map.enableScrollWheelZoom();
        map.value = _map;
        isMapMount.value = true;
        emits('onMapMount', _map);
    }
})
// 地图销毁
onUnmounted(()=>{
    if(map.value){
        map.value.destroy();
    }
})
</script>

<style lang="less">
.c-BMap {
    height: 100%;
    width: 100%;
}

// 百度地图logo
.anchorBL {
    display: none;
}
</style>

2.2 BMap.Marker 实现

<template></template>

<script lang="ts" setup>
import { watch } from 'vue';
import useBMapOverlay from "./composition/useBMapOverlay";

const props = withDefaults(defineProps<{
    position: BMap.Point | null,
    icon?: BMap.Icon,
    rotation?: number
    onClick?: (e: any) => void,
}>(), {

})

const target = new BMap.Marker(props.position || new BMap.Point(0, 0));
if (props.icon) {
    target.setIcon(props.icon);
}
target.disableMassClear();
if (props.onClick) {
    target.addEventListener('click', props.onClick)
}

useBMapOverlay(target);

watch(() => props.position, (position) => {
    if (target) {
        target.setPosition(position || new BMap.Point(0, 0));
    }
})
watch(() => props.rotation, (rotation) => {
    target.setRotation(rotation || 0);
})
</script>

上面有一个函数useBMapOverlay(),因为后续很多BMap的覆盖物,挂载在 BMap上的步骤相同,我给提取出来了,内容如下:

import { inject, onBeforeUnmount, onMounted, type ShallowRef } from "vue";

const useBMapOverlay = (overlay: BMap.Overlay) => {
    // 获取父组件提供的地图,注意必须先生成地图
    const map = inject('map') as ShallowRef<BMap.Map | null>;
    // 将覆盖物挂载在地图上
    onMounted(() => {
        if (map.value) {
            map.value.addOverlay(overlay);
        }
    });
    // 销毁覆盖物
    onBeforeUnmount(() => {
        if (map.value) {
            map.value.removeOverlay(overlay);
        }
    })
}

export default useBMapOverlay;

2.3 自定义 Ovarlay

看过百度地图Api的都知道,覆盖物都继承自 BMap.Overlay
只要实现了BMap.Overlay,就可以创建自己的覆盖物
我将 vue 生成的 dom 传递给自定义 Ovarlay,那么这个覆盖物就有了响应式的特点
当然你弄个什么图表 Echart 什么的,一样都可以

<template>
  <div ref="targetDom">
    <slot></slot>
  </div>
</template>
  
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, ref, watch, type ShallowRef, inject } from 'vue';
import MyOverlay from "./lib/MyOverlay";

const props = withDefaults(defineProps<{
  position: BMap.Point | null,
  show?: boolean,
  offset?: BMap.Size
}>(), {
  position: () => new BMap.Point(0, 0),
  show: true,
})

let overlay: MyOverlay | null = null;
const targetDom = ref<HTMLDivElement>();
const map = inject('map') as ShallowRef<BMap.Map | null>;

const init = () => {
  console.log("创建一次")
  if (targetDom.value && map.value && !overlay) {
    overlay = new MyOverlay(targetDom.value, props.position || new BMap.Point(0, 0));
    map.value.addOverlay(overlay);
    if (props.offset) {
      overlay.setOffset(props.offset);
    }
  }
}

watch(() => props.position, (position) => {
  if (overlay) {
    overlay.setPosition(position || new BMap.Point(0, 0));
  }
})

watch(() => props.show, (show) => {
  if (overlay) {
    if (show) {
      overlay.show();
    } else {
      overlay.hide();
    }
  }
})

onMounted(init);

onBeforeUnmount(() => {
  if (overlay) {
    overlay.destroy();
  }
})
</script>
export default class MyOverlay extends BMap.Overlay {
    private map: any;
    private dom: HTMLDivElement;
    private position: BMap.Point;
    enableMassClear = false;

    constructor(dom: HTMLDivElement, position: BMap.Point) {
        super();
        dom.style.position = 'absolute';
        this.dom = dom;
        this.position = position;
    }

    // 使用 BMap.addOverlay 的时候会自动触发本函数
    initialize(map: BMap.Map) {
        const pane = map.getPanes().markerPane;
        if (pane) {
            pane.appendChild(this.dom);
        }
        this.map = map;
        return this.dom;
    }

    private offset: BMap.Size = new BMap.Size(0, 0)
    setOffset(offset: BMap.Size) {
        this.offset = offset;
        this.draw();
    }

    hide(): void {
        this.dom.style.display = 'none';
    }

    show(): void {
        this.dom.style.display = 'block';
    }

    // 百度地图需要重新绘制的时候触发,比如zoom、地图移动就会触发
    draw() {
        const pixel = this.map.pointToOverlayPixel(this.position);
        const { height, width } = this.offset;
        this.dom.style.left = pixel.x + width + "px";
        this.dom.style.top = pixel.y + height + "px";
    }

    setPosition(position: BMap.Point) {
        this.position = position;
        this.draw();
    }

    destroy() {
        if (this.map) {
            this.map.removeOverlay(this);
        }
    }
}

三、优缺点总结

优点就是大大减少代码
缺点,该系列的组件,本质上套了一层vue,比直接操作js代码,性能上是有问题的,但是问题不是很大,可以牺牲

注意动画,我有这样一个场景,60帧的帧率,Maker 的移动很丝滑,但是自定义 Overlay的移动有点不丝滑
猜测是自定义 Overlay,移动位置是百度地图操作,修改内容是vue操作,这两个动作不同步,所以....