Vue3项目实战:用BMapGL+BMapGLLib实现地图标注与绘制(附完整代码)
在Vue3生态中集成地图功能已成为企业级应用的常见需求。百度地图GL版(BMapGL)凭借其WebGL渲染引擎,为现代前端项目提供了更流畅的地图交互体验。本文将深入探讨如何在Vue3的Composition API环境下,高效整合BMapGL核心库与BMapGLLib扩展工具,实现从基础地图展示到复杂图形绘制的全流程解决方案。
1. 环境配置与SDK接入
1.1 密钥申请与类型声明
首先访问百度地图开放平台申请WebGL版密钥(AK),建议为开发和生产环境配置不同的密钥。在Vue3项目中创建src/types/bmapgl.d.ts文件声明类型:
declare interface Window { BMapGL: any; BMapGLLib: any; } type MapPoint = { lng: number; lat: number; }; type DrawingMode = 'marker' | 'polyline' | 'polygon' | 'rectangle' | 'circle';1.2 动态脚本加载方案
不同于传统script标签引入方式,我们采用动态加载方案以优化首屏性能:
// utils/loadScript.ts export const loadBMapGL = (ak: string) => { return new Promise((resolve) => { if (window.BMapGL) return resolve(true); const script = document.createElement('script'); script.src = `//api.map.baidu.com/api?type=webgl&v=1.0&ak=${ak}`; script.onload = () => { loadDrawingManager().then(resolve); }; document.head.appendChild(script); }); }; const loadDrawingManager = () => { return new Promise((resolve) => { if (window.BMapGLLib) return resolve(true); const link = document.createElement('link'); link.href = '//mapopen.cdn.bcebos.com/github/BMapGLLib/DrawingManager/src/DrawingManager.min.css'; link.rel = 'stylesheet'; const script = document.createElement('script'); script.src = '//mapopen.cdn.bcebos.com/github/BMapGLLib/DrawingManager/src/DrawingManager.min.js'; document.head.append(link, script); script.onload = resolve; }); };提示:动态加载需考虑网络异常情况,建议添加错误处理逻辑和加载超时机制
2. Vue3地图组件封装
2.1 基础地图组件实现
创建BMapGLContainer.vue组件,采用Composition API组织逻辑:
<template> <div ref="mapContainer" class="bmap-gl-container" :style="{ height: `${height}px` }" ></div> </template> <script setup lang="ts"> import { onMounted, ref, watch } from 'vue'; const props = defineProps({ height: { type: Number, default: 500 }, center: { type: Object as () => MapPoint, required: true }, zoom: { type: Number, default: 12 } }); const mapContainer = ref<HTMLElement>(); const mapInstance = ref<any>(); const initMap = async () => { await loadBMapGL('your_ak'); mapInstance.value = new window.BMapGL.Map(mapContainer.value, { enableMapClick: false, displayOptions: { building: true // 开启3D建筑物显示 } }); const point = new window.BMapGL.Point(props.center.lng, props.center.lat); mapInstance.value.centerAndZoom(point, props.zoom); mapInstance.value.enableScrollWheelZoom(true); }; onMounted(initMap); </script> <style scoped> .bmap-gl-container { position: relative; border: 1px solid #eee; border-radius: 4px; overflow: hidden; } </style>2.2 响应式地图控制
利用Vue3的响应式特性实现地图状态同步:
// 在setup()中添加 watch(() => props.center, (newVal) => { if (!mapInstance.value) return; const point = new window.BMapGL.Point(newVal.lng, newVal.lat); mapInstance.value.panTo(point); }, { deep: true }); watch(() => props.zoom, (newVal) => { mapInstance.value?.setZoom(newVal); });3. 高级绘制功能实现
3.1 可组合式绘图逻辑
创建useMapDrawing组合式函数,封装绘图相关逻辑:
// composables/useMapDrawing.ts import { ref } from 'vue'; export default function useMapDrawing(mapInstance: any) { const activeMode = ref<DrawingMode>(); const drawingManager = ref<any>(); const overlays = ref<any[]>([]); const initDrawingManager = (options = {}) => { drawingManager.value = new window.BMapGLLib.DrawingManager( mapInstance.value, { enableCalculate: true, enableSorption: true, sorptiondistance: 15, ...options } ); }; const setDrawingMode = (mode: DrawingMode) => { activeMode.value = mode; drawingManager.value?.setDrawingMode(mode); drawingManager.value?.open(); }; const clearOverlays = () => { overlays.value.forEach(overlay => { mapInstance.value.removeOverlay(overlay); }); overlays.value = []; }; return { activeMode, drawingManager, overlays, initDrawingManager, setDrawingMode, clearOverlays }; }3.2 绘图事件与Vue状态联动
扩展useMapDrawing函数,添加事件监听逻辑:
// 在useMapDrawing.ts中继续添加 const setupEventListeners = () => { drawingManager.value?.addEventListener('overlaycomplete', (event: any) => { const overlay = event.overlay; overlays.value.push(overlay); switch (event.drawingMode) { case 'marker': handleMarkerComplete(overlay); break; case 'circle': handleCircleComplete(overlay); break; // 其他图形处理... } }); }; const handleMarkerComplete = (marker: any) => { const position = marker.getPosition(); console.log('Marker placed at:', position.lng, position.lat); // 反向地理编码示例 const geocoder = new window.BMapGL.Geocoder(); geocoder.getLocation(position, (result: any) => { console.log('Address:', result.address); }); }; const handleCircleComplete = (circle: any) => { const center = circle.getCenter(); const radius = circle.getRadius(); console.log(`Circle - Center: (${center.lng},${center.lat}), Radius: ${radius}m`); };4. 性能优化与最佳实践
4.1 内存管理策略
百度地图实例会持续占用内存,需在组件卸载时正确销毁:
// 在BMapGLContainer.vue的setup()中添加 onUnmounted(() => { if (mapInstance.value) { mapInstance.value.destroy(); mapInstance.value = null; } });4.2 绘图数据序列化
实现图形数据的保存与恢复功能:
// 扩展useMapDrawing.ts const serializeOverlays = () => { return overlays.value.map(overlay => { if (overlay instanceof window.BMapGL.Marker) { const position = overlay.getPosition(); return { type: 'marker', lng: position.lng, lat: position.lat }; } // 其他图形类型处理... }); }; const deserializeOverlays = (data: any[]) => { clearOverlays(); data.forEach(item => { let overlay: any; const point = new window.BMapGL.Point(item.lng, item.lat); switch (item.type) { case 'marker': overlay = new window.BMapGL.Marker(point); break; // 其他图形类型处理... } if (overlay) { mapInstance.value.addOverlay(overlay); overlays.value.push(overlay); } }); };4.3 自定义绘图样式
通过样式配置提升绘图视觉效果:
const drawingStyles = { strokeColor: '#1890ff', fillColor: '#1890ff40', strokeWeight: 2, strokeOpacity: 0.8, fillOpacity: 0.3, strokeStyle: 'solid' // 'dashed' | 'solid' }; // 使用时 initDrawingManager({ circleOptions: drawingStyles, polygonOptions: drawingStyles, rectangleOptions: drawingStyles });5. 典型业务场景实现
5.1 区域标注系统
结合Element Plus实现完整的区域标注流程:
<template> <div class="map-editor"> <el-button-group> <el-button v-for="mode in modes" :key="mode" :type="activeMode === mode ? 'primary' : ''" @click="setMode(mode)" > {{ modeLabels[mode] }} </el-button> </el-button-group> <b-map-gl-container ref="map" :center="initialCenter" :zoom="14" @ready="onMapReady" /> <el-button @click="saveAreas">保存标注</el-button> </div> </template> <script setup> import { ref } from 'vue'; import useMapDrawing from '@/composables/useMapDrawing'; const initialCenter = { lng: 116.404, lat: 39.915 }; const map = ref(); const { activeMode, setDrawingMode, serializeOverlays } = useMapDrawing(); const modes = ['marker', 'circle', 'rectangle']; const modeLabels = { marker: '点标注', circle: '圆形区域', rectangle: '矩形区域' }; const onMapReady = (mapInstance) => { drawing.initDrawingManager(mapInstance); }; const setMode = (mode) => { setDrawingMode(mode); }; const saveAreas = () => { const areaData = serializeOverlays(); console.log('保存区域数据:', areaData); // 提交到后端... }; </script>5.2 坐标转换处理
处理不同坐标系之间的转换问题:
// utils/coordTransform.ts export const wgs84ToBd09 = (lng: number, lat: number) => { // 实际项目中应使用百度官方坐标转换API // 这里简化处理,生产环境请勿直接使用 const x = lng * 1.0002; const y = lat * 1.0001; return { lng: x, lat: y }; }; // 在组件中使用 const point = wgs84ToBd09(116.404, 39.915); mapInstance.value.panTo(new window.BMapGL.Point(point.lng, point.lat));6. 调试技巧与常见问题
6.1 开发环境问题排查
常见问题及解决方案:
- 地图不显示:检查AK是否正确、网络请求是否成功、容器尺寸是否有效
- 绘图工具不生效:确认BMapGLLib加载顺序,应在BMapGL之后加载
- 事件不触发:检查地图实例是否已初始化完成
6.2 性能监控方案
添加地图性能日志:
mapInstance.value.addEventListener('tilesloaded', () => { console.timeEnd('mapTilesLoad'); }); console.time('mapTilesLoad');在项目实际开发中,地图组件的复用边界需要仔细设计。将基础地图功能与业务逻辑分离,通过组合式函数封装通用能力,可以使地图模块保持灵活性和可维护性。对于复杂交互场景,建议采用状态管理工具(如Pinia)来管理地图状态,避免组件间直接耦合。