
简介这份 Leaflet 离线地图案例合集面向 GIS 开发人员与前端地图爱好者围绕网页端二维地图的离线集成、交互定制与性能调优提供系统参考。压缩包共 723 个文件约 14.08MB以 js 脚本、html 页面、png 图示和 md 说明为主并辅以 css、hbs 模板及少量配置与文档文件便于按示例查阅和二次改造。内容覆盖基本地图加载、图层管理、标记与弹出框、控件使用、拖拽缩放等交互、地理编码、路径与多边形绘制、地图事件监听、自定义图层、地图投影、性能优化和响应式设计等十余类场景每个 Demo 对应独立学习点。已有 4150 人学习适合希望从示例出发掌握 Leaflet 核心能力、并迁移到 GIS、导航或房地产地图项目中的读者。1. 离线地图的真实需求一套能直接跑的 Leaflet 示例集合某公司的资产巡检系统上线前一周测试环境里的地图整片灰白——外网瓦片域名全被内网防火墙挡掉了。需求变得很直接地图数据必须跟系统一起打包不能依赖任何外部请求。Leaflet 这个库本身很小BSD 协议插件生态也成熟问题从来不在“要不要用”而在“离线这一层怎么补齐”。一份完整的 leaflet demo 集合价值就在这里它把 gis 场景下最常见的几件事——瓦片加载、标注、图层控制、坐标处理——拆成一个个能直接跑的例子照着改就能拼进 web 项目。这套东西适合两类人正在做毕设或课设、需要交付内网可运行地图模块的学生以及要把离线地图集成进后台系统的前端开发。2. 技术选型与总体架构Leaflet 内核加离线瓦片的三层分工2.1 为什么是 Leaflet而不是整套 GIS 全家桶离线地图能选的路线大致三条。OpenLayers 功能最全但 API 抽象层多打包体积大一个普通二维业务项目为了画几个标记去啃整套投影体系投入产出比并不划算Cesium 面向三维没有三维需求就是杀鸡用牛刀剩下的就是 Leaflet内核压缩后几十 KBAPI 就是一层一层 Layer 往上叠文档读一遍能上手配上少量插件后覆盖绝大多数 web 端二维地图场景。选择 Leaflet 之后要清楚一件容易被忽略的事Leaflet 自己不带任何地图数据。它只负责渲染视图、处理缩放和事件瓦片从哪来得由你指定。在线场景下 L.tileLayer 填一个公共瓦片地址就行离线场景则要求瓦片躺在本地目录或者本地服务里。所以真正决定项目能不能跑起来的从来不是 Leaflet 本身而是瓦片源这一层准备得够不够扎实。这也是为什么一套按功能拆开的 demo 集合比单篇教程好用。教程通常只讲“怎么写一个 Marker”但真实项目里瓦片路径、层级范围、坐标一致性这些问题一旦有一处对不上页面就是一整块灰白。demo 集合的作用是把每个环节单独拆出来先让每个零件能独立转再谈拼装。2.2 三层架构瓦片源、地图内核、业务图层把这套方案摊开是三层结构每层职责清晰改一层不会牵动另一层。层级核心职责典型产物改动频率瓦片源层提供可访问的瓦片地址模板本地瓦片目录 / MBTiles / 切片服务低数据更新时才动地图内核层管理视图、缩放层级、坐标系与事件L.map 实例、基础配置中交互需求变化时动业务图层层承载业务数据与交互Marker、GeoJSON、绘制图层高业务迭代就跑这层瓦片源层是地基。它对外只暴露一个模板比如./tiles/{z}/{x}/{y}.pngLeaflet 拿到模板后按当前视图换算出需要请求的瓦片编号。这一层的关键是“命名规则必须和 Leaflet 的换算逻辑一致”XYZ 和 TMS 两种命名差一个 y 轴翻转搞混了就是整屏错位。地图内核层负责视图状态。中心点、初始层级、最小最大缩放、是否允许跨经度循环这些参数决定了用户第一次打开地图看到什么。离线场景下有个参数特别容易被漏——noWrap默认允许跨经度循环本地瓦片只有一份时往东拖过边界会出现同一套瓦片重复铺开的怪象。业务图层层是真正体现业务价值的地方。巡检点、区域边界、热力分布这些数据都挂在这一层每个图层都可以独立开关、独立绑定事件。把这三层分开之后后面无论是换瓦片格式还是加业务标注都不会牵一发而动全身。2.3 目录结构与依赖清单一套能直接跑起来的 demo 集合目录最好按“公共资源 单例演示”来组织避免每个例子复制一份 Leaflet 造成版本漂移。leaflet-offline-demos/ ├── lib/ │ ├── leaflet/ # 统一入口的 Leaflet 内核与样式 │ └── plugins/ # 按需引入的绘制、聚合等插件 ├── tiles/ # 离线瓦片按 {z}/{x}/{y}.png 存放 ├── data/ # GeoJSON、业务点位等静态数据 ├── img/ # 图标、标记图片 ├── demos/ │ ├── 01-basic-map/ # 最小可运行地图 │ ├── 02-marker-popup/ # 标注与弹窗 │ ├── 03-layer-control/ # 底图与业务图层切换 │ ├── 04-geojson/ # 矢量要素加载 │ └── 05-events/ # 交互事件与坐标拾取 └── index.html # 演示索引页依赖清单保持在最小集Leaflet 内核、一个负责图层控制的官方控件内核已自带、按需的绘制插件、按需的点聚合插件。这里有一条血泪经验——插件版本必须跟 Leaflet 主版本对齐写这篇文章时 Leaflet 主流是 1.x 系列很多老插件还停在 0.7 时代混用会在控制台抛出“L 未定义某方法”的报错排查起来像玄学。统一放在lib/下集中管理是让 demo 集合长期可维护的前提。提示每个 demo 只引公共目录下的 lib不要在单例目录里再复制一份 Leaflet否则改一处版本要改十处。3. 离线瓦片的生产与加载从切片工具到地图渲染3.1 瓦片切片层级、命名规则与存储格式离线瓦片不是拿一张大图切九宫格那么简单它需要按照 Web 墨卡托投影切成金字塔结构每一层zoom把上一层切成四块。常用的切片工具有 gdal2tiles、QGIS 的切片插件、以及一些在线切图工具命令行里最省事的还是 gdal2tiles。# 把一份影像或矢量渲染结果切成 XYZ 瓦片 # -z 指定层级范围-p 指定投影--xyz 让输出目录结构与 Leaflet 默认对齐 gdal2tiles.py -z 6-15 -p mercator --xyz ./source.tif ./tiles几个参数值得单独说明。-z 6-15是层级范围层级越多瓦片数量指数级增长一个中等城市范围到 15 级大概是几万张图打包前要想清楚项目实际需要的最大缩放-p mercator必须是 Web 墨卡托因为 Leaflet 默认就在这个投影下工作换成别的投影后地图放上去会整体拉伸错位--xyz最关键它让输出目录变成tiles/{z}/{x}/{y}.png这种顺序跟 Leaflet 的换算规则一致不加这个参数输出会变成 TMS 顺序y 轴从下往上数直接拿过来用就是上下颠倒。切片完成后建议先做一次完整性检查——随手抽几个层级看目录里 x 和 y 的编号范围是否连续有没有空洞。空洞通常出现在源数据的边缘区域如果源图覆盖范围不够某些瓦片就是空的前端加载时会看到局部空白容易被误判成代码问题。3.2 用 L.tileLayer 加载本地瓦片瓦片准备好之后Leaflet 这一侧其实只有一行核心代码。// 初始化地图实例所有参数都围绕“离线”这两个字来定 const map L.map(map, { center: [30.0, 120.0], // 初始中心点按实际业务范围替换 zoom: 10, // 初始层级落在切片层级范围内 minZoom: 6, // 不能小于切片的最小层级 maxZoom: 15, // 不能大于切片的最大层级 attributionControl: false // 离线场景一般不需要版权信息控件 }); // 指向本地瓦片目录模板必须与切片输出结构完全一致 L.tileLayer(./tiles/{z}/{x}/{y}.png, { minZoom: 6, maxZoom: 15, tms: false, // XYZ 命名用 falseTMS 命名才需要 true noWrap: true, // 关键禁止跨经度循环避免边界重复瓦片 keepBuffer: 4 // 预加载周边瓦片拖动时减少白块 }).addTo(map);minZoom和maxZoom两处要跟切片范围严格对齐。如果地图允许用户缩到 5 级但切片只有 6 级起5 级视图会是一整片空白反过来如果允许放大到 18 级而切片只到 15 级Leaflet 会把 15 级的瓦片强行放大填充画面糊成一片用户第一反应就是“地图坏了”。noWrap值得再强调一次离线项目里这是最容易翻车的参数之一不设它用户往东多拖两屏整片地图就会开始重复出现看上去像是数据错了其实是循环渲染。注意keepBuffer设得越大越平滑但内存占用也越高。移动端建议 2 到 3桌面端 4 左右比较稳。3.3 三种离线存储方案的对比瓦片放哪里决定了这套方案好不好部署、好不好扩展。常见的三种存法对比如下。方案部署难度数据量适应性查询能力适用场景本地目录 静态服务低一个 HTTP 服务即可小到中等无纯文件读取课设、演示、单机部署MBTiles 单文件中需服务端解析大支持按层级查询数据包要整体分发本地切片服务高需额外程序大支持动态切片多项目共用底图本地目录方案最简单把tiles目录扔到任意静态服务器后面就能用缺点是文件数多几万个小文件拷贝和部署都慢。MBTiles 把瓦片塞进一个 SQLite 文件分发时只有一个文件体积也小代价是服务端要能读 SQLite 并按{z}/{x}/{y}查出来再吐给前端查询逻辑大致是这样。-- MBTiles 标准结构里瓦片按 zoom_level / tile_column / tile_row 存储 -- 注意 MBTiles 内部用的是 TMS 行号对外提供 XYZ 时要翻转 y SELECT tile_data FROM tiles WHERE zoom_level ? AND tile_column ? AND tile_row (1 ?) - 1 - ?;最后那句(1 zoom) - 1 - y就是 TMS 行号到 XYZ 行号的翻转公式少了它所有瓦片都会上下颠倒。切片服务方案适合底图被多个项目复用的情况瓦片按需生成并缓存代价是引入了一个额外服务对毕设这种单机交付的场景并不划算。4. 示例集合拆解标注、图层控制与空间查询的落地写法4.1 标注与弹窗Marker、Popup 与自定义图标标注是地图上最基础也最常被改动的元素。默认的蓝色水滴图标几乎每个项目都会换掉换成业务图标这里有两个参数必须调。// 自定义图标iconSize 是图片尺寸iconAnchor 是锚点对准坐标的位置 const pinIcon L.icon({ iconUrl: ./img/pin.png, iconSize: [32, 40], iconAnchor: [16, 40], // 图片底边中点对准坐标图标才不会“飘”在点上方 popupAnchor: [0, -36] // 弹窗从图标顶部弹出 }); // 创建标注并绑定弹窗 const marker L.marker([30.0, 120.0], { icon: pinIcon }).addTo(map); marker.bindPopup(b巡检点 A/bbr状态正常br时间今日 09:20); // 批量标注用 FeatureGroup 统一管理方便后面一次性显示/隐藏 const poiGroup L.featureGroup([marker]).addTo(map);iconAnchor是最容易踩的一个点。不设它Leaflet 会默认把图标中心对准坐标结果就是图标的下半部分戳进了坐标点里视觉上整体偏移几十像素。设成[16, 40]表示图标宽度一半、高度全高正好让图片底边中点落在坐标上。批量场景下用L.featureGroup包一层比直接addTo到地图上更好管理后面要做“显示全部巡检点”这种开关一个addLayer/removeLayer就搞定。4.2 图层切换底图切换与业务图层开关底图和业务图层分开控制是让界面保持清爽的关键。Leaflet 自带的图层控件两行就能配好。// 底图集合至少准备一套离线底图另一套可以是简版灰度底图 const baseLayers { 离线底图: offlineLayer, 灰度底图: grayLayer }; // 业务图层集合每个开关对应一个 featureGroup const overlays { 巡检点位: poiGroup, 区域边界: zoneGroup, 热力分布: heatGroup }; // 右上角挂图层控件collapsed 控制默认是否收起 L.control.layers(baseLayers, overlays, { position: topright, collapsed: true }).addTo(map);底底层和业务图层的区别在于“同一时刻只能显示一张底图”业务图层则可以任意叠加。写的时候要保证每个业务图层都对应一个独立featureGroup不要把所有东西都塞进地图实例里否则控件拿不到切换句柄。图层顺序也有讲究后加的图层在上层弹窗和标注要放在区域边界之后加不然会被面状要素盖住点不中。4.3 空间数据加载GeoJSON 与矢量要素渲染业务数据最常以 GeoJSON 交付Leaflet 对它的支持是一等公民级别的。// 异步加载区域数据并渲染style 控制面样式onEachFeature 挂交互 fetch(./data/zones.geojson) .then(res res.json()) .then(data { L.geoJSON(data, { // 根据要素属性动态决定填充色比写死一个颜色灵活 style: feature ({ color: #0b6, weight: 2, fillOpacity: feature.properties.level 2 ? 0.35 : 0.15 }), // 每个要素加载完都可以单独绑定弹窗 onEachFeature: (feature, layer) { layer.bindPopup(区域${feature.properties.name}); } }).addTo(zoneGroup); });style函数接收每个要素可以按属性决定颜色、透明度、线宽这比一次性设置统一样式实用得多。onEachFeature则是挂事件、绑弹窗的统一入口注意回调里的layer是当前要素图层不是整个 GeoJSON 图层作用域搞错就会让所有要素显示同一个弹窗内容。加载时机上建议等地图load事件之后再拉数据不然首屏要同时下瓦片和 GeoJSON弱网环境下容易卡一下。4.4 交互事件点击、缩放与坐标拾取地图上的交互事件都挂在 map 实例或具体图层上坐标拾取是最常见的一个需求。// 点击地图任意位置把经纬度写到页面某个元素里 map.on(click, e { const { lng, lat } e.latlng; document.getElementById(coord).textContent 经度 ${lng.toFixed(6)}纬度 ${lat.toFixed(6)}; }); // 缩放事件里读取当前层级常见用途是按层级加载不同精度的数据 map.on(zoomend, () { const currentZoom map.getZoom(); // 低层级只显示区域中心高层级才展开点位 poiGroup.setZIndex(currentZoom 12 ? 500 : 100); });事件回调里的e.latlng是已经做过分层换算的坐标不需要自己再乘缩放系数。要注意的是批量标注场景下点击标注会同时触发地图的 click如果业务里禁止点击标注时还记录坐标需要在 marker 上再挂一个click事件阻止冒泡否则每次点标注都会多算一个坐标点。5. 常见问题与排查五类最容易翻车的现场5.1 整屏灰白或瓦片 404现象页面打开后地图区域一直灰白打开开发者工具满屏404 (Not Found)。原因绝大多数是瓦片模板和实际目录对不上。要么路径多了或少了一层目录要么tms参数跟实际命名规则相反要么静态服务把.png当成未知类型拒绝返回。解决先手动把浏览器地址栏改成http://本地地址/tiles/10/512/340.png这种具体路径能打开就说明目录没错问题在模板打不开就查静态服务的根目录是否指到了tiles的上一层。命名规则是 XYZ 就把tms设成false是 TMS 就设true这条确认之后 404 基本会消失。5.2 瓦片错位与层级不连续现象地图能显示但相邻瓦片之间出现明显缝隙或者整体像被平移了几十像素。原因通常来自两处。一是切片时投影不是 Web 墨卡托比例尺跟 Leaflet 的换算不匹配二是瓦片输出不是标准的 256×256 尺寸而是 512×512 但没做对应配置。解决切片命令里补上-p mercator重新切一次。如果是瓦片尺寸问题在L.tileLayer里加tileSize: 512同时把zoomOffset: -1加上让层级换算重新对齐。这类问题翻车之后最难查因为进度条正常、瓦片数量也对就是位置歪了。5.3 坐标系偏移现象标注点落在路上而不是建筑上或者整体偏移固定的一两百米。原因业务数据用的坐标系与底图不一致。底图在 Web 墨卡托下不做额外偏移而部分业务数据来源于其他坐标体系两者叠在一起就会出现固定方向的偏移。解决先确认底图和业务数据是否在同一套坐标下。不一致时应该在数据入库之前就统一转换不要指望前端在渲染时补偏移那会让后续所有空间查询跟着歪。转换脚本单独放一份转换前后各抽三个点人工核对是性价比最高的检查方式。5.4 移动端手势冲突现象在手机或平板上地图一拖动整页跟着上下滚或者双指缩放没反应。原因地图容器没有阻止触摸事件冒泡浏览器的默认滚动行为把拖拽手势吃掉了。解决给地图容器加touch-action: none同时确保地图初始化时没有禁用拖拽。如果页面本身需要滚动就把地图嵌在一个固定高度的容器里让手势从地图上开始时不触发页面滚动。这一条在课设演示用笔记本时完全看不出来一上手机就露馅。5.5 大瓦片包首屏卡顿现象加载十几万张瓦片的大包首屏要等五六秒才出图拖动时大面积白块。原因瓦片文件数太多静态服务的并发和磁盘寻道都扛不住同时前端默认只预加载当前视图附近少量瓦片。解决文件多的场景改成 MBTiles 单文件方案服务端只读一个文件随机读性能稳定。前端侧把keepBuffer调到 3 到 4配合updateWhenIdle: false让拖动过程中就发起请求不要等停下才请求。首屏可以加一个加载遮罩等tileload事件计数达到阈值再撤掉视觉上会顺畅很多。6. 验收与答辩怎么证明离线地图真的跑通了地图类项目最容易出现的验收翻车是“在自己电脑上一切正常换个环境全白”。所以验收不能只看页面能不能出来得按一组明确的检查项走一遍把证明过程留成记录。我一般会用下面这张表作为交付前的最后一道关卡每一项都要有可复现的操作路径。检查项操作方法通过标准完全断网可运行拔掉网线或关掉无线只留本地静态服务地图、标注、弹窗全部正常控制台无外网请求层级边界从最小层级缩到最大层级再缩回无空白、无拉伸模糊、无超出范围依旧请求边界拖动向东、向南拖动超过数据覆盖范围不出现重复瓦片边界处显示为空白而非错乱标注位置点击三个已知识别点核对经纬度坐标与数据源误差在可接受阈值内移动端在手机浏览器打开做拖拽与双指缩放地图滚动与页面滚动互不干扰首屏性能清缓存后强刷记录首图时间达到项目约定阈值一般 3 秒内答辩场景下光有表格不够还得让评审看到“断网仍是活的”。最直接的方式是现场把网络断开再刷新页面地图能起来、点得动、弹窗能打开这三件事做完评委基本不会再纠结技术细节。反过来如果当场需要连网才显示那前面讲的所有架构都不成立。还有一个技巧值得提前做给每个 demo 单独准备一个极短的操作路径说明比如“打开 03-layer-control点右上角图层按钮切换灰度底图观察巡检点位是否保持可见”。答辩时间有限评审不会自己探索你把三步路径写清楚对方照做整套方案的完整度立刻不一样。从那以后我每次准备地图类交付都会强制走一遍“断网 换设备 缩到底 拖出界”这四步哪怕时间再紧也不跳过——血泪经验是跳过哪一步翻车就出在哪一步。希望帮到你。本文还有配套的精品资源点击获取