前言
在很多业务项目里,地图页面常常会演变成“巨型组件”:地图初始化、图层管理、打点画线、浮层弹窗、业务联动全写在一起。
这次我在项目里把地图模块拆成了一个可扩展架构:OpenLayers 负责地图引擎,Widget 负责业务交互,Store 负责状态,Config 负责驱动。
目录在:src/OpenLayers(项目里还有 @/openLayers 的别名引用)。
一、这个模块解决了什么问题?
1)多地图实例同时存在时不串数据
通过 mapKey 做作用域隔离:
- 地图实例按 key 注册/获取
- Widget 归属某个 mapKey
- 激活状态、payload、图层操作都按 mapKey 分域
关键实现可参考 instance.ts、main-base/index.tsx。
2)业务面板完全配置化
所有部件在注册表声明,不在页面硬编码 if/else。
3)业务代码只调用 scoped API
Widget 里只写:
drawPoints / drawLine / onPointClickactivate / disable
底层 mapKey 注入由框架完成,参考 useMapScope.ts。
二、架构拆分(四层思路)
配置层(Config)
- 地图业务常量:中心点、默认缩放、实例 key
参考 map-business-config.ts - 图层 key:点图层/线图层
参考 map-layer-config.ts - 样式预设:点样式、线样式
参考 point-style-config.ts、line-style-config.ts - Widget 注册与主题系统
参考 widget-registry.ts、widget-theme-config.ts
数据/状态层(Store)
- Pinia 管理
widgets、activeWidgetIdsMap、activeWidgetPayloadMap - 支持按 scope 激活、关闭、清空
参考 store/widget.ts。
逻辑层(Map + Hooks)
- 地图实例创建、注册、销毁
- 图层管理(ensure/get/clear/visible)
- 打点/画线/点击事件/hover tooltip
- scoped API 对业务层屏蔽 mapKey 细节
参考 createMap.ts、layerManager.ts、drawPoint.ts、pointTooltip.ts。
UI 层(TSX 组件)
- 地图容器:
main-base / main-view / main-view-copy - 通用面板:
MapWidgetPanel - 业务部件:
widgets/demo1|demo2|common
三、关键代码(节选)
1)Widget 注册表:把业务 UI 变成配置驱动
export const widgetRegistry: WidgetItem[] = [
{
name: "leftPanelWidget",
title: "左侧面板",
component: markRaw(
defineAsyncComponent({
loader: () => import("@/openLayers/widgets/demo1/leftPanelWidget"),
suspensible: false,
delay: 0
})
),
anchor: { left: 16, top: 16 },
mapKey: MAIN_MAP_INSTANCE_KEY,
animation: "converge",
animationDuration: 200,
width: 320,
height: 300
}
];
源码位置:widget-registry.ts
2)地图实例生命周期:创建 + 注册 + 销毁
onMounted(() => {
if (mapRef.value) {
mapInstance = createOpenLayersMap({ target: mapRef.value });
setMapInstance(mapInstance, props.mapKey);
}
if (!store.widgets.length) {
store.loadWidgets();
}
});
onBeforeUnmount(() => {
clearMapInstance(props.mapKey);
if (mapInstance) {
mapInstance.setTarget(undefined);
mapInstance.dispose();
mapInstance = null;
}
});
源码位置:main-base/index.tsx
3)作用域化地图 API:业务层不关心 mapKey
export function useScopedMapActions(explicitMapKey?: string) {
const mapKey = useMapScopeKey(explicitMapKey);
const drawPoints = (points: LonLatPoint[], options?: ScopedDrawPointsOptions) =>
baseDrawPoints(points, { ...options, mapKey });
const drawLine = (segments: LineSegment[], options?: ScopedDrawLineOptions) =>
baseDrawLine(segments, { ...options, mapKey });
const onPointClick = (handler: (payload: PointClickPayload) => void, layerKeys?: MapLayerKey[]) =>
baseOnPointClick(handler, undefined, mapKey, layerKeys);
return { drawPoints, drawLine, onPointClick };
}
源码位置:useMapScope.ts
4)打点 + 自动视野拟合 + 点位点击
export function drawPoints(points: LonLatPoint[], options?: DrawPointOptions, map?: Map | null) {
if (!points.length) return [];
const targetMap = getTargetMap(map, options?.mapKey);
const layerKey = options?.layerKey ?? MAP_LAYER_KEYS.POINT_MAIN;
clearPoints(targetMap, options?.mapKey, layerKey);
const features = points.map(point => drawPoint(point, targetMap, options?.defaultStyleKey, options?.mapKey, layerKey));
if (options?.fitView ?? true) {
const extent = boundingExtent(points.map(point => fromLonLat([point.lon, point.lat])));
targetMap.getView().fit(extent, {
padding: [60, 60, 60, 60],
maxZoom: 11,
duration: 350
});
}
return features;
}
源码位置:drawPoint.ts
四、落地收益
- 可扩展:加新业务面板只需新增 widget + 注册,不改主地图容器。
- 低耦合:地图能力封装在 map 层,UI 不直接操作底层对象。
- 可维护:点线样式、主题、图层 key 都可配置化治理。
- 可复用:同一套能力可复制到第二张地图实例,天然支持“对照地图/双屏地图”。
项目地址
使用方法
拉取后用文件夹包起来放到你项目的src下面或者随便哪里,在页面中调用map-work下面的组件就好
<template>
<div class="map-widget-test">
<div class="map-widget-test__map">
<OlMainView />
</div>
</div>
</template>
<script setup lang="ts">
import { onActivated, onMounted } from "vue";
import OlMainView from "@/你自己设置的文件夹名称/components/map-work/main-view";
import { useMapWidgetTest } from "./useMapWidgetTest";
const { activate } = useMapWidgetTest();
onMounted(() => {
activate("leftPanelWidget");
});
onActivated(() => {
activate("leftPanelWidget");
});
</script>
import { activate, disable, disableAll } from '@/你自己设置的文件夹名称/store/widget';
export function useMapWidgetTest() {
const closeAllWidgets = () => disableAll();
return {
activate,
disable,
disableAll: closeAllWidgets
};
}