第14章 - 插件生态与常用插件
14.1 插件在 Leaflet 里的位置
Leaflet 核心只有约 1.6 万行代码(2.0 分支 src/ 下 70 个 JS 文件共 16343 行),官方把“不做进核心”的功能全部留给插件:聚合、热力、绘制、搜索、路由、时间轴、聚类、矢量切片。官方插件目录的规模能说明生态体量——docs/_plugins 下按 34 个分类收录了 475 个 Markdown 条目(含 1 个 template.md 模板,实际插件约 470 个),从 markers-icons(34 个)到 routing(5 个)都有。核心越薄,插件越多,代价是选型与兼容性判断全部落到使用者身上。
2.0 重写带来一个硬约束:生态正在迁移,绝大多数插件还不支持 2.0。官方目录里明确标记 compatible-v2: true 的只有 14 个:
leaflet-burgermenu Leaflet.markercluster Leaflet.heat
leaflet-edit-osm Leaflet.Polydraw Leaflet GeoSearch
leaflet-pointable leaflet-control-layers-inline
Leaflet.Boating Leaflet.DirectionMarker
Leaflet.MousePosition.ts location-picker
PartiallyEditablePolyline Geocoding from MapTiler
反过来,常见的主力插件 Leaflet.draw、Proj4Leaflet 在目录里仍是 compatible-v2: false。这不是“插件不好”,而是它们依赖的 L.Class.extend()、L.Control.extend()、@types/leaflet 在 2.0 都被移除了。选型顺序应该是:先确认 Leaflet 版本 → 再筛兼容插件 → 最后才看功能匹配。
14.2 判断一个插件能不能用
不需要读完全部源码,按这个顺序检查五分钟就能定性:
peerDependencies是否接受 2.0 beta。写成^2.0.0-beta.1才算支持;写成^1.9.0的装不上,写成^2.0.0的会在 beta 期被 npm 拒绝(预发布版本不匹配普通范围)。- 包是否为 ES module 且导出类。2.0 的插件规范要求具名导出(
import {MarkerClusterGroup} from 'leaflet.markercluster'),不再往window.L上挂东西。仍用L.xxx = ...副作用的插件,在纯 ESM 页面里不会生效。 - 是否仍调用
L.Class.extend()/initialize()。这两者在 2.0.0-beta.1 已移除,调用直接抛is not a function。可以在node_modules/<plugin>/dist里搜extend({快速确认。 - 是否依赖深层导入。
leaflet/src/...和leaflet/dist/...(除 CSS)在 2.0 已不再解析,这类插件安装后会报ERR_PACKAGE_PATH_NOT_EXPORTED。 - 类型从哪来。2.0 自带类型并移除了
@types/leaflet;依赖@types/leaflet的插件类型会编译失败,只能先// @ts-expect-error或等作者更新。
一个已经在做“同时支持 1.9 与 2.0”的插件,package.json 长这样(取自官方插件指南):
{
"name": "leaflet-my-plugin",
"type": "module",
"exports": "./src/MyLayer.js",
"sideEffects": false,
"peerDependencies": {
"leaflet": "^1.9.0 || ^2.0.0-beta.1"
}
}
sideEffects: false 意味着“导入这个模块不改变外部状态”,打包器可以安全 tree-shake。反过来,如果插件在导入时就调 Marker.include() 打补丁,必须把那些文件列进 sideEffects,否则会被打包器整个删掉。
14.3 Leaflet.markercluster:十万级点聚合
这是生态里最常用的插件,2.0 分支是 @next。它把邻近的 marker 合成一个带数量的簇图标,缩放时逐级拆分,只把视口附近的簇放进地图。
npm install leaflet@beta leaflet.markercluster@next
import * as L from 'leaflet';
import {MarkerClusterGroup} from 'leaflet.markercluster';
import 'leaflet.markercluster/dist/MarkerCluster.Default.css';
const map = new L.LeafletMap('map').setView([39.9042, 116.4074], 10);
new L.TileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {maxZoom: 19}).addTo(map);
const points = Array.from({length: 50000}, () => [
39.5 + Math.random() * 0.8,
116.1 + Math.random() * 0.6
]);
const clusters = new MarkerClusterGroup({
maxClusterRadius: 80, // 像素,簇中心到成员的最大半径
disableClusteringAtZoom: 15, // 该级别及以上不再聚合
removeOutsideVisibleBounds: true,
chunkedLoading: true, // 分块加入,避免主线程卡死
chunkInterval: 200,
chunkDelay: 50
});
clusters.addLayers(points.map(([lat, lng]) => new L.Marker([lat, lng])));
map.addLayer(clusters);
几个参数直接决定体感:
| 选项 | 默认值 | 作用 |
|---|---|---|
maxClusterRadius |
80 |
像素半径,越小簇越多越碎;可传函数按 zoom 返回 |
disableClusteringAtZoom |
null |
到该级别停止聚合,通常配合关闭 spiderfyOnMaxZoom |
removeOutsideVisibleBounds |
true |
把视口外的簇/点摘出地图,长距离平移时省内存 |
animate |
true |
簇拆分合并动画;prefers-reduced-motion 下降级为淡入淡出 |
animationDuration |
300 |
动画毫秒数 |
chunkedLoading |
false |
分块加载,大数据集必开 |
chunkInterval / chunkDelay |
200 / 50 |
每块工作时长 / 块间隔(毫秒) |
spiderfyOnMaxZoom |
true |
无法再放大时散开簇内 marker |
showCoverageOnHover |
true |
悬停时画出簇覆盖范围多边形 |
singleMarkerMode |
false |
单个 marker 也套簇图标,视觉统一 |
源码 README 给的量级参考:addLayers 处理 20 万个 marker 在桌面端约 0.4 秒;再大就用 chunkedLoading 拆开,用 chunkProgress 做进度条。
事件有两类,都通过 e.layer 取对象:
clusters.on('click', e => {
console.log('marker', e.layer.getLatLng());
});
clusters.on('clusterclick', e => {
console.log('簇内', e.layer.getChildCount(), '个');
// 想改掉默认的“点击放大”,阻止原始指针事件的默认行为
if (e.layer.getChildCount() > 500) {
e.originalEvent.preventDefault();
}
});
clusters.on('spiderfied', e => {
console.log('散开', e.markers.length, '个 marker');
});
簇对象上的方法在自定义交互里很有用:getChildCount()、getAllChildMarkers()、getBounds()、getConvexHull()、zoomToBounds(fitBoundsOptions)、spiderfy() / unspiderfy()。
有一个方法值得单独记:zoomToShowLayer(marker, callback)。从搜索框定位到某个被折叠的 marker 时,直接 setView 只能让地图移动,marker 仍藏在簇里;这个方法会逐级放大、必要时散开,等 marker 真正可见后回调:
clusters.zoomToShowLayer(marker, () => {
marker.openPopup();
});
无障碍方面,簇被实现为可按 Tab 聚焦的按钮,读屏软件念出 “N markers”,文案可用 clusterLabel 翻译;能散开的簇带 aria-expanded,散开后的 marker 在 Tab 顺序里紧跟簇之后。
14.4 Leaflet.heat:热力图
体积小、依赖 simpleheat,在 <canvas> 上画点密度,并按网格合并邻近点。
import {LeafletMap, TileLayer} from 'leaflet';
import {HeatLayer} from 'leaflet.heat';
const map = new LeafletMap('map').setView([39.9042, 116.4074], 11);
new TileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {maxZoom: 19}).addTo(map);
// [lat, lng, intensity],intensity 省略时为 1
const points = [
[39.9042, 116.4074, 0.8],
[39.9142, 116.4174, 0.5],
[39.8942, 116.3974, 1.0]
];
const heat = new HeatLayer(points, {
radius: 25, // 每个点的影响半径(像素)
blur: 15, // 模糊量
minOpacity: 0.05,
max: 1,
gradient: {0.4: 'blue', 0.65: 'lime', 1: 'red'},
pane: 'overlayPane'
}).addTo(map);
heat.addLatLng([39.92, 116.42, 0.7]);
heat.setOptions({radius: 30});
关键算法在 README 里写得很明确:相距近的点会被合并进 (radius + blur) / 2 像素的网格单元,强度相加;低于 maxZoom 时,每低一级强度减半。所以低缩放级别下 max 比较的是“整个网格内所有点的强度和”,不是单点值——调参时这决定了低层级是不是一片红。
14.5 Leaflet GeoSearch:搜索控件
地理搜索类里最完整的一个,内置 AMap、Bing、Esri、Google、Mapbox、Nominatim、Pelias、Photon 等 provider,并支持自定义 provider 接内网服务。
npm install leaflet-geosearch
用法与普通控件一致,addTo(map) 即可;核心差异是 provider 决定请求地址与结果解析。5.0 版本变更里有两条与安全、体验直接相关:默认的 resultFormat / popupFormat 会对 label 做转义(此前可能注入 HTML),选中建议项后直接显示而不再重复请求一次(requeryOnSelect: true 恢复旧行为),搜索失败改为在地图上触发 geosearch/error 而不是抛未处理的 Promise rejection。
14.6 分类速查
按官方目录的分类整理,标出最常见的几个方向,方便按需检索:
| 分类 | 条目数 | 代表插件 |
|---|---|---|
| 标记与图标 | 34 | leaflet-awesomemarkers、leaflet-rotated-marker(2.0 已内置 rotation) |
| 导航 | 22 | leaflet-fullscreen(2.0 已内置 FullscreenControl)、leaflet-zoomslider |
| 线与形状 | 21 | leaflet-polylinedecorator、leaflet-geodesic |
| 覆盖动画 | 20 | leaflet-ant-path、leaflet-marker-slideto |
| 编辑几何 | 20 | Leaflet.draw(v2 未兼容)、leaflet-geoman |
| 图层切换与图例 | 20 | leaflet-groupedlayercontrol、leaflet-legend |
| 瓦片显示 | 21 | leaflet-tilelayer-gl、tilelayer-boundarycanvas |
| 矢量瓦片 | 8 | protomaps-leaflet、leaflet-maplibre |
| 聚合去簇 | 10 | Leaflet.markercluster(v2 兼容)、prunecluster |
| 热力 | 6 | Leaflet.heat(v2 兼容)、heatcanvas |
| 地理处理 | 12 | Proj4Leaflet(v2 未兼容)、leaflet-geometryutil |
| 数据格式 | 17 | leaflet-gpx、leaflet-shapefile、leaflet-omnivore |
数字来自 2.0 分支 docs/_plugins 的实际条目数(34 个分类目录、475 个 .md 文件,其中 template.md 是模板),会随上游更新变化,选型时以官网 plugins 页面为准。
14.7 排错
TypeError: L.Class.extend is not a function。 插件在 Leaflet 2 下用了 1.x 的继承 API。降级到 Leaflet 1.9.4,或换用已适配 2.0 的版本。1.x 与 2.0 的 API 差异无法在应用层打补丁绕过。
Cannot read properties of undefined (reading 'Marker')。 页面只有 ESM 版 Leaflet,插件却在找全局 L。2.0 的 dist/leaflet.js 不定义 window.L,需要全局变量时用 dist/leaflet-global.js,且插件也要用对应的 global 构建(如 leaflet.markercluster-global.js),两者不能混用。
instanceof 判断为 false,弹窗/marker 行为异常。 页面里存在两份 Leaflet:一份通过 import map 引入,另一份被插件的打包产物内联。检查 package.json 是否把 leaflet 放进了 dependencies 而不是 peerDependencies。同一个页面只能用同一份 Leaflet 实例。
簇图标不显示。 忘记引入 MarkerCluster.Default.css。自己写了 iconCreateFunction 的话不需要这个 CSS,但需要给 DivIcon 的 HTML 提供完整样式。
热力图整片颜色过饱和。 max 默认 1,点位密集时网格强度和远超 1。按数据量级调大 max(例如设成单点强度的 10 倍),或降低每点 intensity。
插件的事件名还是 mouseover。 2.0 插件应改用 pointerover;仍在用 mouse* 的插件即使能加载,事件也不会触发,控制台只报一次移除警告。
ERR_PACKAGE_PATH_NOT_EXPORTED。 导入路径踩到了 2.0 不再暴露的深层路径。只允许 leaflet 主入口和 leaflet/styles.css(或 leaflet/dist/leaflet.css),其它一律改用具名导出。
14.8 选型建议
- 先确认主线版本再选插件。新项目直接上 2.0 的话,可用插件只有那 14 个兼容项;如果业务必需
Leaflet.draw、Proj4Leaflet这类未迁移的插件,答案是锁定 1.9.4,并在架构上把地图层隔离,等生态跟上再评估升级。 - 能用核心就别用插件。2.0 已把
FullscreenControl、markerrotation、map.translate收进核心,过去需要 three 个插件的能力现在零依赖。 - 插件越少越好。每多一个插件就多一份
peerDependencies冲突、多一次事件系统适配、多一份 CSS 污染。聚合 + 热力 + 搜索三个就够覆盖多数业务地图。 - 给插件写一层薄封装。业务代码不直接
new MarkerClusterGroup(),而是走自己的createClusterLayer(options),将来换实现或升级版本只改一处。 - 锁版本。beta 期的插件 API 会变,
package-lock.json必须提交,CI 用npm ci。 - 关注
compatible-v2标记。官方目录会随插件迁移更新,比在 issue 里翻讨论快。
14.9 本章小结
| 要点 | 结论 |
|---|---|
| 生态规模 | 34 个分类、475 个条目(约 470 个插件,另 1 个模板);2.0 兼容标记仅 14 个 |
| 兼容判断 | 看 peerDependencies、是否 ESM 具名导出、是否用 Class.extend |
| 迁移阻碍 | Class.extend/initialize 移除、深层导入失效、@types/leaflet 移除 |
| 聚合 | MarkerClusterGroup,maxClusterRadius 80,20 万点约 0.4s,大数据开 chunkedLoading |
| 热力 | HeatLayer,radius 25 / blur 15,网格合并后强度相加 |
| 搜索 | leaflet-geosearch,provider 架构,5.0 默认转义 label |
| 依赖规范 | leaflet 放 peerDependencies,一份实例,不混用 global 与 ESM |
| 策略 | 1.9 + 必需插件,或 2.0 + 14 个兼容插件,二选一,别硬缝 |