
一、现状总览当前项目同时使用两套渲染引擎存在架构复杂、性能瓶颈和维护成本高的问题引擎用途涉及文件数核心问题Leaflet 1.9.4地图核心底图、轨迹、标记、弹窗、测距、热力图~58 个文件DOM/Canvas 渲染性能瓶颈插件过多维护成本高PixiJS 7.4.2设备图标渲染、呼吸动画、雷达扫描效果~8 个文件通过leaflet-pixi-overlay桥接架构冗余需维护两套渲染逻辑MapLibre GL JS 天然基于 WebGL能够同时替代 Leaflet 和 PixiJS 两套引擎一套搞定所有渲染需求。二、为什么选择 MapLibre GL JS对比项Leaflet 1.9.4MapLibre GL JS 4.x渲染引擎DOM CanvasWebGL2GPU 加速渲染性能万级点吃力十万级点流畅L.glify替代品内置3D 支持不支持支持倾斜、旋转、3D 地形矢量瓦片需要额外插件原生支持热力图插件leaflet-heat内置 heatmap 图层自定义 WebGL 渲染需leaflet-pixi-overlay PixiJSCustomLayerInterface 原生支持动画有限表达式驱动的动画、过渡效果开源许可BSDBSD从 Mapbox GL fork 而来社区活跃度活跃老牌非常活跃增长快三、API 对照映射表最核心的部分3.1 地图初始化LeafletMapLibre GL JSL.map(id, {center, zoom})new Map({container: id, center, zoom})L.tileLayer(url).addTo(map)map.addSource()map.addLayer({type: raster})map.setView(center, zoom)map.flyTo({center, zoom})或map.jumpTo({center, zoom})map.on(click, e e.latlng)map.on(click, e e.lngLat)坐标[lat, lng]坐标[lng, lat]⚠️ 顺序相反3.2 标记与弹窗LeafletMapLibre GL JSL.marker([lat,lng], {icon})new Marker({element}).setLngLat([lng,lat])L.icon({iconUrl, iconSize})new Marker({element: imgEl})或 GeoJSON 符号图层marker.bindPopup(html)new Popup().setHTML(html)L.divIcon({html})new Marker({element: divEl})或symbol-layer的icon-imageL.circleMarker([lat,lng], opts)circle图层 GeoJSON 点数据3.3 几何图形绘制LeafletMapLibre GL JSL.polyline(latlngs, {color})line图层 GeoJSONLineString源L.polygon(latlngs, {color})fill图层 GeoJSONPolygon源L.circle(center, {radius})circle图层 GeoJSONPoint源L.rectangle(bounds)fill图层 GeoJSONPolygon源L.geoJSON(data, {style})map.addSource(src, {type:geojson, data})map.addLayer()3.4 图层管理LeafletMapLibre GL JSL.layerGroup([...]).addTo(map)每个sourcelayer独立管理通过id控制L.featureGroup([...])同上无独立分组概念用source分组map.addLayer(layer)/map.removeLayer(layer)map.addLayer({id, ...})/map.removeLayer(id)layer.bringToFront()map.moveLayer(id, afterId)map.createPane(name, {zIndex})通过layer的before属性控制层级无需 Pane 概念3.5 热力图LeafletMapLibre GL JSL.heatLayer(latlngs, {radius, blur})map.addLayer({type: heatmap, paint: {heatmap-radius: ...}})基于 Canvas 软件渲染基于 WebGL GPU 硬件加速渲染性能提升显著3.6 控件LeafletMapLibre GL JSL.control.zoom()内置NavigationControlL.control.scale()内置ScaleControlL.control.polylineMeasure()需自定义或使用mapbox/mapbox-gl-draw 自行计算距离3.7 事件系统LeafletMapLibre GL JSmap.on(click, fn)map.on(click, fn)marker.on(click, fn)marker.getElement().addEventListener(click, fn)或图层点击map.on(zoomend, fn)map.on(zoomend, fn)map.on(moveend, fn)map.on(moveend, fn)图层事件layer.on(click)图层级事件map.on(click, layerId, fn)✨ 更强大四、PixiJS 替换方案4.1 当前 PixiJS 的核心用途功能当前实现替换方案设备图标~40 个 SpritePIXI.SpriteL.pixiOverlayMapLibresymbol图层 图标图片原生 WebGL 渲染呼吸圆动画PIXI.Graphics每帧重绘MapLibre 自定义 WebGL 图层CustomLayerInterface或用 CSS 动画叠加 Marker雷达扫描旋转PIXI.Spriterotation每帧更新MapLibre 自定义 WebGL 图层或image源 表达式动画图标点击交互sprite.interactive trueon(click)MapLibre 图层级事件map.on(click, layerId, fn)或Marker原生点击4.2 推荐方案用 MapLibre 原生能力替代 PixiJS方案 A最推荐 — 90% 场景使用 MapLibre 原生 symbol/icon 图层// 1. 加载设备图标到地图样式 map.loadImage(/assets/device-icon.png).then(image { map.addImage(device-icon, image); }); // 2. 添加 GeoJSON 数据源 map.addSource(devices, { type: geojson, data: { type: FeatureCollection, features: [...] } }); // 3. 创建 symbol 图层渲染设备图标 map.addLayer({ id: device-icons, type: symbol, source: devices, layout: { icon-image: device-icon, icon-size: [interpolate, [linear], [zoom], 10, 0.5, 20, 1.5], icon-allow-overlap: true }, paint: { icon-opacity: 1 } });方案 B动画/特效场景使用 MapLibre CustomLayerInterface对于呼吸动画、雷达扫描旋转等需要每帧更新的场景用 CustomLayerInterface 直接操作 WebGLmap.addLayer({ id: radar-scan, type: custom, renderingMode: 2d, onAdd: function(map, gl) { // 初始化 WebGL 程序、纹理、缓冲区 // 使用自定义着色器实现雷达扫描效果 }, render: function(gl, matrix) { // 每帧更新更新旋转角度、重新绘制 gl.uniformMatrix4fv(uMatrix, false, matrix); gl.drawArrays(gl.TRIANGLES, 0, 6); } });方案 C简单动画场景利用 expression 驱动动画MapLibre 的表达式系统支持一些基础动画效果比如呼吸脉冲可以通过heatmap图层的权重表达式或circle的半径动态控制来实现。4.3 具体替换对照原 PixiJS 代码MapLibre 替换代码new PIXI.Container()map.addSource()map.addLayer()new PIXI.Sprite(texture)symbol图层 icon-imagenew PIXI.Graphics()→drawCircle()circle图层 或 CustomLayersprite.anchor.set(0.5)icon-anchor: centersprite.scale.set(1/s)icon-size: [interpolate, [linear], [zoom], ...]sprite.x px; sprite.y pysetData()更新 GeoJSON 坐标sprite.rotation angle更新数据或自定义 WebGL 旋转矩阵requestAnimationFrameredraw()render回调CustomLayer或map.on(render, ...)renderer.render(container)由 MapLibre 内部渲染循环自动管理sprite.on(click, fn)map.on(click, layer-id, fn)PIXI.Assets.load(url)map.loadImage(url)五、插件替换方案当前 Leaflet 插件用途MapLibre 替代方案leaflet.chinatmsproviders天地图瓦片直接配置栅格源{tiles: [https://t{s}.tianditu.gov.cn/...]}leaflet.pm绘制/编辑多边形mapbox/mapbox-gl-draw或maplibre-gl-draw社区 forkleaflet.pixi-overlayPixiJS 桥接完全移除改用 MapLibre 原生图层leaflet-glifyWebGL 大点渲染MapLibre symbol/circle 图层原生支持leaflet-heat热力图MapLibre 内置 heatmap 图层leaflet-ruler测距mapbox-gl-draw 绘制 自定义测距计算leaflet-gradient渐变轨迹MapLibre line-gradient paint 属性leaflet-rotatedmarker旋转标记MapLibre symbolicon-rotate属性leaflet-kmlKML 加载转 GeoJSON 后加载天地图配置示例map.addSource(tianditu, { type: raster, tiles: [ https://t0.tianditu.gov.cn/vec_w/wmts?SERVICEWMTSREQUESTGetTileVERSION1.0.0LAYERvecSTYLEdefaultTILEMATRIXSETwFORMATtilesTILEMATRIX{z}TILEROW{y}TILECOL{x}tkYOUR_KEY ], tileSize: 256 }); map.addLayer({ id: tianditu-bg, type: raster, source: tianditu, before: first // 放在最底层 });六、分阶段迁移路线图第一阶段基础架构搭建预估 2-3 天安装maplibre-gl依赖移除leaflet、pixi.js、leaflet-pixi-overlay等依赖创建src/utils/maplibre.js封装工具函数坐标转换、公共配置改造src/utils/LngLonUtil.js增加[lng, lat]格式支持新建src/components/map-global-new/平行目录基于 MapLibre 实现最小可用地图第二阶段核心地图组件改造预估 3-5 天实现 MapLibre 版map-global底图加载在线/离线、图层管理、层级控制重建设备标记系统symbol图层替代PIXI.Sprite这是最重要的替换实现轨迹绘制line图层动态更新GeoJSON源替代L.polyline实现禁飞区/电子围栏fill图层替代L.polygon实现热力图MapLibre 内置heatmap图层替代leaflet-heat实现地图交互弹窗、点击、缩放、移动事件第三阶段PixiJS 特效替换预估 2-3 天呼吸圆动画CustomLayerInterface 或 CSS Marker 方案雷达扫描效果CustomLayerInterface 实现旋转纹理设备图标点击交互迁移到 MapLibre 图层事件体系第四阶段高级功能迁移预估 2-3 天数据回放组件改造仿真配置组件改造扇区参数组件中的地图改造测距功能基于mapbox-gl-draw重建绘制/编辑功能基于mapbox-gl-draw或maplibre-gl-draw重建全部 6 个轨迹工具模块uavTrack、tdoaTrack、radarTrack 等改造第五阶段集成与测试预估 2-3 天全部样式迁移.leaflet-*CSS → 自定义 CSS移除旧 Leaflet 依赖和代码性能对比测试帧率、内存、加载时间回归测试所有功能点总预估工时约 12-17 人天七、核心代码示例7.1 地图初始化MapLibre 版// src/utils/maplibre.js import { Map, NavigationControl, ScaleControl, Marker, Popup } from maplibre-gl; import maplibre-gl/dist/maplibre-gl.css; export function createMap(containerId, options {}) { const map new Map({ container: containerId, style: { version: 8, sources: {}, layers: [] }, center: options.center || [116.4, 39.9], // [lng, lat] zoom: options.zoom || 12, attributionControl: true }); map.addControl(new NavigationControl(), top-right); map.addControl(new ScaleControl(), bottom-left); return map; } // 添加在线底图天地图 export function addTiandituLayer(map, key) { map.addSource(tianditu-vec, { type: raster, tiles: [ https://t0.tianditu.gov.cn/vec_w/wmts?SERVICEWMTSREQUESTGetTileVERSION1.0.0LAYERvecSTYLEdefaultTILEMATRIXSETwFORMATtilesTILEMATRIX{z}TILEROW{y}TILECOL{x}tk${key} ], tileSize: 256 }); map.addLayer({ id: tianditu-vec, type: raster, source: tianditu-vec }); map.addSource(tianditu-cva, { type: raster, tiles: [ https://t0.tianditu.gov.cn/cva_w/wmts?SERVICEWMTSREQUESTGetTileVERSION1.0.0LAYERcvaSTYLEdefaultTILEMATRIXSETwFORMATtilesTILEMATRIX{z}TILEROW{y}TILECOL{x}tk${key} ], tileSize: 256 }); map.addLayer({ id: tianditu-cva, type: raster, source: tianditu-cva }); } // 坐标转换latlng - lnglat export function toLngLat(lat, lng) { return [lng, lat]; } export function toLatLng(lng, lat) { return { lat, lng }; }7.2 设备标记替代 PixiJS Sprite// 加载设备图标 export function loadDeviceIcon(map, iconUrl, iconName) { return map.loadImage(iconUrl).then(image { if (!map.hasImage(iconName)) { map.addImage(iconName, image, { sdf: false }); } return iconName; }); } // 添加设备标记图层 export function addDeviceLayer(map, deviceData, iconName) { const sourceId devices-source; const layerId devices-layer; // 移除旧源 if (map.getSource(sourceId)) { map.removeLayer(layerId); map.removeSource(sourceId); } map.addSource(sourceId, { type: geojson, data: { type: FeatureCollection, features: deviceData.map(d ({ type: Feature, properties: { id: d.id, name: d.name, status: d.status, type: d.type }, geometry: { type: Point, coordinates: [d.longitude, d.latitude] } })) } }); map.addLayer({ id: layerId, type: symbol, source: sourceId, layout: { icon-image: iconName, icon-size: [ interpolate, [linear], [zoom], 10, 0.5, 18, 1.2 ], icon-allow-overlap: true, icon-anchor: center, text-field: [get, name], text-offset: [0, 1.5], text-size: 12, text-optional: true }, paint: { icon-opacity: 1, text-color: #ffffff, text-halo-color: #000000, text-halo-width: 1 } }); return { sourceId, layerId }; } // 更新设备位置动态更新 export function updateDevicePosition(map, sourceId, deviceData) { const source map.getSource(sourceId); if (source) { source.setData({ type: FeatureCollection, features: deviceData.map(d ({ type: Feature, properties: { id: d.id }, geometry: { type: Point, coordinates: [d.longitude, d.latitude] } })) }); } } // 设备点击事件 export function onDeviceClick(map, layerId, callback) { map.on(click, layerId, (e) { const feature e.features[0]; if (feature) { callback(feature.properties, e.lngLat); } }); // 鼠标样式 map.on(mouseenter, layerId, () { map.getCanvas().style.cursor pointer; }); map.on(mouseleave, layerId, () { map.getCanvas().style.cursor ; }); }7.3 轨迹绘制替代 L.polyline// 添加无人机轨迹 export function addTrackLine(map, trackId, coordinates, color #ff0000) { const sourceId track-${trackId}; const layerId track-line-${trackId}; if (map.getSource(sourceId)) { map.getSource(sourceId).setData({ type: Feature, properties: {}, geometry: { type: LineString, coordinates: coordinates // [[lng1,lat1], [lng2,lat2], ...] } }); return; } map.addSource(sourceId, { type: geojson, data: { type: Feature, properties: {}, geometry: { type: LineString, coordinates: coordinates } } }); map.addLayer({ id: layerId, type: line, source: sourceId, layout: { line-join: round, line-cap: round }, paint: { line-color: color, line-width: 3, line-opacity: 0.8 } }); } // 渐变轨迹线替代 leaflet-gradient export function addGradientTrackLine(map, trackId, coordinates) { const sourceId gradient-track-${trackId}; const layerId gradient-track-line-${trackId}; map.addSource(sourceId, { type: geojson, lineMetrics: true, // 必须开启以支持渐变 data: { type: Feature, properties: {}, geometry: { type: LineString, coordinates: coordinates } } }); map.addLayer({ id: layerId, type: line, source: sourceId, layout: { line-join: round, line-cap: round }, paint: { line-width: 3, line-gradient: [ interpolate, [linear], [line-progress], 0, #ff0000, 0.5, #ff8800, 1, #00ff00 ], line-opacity: 0.8 } }); }7.4 热力图替代 leaflet-heatexport function addHeatmapLayer(map, heatmapId, points, options {}) { // points: [{latitude, longitude, intensity}, ...] const sourceId heatmap-${heatmapId}; const layerId heatmap-layer-${heatmapId}; map.addSource(sourceId, { type: geojson, data: { type: FeatureCollection, features: points.map(p ({ type: Feature, properties: { intensity: p.intensity || 1 }, geometry: { type: Point, coordinates: [p.longitude, p.latitude] } })) } }); map.addLayer({ id: layerId, type: heatmap, source: sourceId, paint: { heatmap-radius: options.radius || 30, heatmap-blur: options.blur || 15, heatmap-opacity: options.opacity || 0.8, heatmap-intensity: 1, heatmap-color: [ interpolate, [linear], [heatmap-density], 0, rgba(33,102,172,0), 0.2, rgb(103,169,207), 0.4, rgb(209,229,240), 0.6, rgb(253,219,199), 0.8, rgb(239,138,98), 1, rgb(178,24,43) ], heatmap-weight: [get, intensity] } }); }7.5 呼吸圆动画CustomLayer 替代 PIXI.Graphics// 通过 CSS 动画 Marker 实现简单呼吸效果 export function createBreathingMarker(map, lngLat, options {}) { const el document.createElement(div); el.className breathing-marker; el.innerHTML options.icon ? img src${options.icon} / : ; el.style.cssText width: 40px; height: 40px; background: radial-gradient(circle, rgba(56,189,248,0.6), rgba(56,189,248,0)); border-radius: 50%; animation: breathe 2s ease-in-out infinite; ; // 添加 CSS 动画 const style document.createElement(style); style.textContent keyframes breathe { 0%, 100% { transform: scale(1); opacity: 0.8; } 50% { transform: scale(1.3); opacity: 0.4; } } ; document.head.appendChild(style); return new Marker({ element: el }).setLngLat(lngLat).addTo(map); } // 如需更复杂的 WebGL 动画可参考 CustomLayerInterface 实现八、CSS 样式迁移原 Leaflet 样式类全部需要迁移到自定义样式原 Leaflet 类迁移方案.leaflet-popup-*自定义.maplibregl-popup-*或覆盖默认样式.leaflet-div-iconMapLibre 使用自定义 Marker 元素无需该类.leaflet-tooltip-*MapLibre 有内置 Tooltip 样式.leaflet-pixi-overlay完全移除不再需要.leaflet-layer2自定义图层容器.leaflet-right/.leaflet-barMapLibre 控件样式不同需重新定义九、风险与注意事项9.1 高风险项坐标顺序反转Leaflet[lat, lng]vs MapLibre[lng, lat]解决方案在src/utils/maplibre.js中统一封装转换函数所有组件调用封装函数建议在迁移过程中保留一个全局的toLngLat()/toLatLng()转换层天地图密钥天地图 WMTS 服务在 MapLibre 中配置方式和 Leaflet 不同需验证兼容性天地图矢量瓦片目前不直接支持 MapLibre 格式推荐使用栅格瓦片方式绘制/编辑功能原 leaflet.pmmapbox-gl-draw功能不如 leaflet.pm 丰富社区有maplibre-gl-draw但可能不够成熟需评估是否需要自行实现部分绘制功能9.2 中风险项离线地图原L.imageOverlayMapLibre 支持raster源加载离线瓦片兼容性好也支持image源加载单张图片覆盖PixiJS 动画替换简单动画呼吸、旋转可用 CSS Marker 或表达式实现复杂动画雷达扫描需 CustomLayer 实现学习成本较高性能MapLibre WebGL 渲染性能通常优于 Leaflet Canvas但需注意大量频繁的setData()调用可能触发重绘开销建议批量更新 GeoJSON 数据而非逐条更新9.3 建议不要一次性替换整个项目建议在新分支中从核心地图组件开始逐步替换各功能模块保留旧代码作为参考在迁移完成前保留完整的旧 Leaflet 组件先做小规模概念验证用 1-2 天时间先在独立页面验证 MapLibre 的核心能力天地图、设备标记、轨迹建立适配层创建src/utils/maplibre.jsAPI 封装层隔离 MapLibre 与业务代码十、总结维度评估可行性✅ 完全可行MapLibre 能覆盖所有 Leaflet 功能PixiJS 替换✅ 完全可移除MapLibre 内置 WebGL 能力替代性能提升⬆️ WebGL GPU 渲染预计 3-5 倍性能提升维护成本⬇️ 从两套渲染引擎减为一套长期维护成本降低迁移工作量中等偏高约 12-17 人天但收益显著推荐策略分阶段渐进式迁移从核心组件开始逐步替换