第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 判断一个插件能不能用

不需要读完全部源码,按这个顺序检查五分钟就能定性:

  1. peerDependencies 是否接受 2.0 beta。写成 ^2.0.0-beta.1 才算支持;写成 ^1.9.0 的装不上,写成 ^2.0.0 的会在 beta 期被 npm 拒绝(预发布版本不匹配普通范围)。
  2. 包是否为 ES module 且导出类。2.0 的插件规范要求具名导出(import {MarkerClusterGroup} from 'leaflet.markercluster'),不再往 window.L 上挂东西。仍用 L.xxx = ... 副作用的插件,在纯 ESM 页面里不会生效。
  3. 是否仍调用 L.Class.extend() / initialize()。这两者在 2.0.0-beta.1 已移除,调用直接抛 is not a function。可以在 node_modules/<plugin>/dist 里搜 extend({ 快速确认。
  4. 是否依赖深层导入。leaflet/src/... 和 leaflet/dist/...(除 CSS)在 2.0 已不再解析,这类插件安装后会报 ERR_PACKAGE_PATH_NOT_EXPORTED。
  5. 类型从哪来。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、marker rotation、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 个兼容插件,二选一,别硬缝