简介:面向网页地图开发与Web GIS应用场景的Leaflet离线地图案例合集,压缩包约14MB,涵盖不同难度层次,共723个文件。资源包含大量JavaScript脚本、HTML示例页面、CSS样式,以及PNG图标和Markdown说明文档,覆盖瓦片加载、图层管理、标记弹层、控件配置、事件监听、路径绘制、投影设置、性能优化等十几类主题。每个Demo独立成例,便于按需检索、对照修改。已有4147人学习下载。对于需要在网页中集成离线地图或搭建轻量级地理信息系统的开发者,这套合集提供了从基础显示到高级交互的完整代码参考,并涉及地理编码、自定义图层与响应式适配等实用技巧,可显著降低上手门槛。
1. 离线地图不是把网页存下来:Leaflet demo集合到底能帮你解决什么
做GIS Web开发的人,迟早会遇到一个需求:内网环境部署、数据敏感不能上公网地图,或者客户要求大屏在断网时也能操作。这时候Leaflet离线地图几乎是被问得最多的方案。这套demo集合把离线地图常用的初始化、瓦片加载、覆盖物、绘制、轨迹、聚合拆成了大量可直接运行的案例,注释和参数说明也够细。新手能照着跑通,熟手能直接抽代码改参数。资源解决的是离线地图从零到可用这件事,适合GIS开发、前端工程师和可视化项目交付人员。
2. 为什么选Leaflet做离线地图:瓦片机制与demo集合的目录结构
2.1 离线地图的技术基础:栅格瓦片与金字塔结构
先明确一个概念:离线地图不是把网页另存为,而是把底图切成一张张256x256的PNG图片,放在服务器上让Leaflet按需请求。Leaflet的L.tileLayer请求地址形如tiles/z/x/y.png,z表示缩放级别,x和y表示瓦片编号。级别越高,瓦片数量爆炸式增长:z级别对应的单层瓦片数是2^z乘以2^z,也就是2^(2z)张。比如第3层有64张,第4层有256张,到了第18层一张全中国范围的瓦片数量会达到惊人的2^36张,所以离线瓦片必须配合行政边界或自定义范围进行裁剪,不能盲目整层下载。
瓦片金字塔组织方式决定了Leaflet的工作方式。每个层级把一张总图切成多个小方块,往下每层分叉成4个瓦片。Leaflet在渲染时,根据当前可视范围bound和zoom动态计算需要请求哪些x/y编号,这个计算是内置的,开发者不需要手写。但你只要理解了x向右增加、y向下增加,就能在调试Network面板时快速判断URL是否正确。很多demo集合的第一个案例就是打印当前可视区域的瓦片编号范围,方便你验证瓦片服务是否连通。
坐标系是另一个绕不开的地基。Leaflet默认CRS是EPSG:3857,也就是Web墨卡托投影。公开地图源的切片底图几乎都基于3857,所以本地瓦片下载时投影参数必须选择EPSG:3857。如果下载工具里误选了EPSG:4326,瓦片图片的长宽比会与Leaflet预期不一致,最终表现出来就是底图被拉伸或偏移。我遇到过同事把ArcGIS导出的WGS84切片直接塞给Leaflet,结果点位全部偏到海里,这就是投影层面的错。
栅格瓦片还有一个容易被忽略的细节:瓦片格式。标准XYZ瓦片常用PNG或JPG,PNG体积大但支持透明,JPG体积小但通常有白底。地图下载器导出的瓦片如果带空白边,地图叠加时会显示白边,处理办法是在下载参数里开启裁剪或填充设置。demo集合里一般会预先处理好瓦片,你只需要关注涵盖的层级是否完整。
2.2 demo集合的目录划分与加载方式
这套demo集合的目录结构,建议按功能线去读。一般会分为basics、overlays、interactions、biz-demos四块。basics包含底图初始化、视图控制、图层控件、比例尺;overlays则把所有覆盖物案例集中在一起,包括自定义icon、popup、tooltip、circle、rectangle、polyline、polygon;interactions主要是事件绑定、右键菜单、键盘操作。biz-demos是业务向的完整案例,比如轨迹回放、聚合标记、热力图、坐标转换。这样分的好处是,你在业务里遇到某个需求,直接进对应目录找相似demo,而不是在整个文件夹里乱翻。
加载方式上,一种是用VS Code的Live Server起本地服务,另一种是直接放到nginx或IIS下。我不建议用file协议直接打开HTML,因为很多插件会通过fetch或XHR加载GeoJSON、样式、Worker脚本,file协议下会被浏览器安全策略拦截,报CORS错误。以下是一个典型的目录骨架:
| 目录名 | 典型文件 | 核心内容 | 适合场景 |
|---|---|---|---|
| basics | map-init.html | 地图初始化、底图切换、视图跳转 | 跑通框架 |
| overlays | marker-icon.html | 自定义标记、弹窗、几何图形 | 标绘展示 |
| interactions | draw-polygon.html | 绘制工具、事件回调、数据提取 | 用户交互 |
| biz-demos | track-playback.html | 轨迹回放、聚合、热力叠加 | 业务可视化 |
实际使用中,我一般把demo整个拷进一个项目子目录,比如/map-demo,然后用nginx把根目录指到项目目录。要注意Linux静态服务器对路径大小写敏感,tiles和Tiles会被当成两个不同目录。demo集合里如果引用了相对路径,目录层级不要随意改动,否则瓦片路径会断。还有一点,有些案例依赖第三方插件的CDN,离线部署时必须把JS和CSS下载到本地,替换script标签中的外链地址,否则内网会加载失败。
2.3 为什么不是OpenLayers或MapLibre:选型对比
有人会问,离线地图为什么不用OpenLayers?OpenLayers功能全面,内置了WMTS、WMS、向量切片等一堆加载器,但同样一个底图初始化,Leaflet的代码量大概只有OpenLayers的一半。更重要的是,离线地图这个细分场景,社区资料和现成demo大半都是Leaflet写的。MapLibre虽然渲染效果好,主打矢量瓦片,但如果你的瓦片源是栅格XYZ,MapLibre并不能带来额外的性能优势,反而要处理GL样式配置的复杂度。
Leaflet的插件生态也是离线场景的加分项。坐标转换、聚合、绘制、热力图、轨迹回放都有对应插件,而且插件体积普遍轻量。demo集合里大量案例都是基于这些插件写好的,你直接拿过来调参就行。对于多数业务系统,Leaflet的性能完全够用——单页加载几百个marker、几十个图层都比较流畅,没必要引入更重的框架。选型的核心还是看团队熟悉度和交付周期。如果你需要在三维场景里叠加地形,那就另说,但二维GIS和可视化管理后台,Leaflet是低风险高回报的选项。
3. 从demo里抄作业:地图初始化和三组高频功能的可运行代码
3.1 最小离线地图初始化:容器、CSS与视图控制
先给一个最基础、能直接复制的离线地图初始化。假设瓦片已经放在项目根目录下的tiles文件夹,层级结构是tiles/{z}/{x}/{y}.png。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>离线地图最小示例</title> <link rel="stylesheet" href="libs/leaflet/leaflet.css" /> <style> html, body, #map { height: 100%; margin: 0; } </style> </head> <body> <div id="map"></div> <script src="libs/leaflet/leaflet.js"></script> <script> const map = L.map('map', { center: [30.5, 114.3], zoom: 12, zoomControl: true, attributionControl: false }); L.tileLayer('tiles/{z}/{x}/{y}.png', { minZoom: 3, maxZoom: 18, tileSize: 256, keepBuffer: 2 }).addTo(map); </script> </body> </html>这段代码是Leaflet离线地图的最小闭环。center是经纬度坐标,如果数据源是GCJ02坐标,center也要填GCJ02的值。zoom是初始缩放级别,一般结合业务重点区域来定,过一个厂区用15~16,过一个地级市用10~12。minZoom和maxZoom限制瓦片请求范围,超出范围会出现灰色底图,所以下载瓦片时的范围要跟这两个值对齐。tileSize大多数是256,高清瓦片可能是512,如果不匹配,底图会显得模糊或者出现重复。keepBuffer是预加载缓冲区,值越大滑动越跟手,但内存占用也越高,根据目标设备性能调整。
跑通这个demo后,可以再添加比例尺控件和全屏控件,demo集合里都有现成代码。我一般会把map实例挂在window上方便调试,生产环境则用模块化封装,避免全局污染。如果页面还有放大的按钮,可以通过map.setView([lat, lng], zoom)来跳转,这在业务里很常用。
3.2 覆盖物与交互:标记、弹窗、矢量绘制的demo套路
覆盖物是业务开发里最常用的模块。marker、popup、circle、polyline这类,demo通常会给三种写法的对比:直接添加、从数组循环添加、带事件绑定添加。下面这段是从demo里精简出来的"数组循环添加标记并绑定弹窗"模板:
const points = [ { lat: 30.55, lng: 114.32, name: '点位A' }, { lat: 30.56, lng: 114.36, name: '点位B' } ]; points.forEach(p => { const marker = L.marker([p.lat, p.lng]).addTo(map); marker.bindPopup(`<b>${p.name}</b><br>坐标:${p.lat}, ${p.lng}`); marker.on('click', () => { // 业务里常见的是打开侧边面板 console.log(marker.getLatLng()); }); });bindPopup传的是HTML字符串,Leaflet会解析成弹窗内容。这里有个性能陷阱:点位超过几百个时,用forEach加marker会导致DOM节点过多,页面上开始卡。常见做法是使用L.markerClusterGroup聚合,或者使用Canvas渲染图层的插件。如果你只是要显示大量纯坐标点,可以考虑用L.circleMarker并设置半径为一个很小的值,性能远好于L.marker。
矢量绘制方面,L.polyline传一个latlng数组就能生成轨迹或边界,线宽和颜色都在options里配:
const latlngs = [ [30.50, 114.30], [30.51, 114.31], [30.52, 114.33] ]; L.polyline(latlngs, { color: '#ff0000', weight: 3, opacity: 0.8 }).addTo(map);color用十六进制字符串,weight是像素宽度,opacity是透明度。如果需要带箭头或者虚线,要在options里设置dashArray。如果是多边形,把L.polyline换成L.polygon即可,闭合由Leaflet自动处理。业务里最常见的需求是用户在地图上画多边形,然后拿到范围去做空间查询。这个需要引入leaflet.draw插件,核心套路是初始化控制条,监听draw:created事件,再对layer.toGeoJSON()做后续存储。demo集合里的绘制案例已经把工具栏汉化和回显写好了,你只要改接口地址就行。
3.3 业务场景:轨迹回放、聚合标记与热力图扩展
轨迹回放是离线地图demo里比较出效果的功能。原理很简单:不移动地图,而是移动marker,或者边移动边追加轨迹线。下面是一个基于定时器的marker轨迹回放骨架:
const route = [ [30.50, 114.30], [30.51, 114.31], [30.52, 114.33], [30.53, 114.35] ]; let step = 0; const carMarker = L.marker(route[0]).addTo(map); const routeLine = L.polyline([], { color: '#f00' }).addTo(map); const timer = setInterval(() => { if (step >= route.length) { clearInterval(timer); return; } carMarker.setLatLng(route[step]); routeLine.addLatLng(route[step]); step++; }, 500);setInterval控制回放节奏,真实项目里用requestAnimationFrame会更平滑,或者把点位数据异步加载后再播放。carMarker.setLatLng会移动marker,而不是重新创建,这样可以减少DOM操作。routeLine.addLatLng是逐步追加顶点,避免了全量替换路线数组。如果轨迹点很多,建议先抽稀再回放。
聚合标记是另一个高频场景。引入leaflet.markercluster插件后,把marker都放进L.markerClusterGroup再addTo(map)即可:
const markers = points.map(p => L.marker([p.lat, p.lng])); const clusterGroup = L.markerClusterGroup({ maxClusterRadius: 60, spiderfyOnMaxZoom: true }); clusterGroup.addLayers(markers); map.addLayer(clusterGroup);maxClusterRadius是聚合半径,数值越小,聚合越不明显,但展开后marker越清晰。spiderfyOnMaxZoom控制最大缩放级别时是否展开成蜘蛛腿,在marker密集区域很有用。注意聚合插件依赖Leaflet主版本,用Leaflet 1.x就要选配套的插件版本,否则报L.markerClusterGroup is not a function。demo集合里如果带热力图案例,一般是引入leaflet.heat插件,传入[[lat,lng,weight]]数组即可。热力权重建议归一化到0~1,否则颜色分布会失真。
4. 离线地图demo的常见坑:坐标系、跨域与缓存避坑记录
4.1 瓦片显示空白但控制台没有报错
现象:地图容器出来了,控制台也没有404,但页面是大片灰色。 原因:大概率是瓦片路径层级对不上。很多下载工具导出的路径是tiles/12/1234/567.png,但Leaflet的URL模板写成tiles/{z}/{x}/{y}.png,中间少了zoom目录,或者x/y顺序反了。还有一种情况是瓦片加载成功但z-index被某些CSS样式盖住。 解决:在Network面板过滤图片请求,看实际请求的URL和文件路径是否一致。如果看到请求返回200但瓦片尺寸异常,多半是瓦片本身损坏。我一般会写脚本,随机抽查三个层级下的瓦片路径是否存在,直接排除路径问题。如果底图被覆盖,检查父容器有没有position和z-index的干扰。
4.2 标记点位偏移:WGS84与GCJ02混用
现象:marker和瓦片底图对不上,点位明显偏移几十米到几百米。 原因:国内离线瓦片来源通常是高德或天地图,基于GCJ02或CGCS2000,而业务数据用的是WGS84。Leaflet默认坐标被认为是WGS84,如果底图不是WGS84,数据点必须做偏移转换。 解决:统一坐标系。如果底图是GCJ02,把业务坐标先转成GCJ02再塞给Leaflet;或者使用坐标系转换插件,但手动做三参数转换更可控。我习惯在进入Leaflet前完成转换,不在渲染层做,避免每个图层都去转一遍。转换公式并不复杂,网上有现成算法,demo集合里的坐标转换案例就是专门演示这个的。遇到天地图瓦片时要注意,天地图默认是CGCS2000,它与WGS84在米级上误差很小,但在高精度场景仍然要做七参数转换。
4.3 浏览器本地文件打开跨域限制
现象:直接双击HTML,页面可以打开,但瓦片加载不出来,或者插件报跨域错误。 原因:file协议下,向本地文件夹请求资源会受到严格限制,尤其是某些浏览器对本地文件的AJAX和图片请求做安全拦截。Leaflet瓦片虽然是img标签,但部分插件用fetch加载GeoJSON,file协议会直接失败。 解决:用HTTP方式访问。最简单是进入demo目录执行python -m http.server 8080,然后在浏览器访问localhost:8080。如果用了nginx,注意worker脚本和静态资源跨域配置。还有一个容易忽略的点:如果瓦片服务放在另一个端口,Leaflet通过img标签请求瓦片通常不受跨域限制,但通过fetch请求瓦片元数据就会受CORS限制。
4.4 缓存导致上线后看不到新瓦片
现象:本地更新了瓦片或地图代码,但浏览器里看到的还是旧图。 原因:浏览器对图片有强缓存,瓦片文件名没变时,浏览器直接使用memory cache或disk cache。尤其在nginx默认配置下,PNG和JS都会被缓存较长时间。 解决:在nginx或后端响应头里设置Cache-Control: no-cache,或者对瓦片目录单独设置缓存策略。开发阶段可以频繁清缓存,或给瓦片URL加版本参数。但要注意,给瓦片URL加随机参数能破缓存,也会导致缓存命中率下降,如果瓦片量很大,请求参数会污染日志。生产环境更好的做法是更新瓦片时改目录名,比如tiles_v2,然后修改前端配置。
4.5 离线插件引入顺序错误
现象:引入插件后控制台报某个构造方法不存在,比如L.draw is undefined。 原因:Leaflet的插件大多在L命名空间上挂载方法,如果插件JS在Leaflet核心JS之前加载,或者插件之间有依赖关系,就会报错。比如leaflet.draw依赖leaflet.util,如果先加载draw后加载core,就找不到基础方法。 解决:严格按照demo里面的script顺序,Leaflet核心库在前,扩展插件在后。用了worker的插件还要注意baseURL设置,确保worker文件能被正确加载。判断顺序问题的方法:打开Network面板,看脚本加载顺序和报错时间点,报错发生在插件文件加载完成后,基本就是顺序问题。
5. 把离线瓦片做出来:从在线地图下载到本地部署的实操流程
5.1 瓦片下载工具选型与参数设置
手里没有现成瓦片,就需要从在线地图源下载。常见做法是使用地图下载器,桌面端有开源工具和商业工具,命令行也有可编程方案。选择时关注四点:是否支持多源、是否按z/x/y导出、是否支持断点续传、是否保持墨卡托投影。以下是常用参数对照:
| 参数项 | 推荐值 | 说明 |
|---|---|---|
| 坐标系 | EPSG:3857 | 与Leaflet默认CRS一致 |
| 瓦片尺寸 | 256 x 256 | 标准尺寸 |
| 图片格式 | PNG | 支持透明,体积可控 |
| 最小层级 | 3 | 全国浏览 |
| 最大层级 | 17或18 | 视需求,每多一级数量翻4倍 |
| 线程数 | 8~16 | 太高容易被源站限速 |
下载时最耗时间的不是单张瓦片,而是高层级数量太大。一个城市中心区到第18级可能超过数十万张,所以最好按行政区域边界裁剪下载,缩小范围。下载工具通常会提示下载面积,图层范围可以先用GeoJSON或框选。注意下载源的版权限制,涉密和商业项目要确认授权,否则后续使用有风险。
5.2 本地瓦片目录结构验证与打包
下载完成后,务必验证目录结构是否满足Leaflet的URL模板。可以在项目目录下执行命令:
# 列出瓦片目录结构,检查层级是否完整 find tiles -type d | head -20 # 统计瓦片总数,评估部署体积 find tiles -name "*.png" | wc -l # 抽检第14层级下的前5张瓦片大小 find tiles/14 -type f -name "*.png" | head -5 | xargs ls -lhfind命令的输出能直观看到tiles/{z}/{x}/{y}的层级关系。如果导出的路径是tiles/14/xxx/yyy.png,而Leaflet模板需要tiles/{z}/{x}/{y}.png,那就正好匹配。如果多了层级目录,比如tiles/amap/14/xxx/yyy.png,就需要把amap这一级去掉,或者修改URL模板。统计瓦片总数也能帮你估算发布包体积,通常10万张PNG大约有2GB左右。
打包时可以按层级分包,也可以统一用一个可执行的分卷工具。我一般会把瓦片目录用tar归档,然后保留一份原片目录作为备份。如果交付的是部署包,建议移除调试用的高倍层级,只在maxZoom里保留实际用到的层级,这样能显著减小体积。
5.3 与Leaflet URL模板对齐:命名规则与边界修正
最常犯的错误是下载工具导出的命名规则不是标准的z/x/y。有些工具导出为z_14_x_1234_y_5678.png,也就是单层目录下平铺所有瓦片,这种情况下Leaflet默认模板是拿不到瓦片的。解决办法是写一个脚本或者用工具内置的"按规格重命名"功能转为标准目录。
另一种常见情况是下载工具导出时带了_后缀,比如14/123/456.png?version=1,这会导致Leaflet请求失败。你应该让下载工具输出纯文件名。对齐URL模板后,还要检查边界瓦片是否完整。很多下载器对范围边缘的瓦片会留白或半截,表现是地图边缘出现碎块。解决方法是下载时向外扩2个瓦片,再在Leaflet里用bounds设置瓦片显示范围,把多余边缘裁掉。
L.tileLayer('tiles/{z}/{x}/{y}.png', { minZoom: 3, maxZoom: 17, bounds: [[30.40, 114.10], [30.60, 114.50]] // 只显示这个矩形范围 }).addTo(map);bounds可以用一个西南角、东北角数组限制显示范围。这样即使瓦片目录里有多余瓦片,也不会污染视觉。但要注意,maxZoom层级对应的瓦片必须包含bounds范围内所有瓦片,否则缩放到最大层级时仍会出现灰块。
5.4 部署到nginx并配置缓存策略
瓦片部署到nginx时,最简单的配置就是把tiles目录作为静态资源根目录下的子目录。以下是适合离线地图的nginx配置片段:
server { listen 80; server_name localhost; root /data/map-demo; index index.html; location /tiles/ { add_header Cache-Control "public, max-age=86400"; sendfile on; tcp_nopush on; } }location /tiles/里设置的Cache-Control是给瓦片用的。如果要频繁更新瓦片,把max-age改小成3600;如果瓦片基本不变,可以开到7天。sendfile和tcp_nopush是静态文件优化项,能减少内核态到用户态拷贝次数。部署完成后,访问http://localhost/tiles/14/1234/567.png,看是否正确返回图片。这一步可以放到浏览器地址栏直接验证,如果404,说明目录结构与URL模板不一致,需要回到5.2节检查。
6. 让demo更适合生产:多底图切换、性能优化与验证手段
6.1 多底图切换与透明度叠加
demo集合里多底图案例一般用L.control.layers实现。离线场景下,可以把卫星图、路网图放在同一组,通过半透明叠加做比对。前提是两组瓦片覆盖同一坐标系和相同层级,否则叠上去完全错位。透明度可以在L.tileLayer的options里设置opacity,比如0.5。带透明度叠加时,建议关闭瓦片渐变动画,否则拖拽时会有掉帧感。
6.2 性能优化手段
瓦片数量大时,首屏加载慢。常见做法:限制初始层级,用minZoom降低请求数量;使用L.tileLayer的updateWhenIdle选项,让连续拖动结束后再刷新瓦片;对于边缘瓦片,用keepBuffer减少跳动。另外,把瓦片服务放在nginx并开启sendfile,对PNG来讲效果比gzip更大,因为PNG再压增益有限。
6.3 验证整体可用的自检清单
我每次交付前都用一套固定清单验证:
| 验证点 | 期望结果 | 排查方向 |
|---|---|---|
| 初始3个层级瓦片加载 | 无404,底图完整 | 路径模板和目录层级 |
| 覆盖物坐标与底图重合 | 偏差小于5米 | 投影坐标统一 |
| 5秒钟内首屏出图 | 延迟低于预期 | 初始缩放级别、瓦片体积 |
| 缩放层级切换连续 | 无长时间灰块 | maxZoom设置、瓦片缺失 |
| 断网后刷新页面 | 全部功能可用 | 是否还有CDN依赖 |
这套清单跑完,基本就敢把demo代码合入业务项目了。在那之后我每次部署离线地图,都强制先跑一遍瓦片路径与URL模板的匹配检查,再验证断网可用性,这两步帮我把很多低级问题挡在上线之前。希望帮到你。
本文还有配套的精品资源,点击获取