我把 OpenLayers 做成了“可配置业务部件平台”:多地图实例隔离 + 地图能力作用域化实践

1 阅读4分钟

ezgif-73681bdd6ebded87.gif

前言

在很多业务项目里,地图页面常常会演变成“巨型组件”:地图初始化、图层管理、打点画线、浮层弹窗、业务联动全写在一起。
这次我在项目里把地图模块拆成了一个可扩展架构:OpenLayers 负责地图引擎,Widget 负责业务交互,Store 负责状态,Config 负责驱动

目录在:src/OpenLayers(项目里还有 @/openLayers 的别名引用)。


一、这个模块解决了什么问题?

1)多地图实例同时存在时不串数据

通过 mapKey 做作用域隔离:

  • 地图实例按 key 注册/获取
  • Widget 归属某个 mapKey
  • 激活状态、payload、图层操作都按 mapKey 分域

关键实现可参考 instance.tsmain-base/index.tsx

2)业务面板完全配置化

所有部件在注册表声明,不在页面硬编码 if/else。

参考 widget-registry.ts

3)业务代码只调用 scoped API

Widget 里只写:

  • drawPoints / drawLine / onPointClick
  • activate / disable

底层 mapKey 注入由框架完成,参考 useMapScope.ts


二、架构拆分(四层思路)

配置层(Config)

数据/状态层(Store)

  • Pinia 管理 widgetsactiveWidgetIdsMapactiveWidgetPayloadMap
  • 支持按 scope 激活、关闭、清空

参考 store/widget.ts

逻辑层(Map + Hooks)

  • 地图实例创建、注册、销毁
  • 图层管理(ensure/get/clear/visible)
  • 打点/画线/点击事件/hover tooltip
  • scoped API 对业务层屏蔽 mapKey 细节

参考 createMap.tslayerManager.tsdrawPoint.tspointTooltip.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 都可配置化治理。
  • 可复用:同一套能力可复制到第二张地图实例,天然支持“对照地图/双屏地图”。

项目地址

gitee.com/zhaoyan0814…

使用方法

拉取后用文件夹包起来放到你项目的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
  };
}