当前位置: 首页 > news >正文

从分散到统一:基于Vue3的地图模块重构之路

前言

在过去的几个月里,我对负责的某调查Web应用进行了一次大规模的重构,特别是针对地图模块进行了深度优化。这次重构不仅解决了性能问题,更重要的是建立了一套可复用、可维护的地图组件体系。今天就来分享一下这次重构的心路历程和技术细节。

🎯 重构背景:痛点的积累

原有架构的问题

在重构之前,我们的系统存在多个独立的地图页面:

  • 时空分析页面 - 时间轴播放、卷帘对比
  • 数据分析页面 - 图表展示、样点搜索
  • 一张图页面 - 数据概览、统计展示
  • 数据管理页面 - 基础数据管理

每个页面都有自己独立的地图实现,这导致了以下问题:

1. 性能问题
// 每次切换页面都要重新初始化地图
const initMap = () => {map = new Map({target: 'map-container',layers: [/* 重新创建所有图层 */],view: new View({/* 重新设置视图 */})})// 重新加载所有数据...
}
2. 用户体验差
  • 地图频繁闪烁
  • 状态丢失(缩放级别、中心点、选中图层)
  • 加载等待时间长
  • 操作不连贯
3. 代码重复
  • 地图初始化逻辑重复
  • 图层管理代码重复
  • 事件处理逻辑重复
  • 样式定义重复
4. 维护困难
  • 修改一个功能需要改多个文件
  • 新功能开发成本高
  • 测试覆盖困难

🚀 重构方案:统一地图架构

核心设计思想

“地图保持不动,只切换侧边栏内容”

这个看似简单的想法,却带来了巨大的性能提升和用户体验改善。

新架构设计

重构前后对比

重构前 - 分散式地图页面:

重构前架构
地图实例A
时空分析页面
图层管理A
事件处理A
样式定义A
地图实例B
数据分析页面
图层管理B
事件处理B
样式定义B
地图实例C
一张图页面
图层管理C
事件处理C
样式定义C
地图实例D
数据管理页面
图层管理D
事件处理D
样式定义D

重构后 - 统一地图模块:

重构后架构
AdvancedMap组件
统一地图页面
MapManager管理器
MapConfigManager配置管理
底图管理
WFS图层管理
事件系统
map-configs.json
场景配置
坐标系配置
底图配置
图层配置
动态面板系统
时空分析面板
数据分析面板
一张图面板
数据管理面板
use-sample-points.ts
组件层次结构
UnifiedMap 统一地图页面
AdvancedMap 高级地图组件
MarsWidget Widget容器
ModeSwitcher 模式切换器
MapManager 地图管理器
MapConfigManager 配置管理器
WFSLayerControl 图层控制
FeaturePopup 要素弹窗
BaseMapManager 底图管理
WFSLayerManager WFS图层管理
EventManager 事件管理
TimeSpacePanel 时空分析面板
DataAnalysePanel 数据分析面板
OneMapPanel 一张图面板
DataManagePanel 数据管理面板
use-sample-points.ts
图层创建
筛选功能
高亮功能
定位功能

🔧 技术实现细节

1. 配置驱动的地图系统

首先,我们建立了配置驱动的架构:

// map-configs.json
{"default": {"container": "map-container","center": [117.2728, 30.1314],"zoom": 7,"projection": "EPSG:3857","coordinateSystems": {"EPSG:4326": {"name": "WGS84地理坐标系","isStandard": true},"EPSG:3857": {"name": "Web Mercator投影坐标系", "isStandard": true}},"baseMaps": [{"type": "tianditu_img","name": "tianditu_img","visible": true,"opacity": 1}]}
}

2. 高级地图组件 (AdvancedMap)

基于Vue 3 Composition API,我们创建了一个功能完整的地图组件:

// AdvancedMap.vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
import { MapManager, MapEventType, type MapConfig } from '../core/MapManager'
import { MapConfigManager } from '../core/MapConfigManager'interface Props {mapId: stringscene?: stringcenter?: [number, number]zoom?: numbershowBaseMapSwitcher?: booleanshowMapInfo?: boolean
}const props = withDefaults(defineProps<Props>(), {scene: 'default',showBaseMapSwitcher: true,showMapInfo: true
})// 地图管理器
const mapManager = ref<MapManager | null>(null)
const mapConfigManager = new MapConfigManager()// 响应式状态
const loading = ref(true)
const mapCenter = ref<[number, number]>([0, 0])
const currentZoom = ref(0)
const currentBaseMap = ref('')// 初始化地图
const initMap = async () => {try {loading.value = true// 加载配置const config = await mapConfigManager.getConfig(props.scene)// 创建地图管理器mapManager.value = new MapManager(props.mapId, config)// 监听地图事件mapManager.value.on(MapEventType.VIEW_CHANGE, handleViewChange)mapManager.value.on(MapEventType.CLICK, handleMapClick)// 初始化地图await mapManager.value.init()// 更新状态updateMapState()loading.value = falseemit('onload', mapManager.value.getMap())} catch (error) {console.error('地图初始化失败:', error)loading.value = false}
}// 底图切换
const switchBaseMap = async (baseMapName: string) => {if (mapManager.value) {await mapManager.value.switchBaseMap(baseMapName)currentBaseMap.value = baseMapName}
}// 视图变化处理
const handleViewChange = () => {updateMapState()emit('onviewchange', {center: mapCenter.value,zoom: currentZoom.value})
}// 地图点击处理
const handleMapClick = (event: any) => {emit('onclick', event)
}// 更新地图状态
const updateMapState = () => {if (mapManager.value) {const view = mapManager.value.getMap().getView()const center = view.getCenter()const zoom = view.getZoom()if (center) {mapCenter.value = [center[0], center[1]]}currentZoom.value = zoom || 0}
}onMounted(() => {initMap()
})onUnmounted(() => {mapManager.value?.dispose()
})
</script>

3. 地图管理器 (MapManager)

为了统一管理地图的各个功能,我们创建了MapManager类:

// MapManager.ts
export class MapManager {private map: Map | null = nullprivate config: MapConfigprivate eventBus = new EventTarget()private layers: Map<string, Layer> = new Map()private baseMapLayers: Map<string, Layer> = new Map()constructor(containerId: string, config: MapConfig) {this.containerId = containerIdthis.config = config}async init(): Promise<void> {// 创建地图实例this.map = new Map({target: this.containerId,layers: [],view: new View({center: this.config.center,zoom: this.config.zoom,projection: this.config.projection})})// 初始化底图await this.initBaseMaps()// 初始化WFS图层await this.initWFSLayers()// 设置事件监听this.setupEventListeners()}// 底图管理async initBaseMaps(): Promise<void> {for (const baseMapConfig of this.config.baseMaps) {const layer = await this.createBaseMapLayer(baseMapConfig)this.baseMapLayers.set(baseMapConfig.name, layer)this.map?.addLayer(layer)}}// WFS图层管理async initWFSLayers(): Promise<void> {for (const wfsConfig of this.config.wfsLayers) {const layer = await this.createWFSLayer(wfsConfig)this.layers.set(wfsConfig.name, layer)this.map?.addLayer(layer)}}// 图层控制toggleLayer(layerName: string, visible?: boolean): void {const layer = this.layers.get(layerName)if (layer) {layer.setVisible(visible !== undefined ? visible : !layer.getVisible())}}// 底图切换async switchBaseMap(baseMapName: string): Promise<void> {// 隐藏所有底图this.baseMapLayers.forEach(layer => layer.setVisible(false))// 显示选中的底图const targetLayer = this.baseMapLayers.get(baseMapName)if (targetLayer) {targetLayer.setVisible(true)}}// 事件系统on(eventType: MapEventType, callback: Function): void {this.eventBus.addEventListener(eventType, callback as EventListener)}off(eventType: MapEventType, callback: Function): void {this.eventBus.removeEventListener(eventType, callback as EventListener)}// 资源清理dispose(): void {this.map?.dispose()this.layers.clear()this.baseMapLayers.clear()}
}

4. 组合式函数 (Composables)

为了更好的代码复用,我们提取了通用的地图逻辑:

// use-sample-points.ts
export function useSamplePoints(map: Map) {const pointLayer = ref<VectorLayer<any> | null>(null)const clusterLayer = ref<VectorLayer<any> | null>(null)const vectorSource = ref<VectorSource<any> | null>(null)const originalFeatures = ref<Feature<any>[]>([])// 创建样点图层const createLayers = (geoJsonData: any) => {// 解析GeoJSON数据const features = new GeoJSON().readFeatures(geoJsonData, {featureProjection: 'EPSG:3857'})// 创建矢量源vectorSource.value = new VectorSource({features: features})// 创建聚合源const clusterSource = new Cluster({source: vectorSource.value,distance: 50})// 创建单点图层pointLayer.value = new VectorLayer({source: vectorSource.value,style: createPointStyle})// 创建聚合图层clusterLayer.value = new VectorLayer({source: clusterSource,style: createClusterStyle})// 添加到地图map.addLayer(pointLayer.value)map.addLayer(clusterLayer.value)}// 筛选功能const filterPointsByThreshold = (thresholdRange: string, property: string) => {if (!vectorSource.value) returnconst [min, max] = thresholdRange.split('-').map(Number)const features = vectorSource.value.getFeatures()features.forEach(feature => {const value = feature.get(property)const visible = value >= min && value <= maxfeature.setStyle(visible ? createPointStyle(feature) : null)})}// 高亮功能const highlightFeature = (feature: Feature<any>) => {// 清除之前的高亮clearHighlight()// 创建高亮样式const highlightStyle = new Style({image: new Circle({radius: 8,fill: new Fill({ color: '#ff6b6b' }),stroke: new Stroke({ color: '#fff', width: 3 })})})feature.setStyle(highlightStyle)currentHighlightedFeature.value = feature}// 定位功能const zoomToFeature = (featureId: string, options: any = {}) => {const feature = vectorSource.value?.getFeatureById(featureId)if (feature) {const geometry = feature.getGeometry()if (geometry) {const extent = geometry.getExtent()map.getView().fit(extent, {duration: options.duration || 1000,padding: options.padding || [50, 50, 50, 50]})}}}// 清理资源const cleanup = () => {if (pointLayer.value) {map.removeLayer(pointLayer.value)pointLayer.value.dispose()}if (clusterLayer.value) {map.removeLayer(clusterLayer.value)clusterLayer.value.dispose()}}return {createLayers,filterPointsByThreshold,highlightFeature,zoomToFeature,cleanup}
}

5. 数据流架构

在统一地图架构中,数据流的设计至关重要:

用户UnifiedMapAdvancedMapMapManagerMapConfigManagerPanel组件切换模式显示对应面板发送地图事件处理地图操作获取配置返回配置更新地图状态返回地图事件更新界面显示地图容器保持稳定,只切换面板内容用户UnifiedMapAdvancedMapMapManagerMapConfigManagerPanel组件

6. 统一地图页面

最后,我们创建了统一的地图页面,整合所有功能:

<!-- unified-map/index.vue -->
<template><div class="unified-map-container"><!-- 地图容器 --><div class="map-container"><AdvancedMapref="mapRef"map-id="unified-main-map"scene="default":center="mapCenter":zoom="mapZoom":show-base-map-switcher="true":show-map-info="true"@onload="handleMapLoad"@onclick="handleMapClick"@onviewchange="handleViewChange"/><!-- 图层控制面板 --><div class="control-panel left-panel"><WFSLayerControl /></div><!-- 属性信息弹窗 --><FeaturePopup ref="featurePopupRef" /></div><!-- Widget容器 --><template v-for="widget in visibleWidgets" :key="widget.key || widget.name"><MarsWidget :widget="widget" /></template><!-- 模式切换按钮 --><div class="mode-switcher"><button class="mode-btn" :class="{ active: useCustomLayout }"@click="toggleLayout"><component :is="useCustomLayout ? 'AppstoreOutlined' : 'LayoutOutlined'" /></button></div></div>
</template><script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import AdvancedMap from './components/AdvancedMap.vue'
import WFSLayerControl from './components/WFSLayerControl.vue'
import FeaturePopup from './components/FeaturePopup.vue'
import MarsWidget from './components/MarsWidget.vue'// 地图引用
const mapRef = ref()
const featurePopupRef = ref()// 地图状态
const mapCenter = ref<[number, number]>([113.07816, 30.61559])
const mapZoom = ref(7)// 布局模式
const useCustomLayout = ref(false)// 地图加载完成
const handleMapLoad = (map: Map) => {console.log('地图加载完成:', map)// 可以在这里进行一些初始化操作
}// 地图点击事件
const handleMapClick = (event: any) => {// 处理地图点击事件console.log('地图点击:', event)
}// 视图变化事件
const handleViewChange = (viewState: any) => {mapCenter.value = viewState.centermapZoom.value = viewState.zoom
}// 切换布局模式
const toggleLayout = () => {useCustomLayout.value = !useCustomLayout.value
}onMounted(() => {// 页面初始化
})
</script>

📊 重构效果对比

性能提升对比

重构后性能提升
重构前性能问题
复用地图实例
页面切换
缓存图层数据
保持样式状态
复用事件绑定
0.1-0.2秒响应
80-120MB
内存使用
<10%重复率
代码重复
重新初始化地图
页面切换
重新加载图层
重新设置样式
重新绑定事件
2-3秒等待时间
150-200MB
内存使用
60%+重复率
代码重复

详细性能数据

指标重构前重构后提升幅度
页面切换时间2-3秒0.1-0.2秒90%+
内存使用150-200MB80-120MB40%+
地图初始化次数每次切换仅一次100%
代码重复率60%+<10%80%+

用户体验改善

  • 无闪烁切换 - 地图保持稳定
  • 状态保持 - 缩放级别、中心点、选中图层
  • 快速响应 - 切换模式无需等待
  • 操作连贯 - 用户操作状态得到保持

开发效率提升

  • 代码复用 - 地图逻辑只需维护一份
  • 统一管理 - 所有地图功能集中管理
  • 易于扩展 - 新增功能只需添加面板组件
  • 类型安全 - TypeScript提供更好的开发体验

🎯 技术亮点

1. 配置驱动架构

通过JSON配置文件管理地图的各种参数,实现了:

map-configs.json
MapConfigManager
场景选择
default场景
xx-survey场景
time-space场景
onemap场景
high-precision场景
mobile场景
坐标系配置
底图配置
WFS图层配置
样式配置
EPSG:4326
EPSG:3857
EPSG:404000
天地图影像
天地图矢量
OpenStreetMap
县级区域
市级区域
行政区划

实现特性:

  • 多场景支持(默认、土壤调查、时空分析等)
  • 坐标系统一管理
  • 底图和图层配置化
  • 样式主题化

2. 组合式函数设计

基于Vue 3 Composition API,提取了可复用的逻辑:

  • use-sample-points.ts - 样点图层管理
  • use-thematic-map.ts - 专题地图功能
  • use-admin-district.ts - 行政区划管理

3. 事件驱动通信

建立了完善的事件系统:

  • 地图与面板之间的通信
  • 组件间的状态同步
  • 用户交互的响应处理

4. 响应式设计

支持多种设备:

  • 桌面端:完整功能展示
  • 平板端:自适应布局
  • 移动端:触摸优化

🚀 未来规划

短期目标

  • 完善单元测试覆盖
  • 添加更多地图控件
  • 优化移动端体验
  • 增加地图主题切换

长期目标

  • 支持3D地图渲染
  • 集成更多数据源
  • 实现离线地图功能
  • 添加地图编辑功能

💡 经验总结

1. 架构设计的重要性

好的架构设计是成功重构的基础。在开始编码之前,一定要:

  • 充分分析现有问题
  • 设计清晰的架构图
  • 考虑扩展性和维护性
  • 制定详细的实施计划

2. 渐进式重构

不要试图一次性重构所有代码,应该:

  • 先建立新的架构基础
  • 逐步迁移现有功能
  • 保持系统稳定运行
  • 及时收集用户反馈

3. 性能优化的思路

性能优化要从多个维度考虑:

  • 减少重复计算 - 缓存和复用
  • 减少DOM操作 - 虚拟化和懒加载
  • 优化内存使用 - 及时清理资源
  • 提升用户体验 - 响应式设计

4. 代码质量的重要性

高质量的代码是项目成功的关键:

  • 类型安全 - 使用TypeScript
  • 代码复用 - 提取通用逻辑
  • 文档完善 - 清晰的注释和文档
  • 测试覆盖 - 充分的单元测试

结语

这次重构让我深刻体会到了架构设计的重要性。通过建立统一的地图模块,我们不仅解决了性能问题,更重要的是为未来的功能扩展奠定了坚实的基础。

重构的过程虽然辛苦,但看到用户反馈的改善和开发效率的提升,所有的努力都是值得的。希望这篇文章能对正在考虑重构的朋友们有所帮助。

如果你对这次重构有任何疑问,或者想了解更多技术细节,欢迎在评论区交流讨论!


🏗️ 技术栈架构

地图功能模块
前端技术栈
底图管理
MapManager
WFS图层管理
事件系统
JSON配置
MapConfigManager
场景切换
参数管理
use-sample-points
Composables
use-thematic-map
use-admin-district
Composition API
Vue 3 + TypeScript
响应式系统
组件化架构
地图渲染
OpenLayers 7.x
图层管理
事件处理
坐标转换
快速构建
Vite
热更新
模块打包
UI组件
Ant Design Vue
主题系统
响应式布局
状态管理
Pinia
数据持久化
模块化状态

技术栈总结:

  • 前端框架:Vue 3 + TypeScript
  • 地图引擎:OpenLayers 7.x
  • 构建工具:Vite
  • UI组件:Ant Design Vue
  • 状态管理:Pinia
  • 样式方案:Less + WindiCSS

项目地址: [GitHub仓库链接]

作者简介: 10年全栈开发经验,专注于Vue生态和地图可视化技术

http://www.dtcms.com/a/395017.html

相关文章:

  • JVM实际内存占用
  • Spark SQL 桶抽样(Bucket Sampling)
  • 常见的【垃圾收集算法】
  • 如何解决 pip install 安装报错 ModuleNotFoundError: No module named ‘django’ 问题
  • jvm之【垃圾回收器】
  • Tomcat基础知识
  • Will、NGC游戏模拟器 Dolphin海豚模拟器2509最新版 电脑+安卓版 附游戏
  • ELK企业级日志分析系统详解:从入门到部署实践
  • 2025年Spring Security OAuth2实现github授权码模式登录
  • Kafka面试精讲 Day 22:Kafka Streams流处理
  • ELK大总结20250922
  • 基于Hadoop生态的汽车全生命周期数据分析与可视化平台-基于Python+Vue的二手车智能估价与市场分析系统
  • 基于TV模型利用Bregman分裂算法迭代对图像进行滤波和复原处理
  • 利用 Perfmon.exe 与 UMDH 组合分析 Windows 程序内存消耗
  • hello算法笔记 02
  • 二级域名解析与配置
  • 如何学习国库会计知识
  • 【读论文】压缩双梳光谱技术
  • Spark Structured Streaming端到端延迟优化实践指南
  • 【.NET实现输入法切换的多种方法解析】,第566篇
  • 性能测试-jmeter13-性能资源指标监控
  • 基于华为openEuler系统安装PDF查看器PdfDing
  • PyTorch 神经网络工具箱核心知识梳理
  • 【LangChain指南】Agents
  • Linux 的进程信号与中断的关系
  • IS-IS 协议中,是否在每个 L1/L2 设备上开启路由渗透
  • pycharm常用功能及快捷键
  • 滚珠导轨在半导体制造中如何实现高精度效率
  • 如何实现 5 μm 精度的视觉检测?不仅仅是相机的事
  • JavaScript学习笔记(六):运算符