用 ECharts 实现高性能地图可视化的实战经验分享
先看效果,再看代码
上周临时接了个需求:在后台展示全国门店的分布,还要能按区域筛选、点击弹窗查看详情。本来以为就是套个地图组件完事,结果搞了两天才上线——中间踩的坑真不少。
最终用的是 Leaflet + GeoJSON 的方案,轻量、灵活,适合我们这种不需要 3D 效果、也不需要 Google Maps 的场景。国内很多开发者喜欢直接上高德或百度 API,但说实话,如果你只是做静态可视化或者内网系统,Leaflet 真的够用,而且不卡。
先放一段核心初始化代码,这是我每次写地图都复制粘贴的模板:
const map = L.map('map').setView([35.8617, 104.1954], 5); // 中国中心
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: '© OpenStreetMap'
}).addTo(map);
注意这里 setView 的坐标是中国的地理中心,缩放级别设为 5 刚好能完整看到全国。如果你一开始 zoom 设太大(比如 8),用户打开地图会感觉“怎么只看到一块”;太小又看不清细节。这个值我调了三次才定下来。
点标记怎么做才不卡
最开始我用普通的 L.marker() 循环添加上千个门店点位,页面直接卡成幻灯片。后来换成 L.circleMarker(),并配合 Leaflet.markercluster 插件做了聚合,体验立马顺滑了。
代码长这样:
// 安装插件:npm install leaflet.markercluster
import 'leaflet.markercluster/dist/MarkerCluster.css';
import 'leaflet.markercluster';
const markers = L.markerClusterGroup();
data.forEach(item => {
const marker = L.circleMarker([item.lat, item.lng], {
radius: 6,
fillColor: '#f39c12',
color: '#c97f0d',
weight: 1,
opacity: 1,
fillOpacity: 0.8
});
marker.bindPopup(<h3>${item.name}</h3><p>地址:${item.address}</p>);
markers.addLayer(marker);
});
map.addLayer(markers);
这里的关键是:别把上千个标记直接 add 到 map 上,一定要用 cluster 包一层。不然你浏览器内存占用蹭蹭往上涨,尤其是低配电脑,用户点开就怀疑人生。
另外 circleMarker 比默认图标性能更好,因为它是 SVG 渲染的,缩放也清晰。你要是非要用图片图标,记得压缩尺寸,别塞个 100×100 的 png 进去。
GeoJSON 渲染省级区域边界
另一个常见需求是按省份着色,比如热力图那种。这时候就得上 GeoJSON 了。我在网上找了个中国省级边界的 GeoJSON 文件(choropleth 数据),加载进去后用 L.geoJSON 渲染。
fetch('https://jztheme.com/data/china-provinces.geojson')
.then(res => res.json())
.then(data => {
L.geoJSON(data, {
style: feature => ({
fillColor: getColorByData(feature.properties.name),
weight: 1,
color: '#666',
fillOpacity: 0.7
}),
onEachFeature: (feature, layer) => {
layer.bindPopup(<strong>省份:</strong>${feature.properties.name});
}
}).addTo(map);
});
其中 getColorByData() 是你自己写的逻辑,根据某个数值(比如销售额)返回颜色。我用了简单的条件判断,没上 d3-scale,毕竟项目急,没必要过度设计。
function getColorByData(provinceName) {
const value = salesData[provinceName] || 0;
return value > 1000 ? '#800026' :
value > 500 ? '#BD0026' :
value > 200 ? '#E31A1C' :
value > 100 ? '#FC4E2A' :
'#FEB24C';
}
这里有个巨坑:GeoJSON 文件里的省份名称和你数据里的 key 必须完全一致!我一开始拿的文件里是 “内蒙古自治区”,而我的销售数据 key 是 “内蒙古”,直接匹配不上,查了半天才发现是这问题。建议统一前处理一下名字映射表。
移动端手势差点翻车
测试时发现,在 iPad 上双指缩放特别不跟手,touchmove 响应慢半拍。排查半天发现是被页面其他事件干扰了。最后在 map 容器上加了 CSS 强制启用硬件加速:
#map {
height: 600px;
width: 100%;
touch-action: pan-x pan-y;
-webkit-transform: translateZ(0);
transform: translateZ(0);
}
同时确保没有其他元素覆盖在地图上方,否则 touch 事件会被拦截。我还见过有人给 body 加了 overflow: hidden 导致地图拖不动的……这种低级错误自己调试的时候根本想不到。
异步数据加载顺序要小心
还有一个容易忽略的问题:地图初始化完成后再加载数据。如果你在 map.setView() 之前就执行 addLayer(),可能会报错说 map not ready。
稳妥做法是在 map.on('load', ...) 或者至少等 DOM ready 后再请求数据:
map.whenReady(() => {
fetch('/api/stores').then(...).then(addToMap);
});
或者更简单粗暴:把数据加载逻辑放在 setTimeout 里延后 100ms 执行(别笑,生产环境真有人这么干,我也干过)。虽然不够优雅,但比白屏强。
自定义控件其实很简单
产品非要加个“重置视图”按钮,不想用手势滚回原状。其实 Leaflet 提供了 L.control 接口,可以轻松扩展:
const resetControl = L.Control.extend({
options: { position: 'bottomright' },
onAdd: function() {
const container = L.DomUtil.create('div', 'leaflet-bar');
container.innerHTML = '<a href="#" title="重置">⟲</a>';
L.DomEvent.on(container, 'click', () => {
map.setView([35.8617, 104.1954], 5);
});
return container;
}
});
map.addControl(new resetControl());
CSS 也得配套写一点,让按钮看起来像原生样式:
.leaflet-bar a {
background-color: white;
border-bottom: 1px solid #ccc;
text-align: center;
line-height: 30px;
width: 30px;
height: 30px;
text-decoration: none;
color: #333;
}
.leaflet-bar a:hover {
background-color: #f4f4f4;
}
别忘了阻止默认跳转行为,加一行 L.DomEvent.disableClickPropagation(container); 防止地图误触。
踩坑提醒:这三点一定注意
- 坐标顺序别搞反:GeoJSON 和 Leaflet 都是 [纬度, 经度],不是 [经度, 纬度]。我有一次从接口拿到的数据是 lng-lat,直接塞进去导致整个地图偏移几千公里,定位到西伯利亚去了……
- 大量数据分页或懒加载:超过 2000 个点建议做分区域加载,或者用 WebGL 方案如 Mapbox with deck.gl。Leaflet 不是万能的。
- 避免频繁 re-render:比如你在定时轮询更新位置,记得先
markers.clearLayers()再重新添加,不要每次都 new 一个 cluster group,否则内存泄漏。
还有个小技巧:开发时可以用浏览器的 Memory Snapshot 工具看看地图组件是否释放干净,特别是你做 SPA 路由跳转的时候。
进阶玩法:飞线动画试试看
最近在搞个新功能,想展示用户从 A 市到 B 市的流动路径。用了 L.polyline 加了个过渡动画:
function drawFlyLine(start, end) {
const line = L.polyline([start], {
color: '#00f',
opacity: 0.8,
weight: 2
}).addTo(map);
let progress = 0;
const duration = 2000;
const interval = 16;
const timer = setInterval(() => {
progress += interval / duration;
if (progress >= 1) {
line.setLatLngs([start, end]);
clearInterval(timer);
setTimeout(() => map.removeLayer(line), 1000);
return;
}
const currentLat = start[0] + (end[0] - start[0]) * progress;
const currentLng = start[1] + (end[1] - start[1]) * progress;
line.setLatLngs([start, [currentLat, currentLng]]);
}, interval);
}
视觉效果还行,就是 CPU 占用有点高,十几个动画同时跑就会卡。后续打算改成 CSS transform 动画或者用 canvas 自绘路径。但这已经是目前最快能上线的方案了。
这个技术的拓展用法还有很多
以上是我踩坑后的总结,希望对你有帮助。地图可视化不只是“放几个点”那么简单,性能、交互、数据结构都要考虑。不过 Leaflet 确实是个不错的起点,API 清晰,插件生态丰富,文档虽然英文但还算友好。
这个技巧的拓展用法还有很多,比如结合 ECharts 做迁徙图、用 Turf.js 做空间分析、甚至集成 Three.js 实现 3D 地形。后续会继续分享这类博客,包括怎么优化大数据量下的渲染效率,以及如何搭建离线地图服务。
以上是我个人对这个功能的完整讲解,有更优的实现方式欢迎评论区交流。毕竟前端这行,谁也不是一开始就写对的,都是改着改着就上线了……

暂无评论