ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

useTileCache:前端GIS瓦片缓存工具,解决地图加载慢与离线难题

useTileCache:前端GIS瓦片缓存工具,解决地图加载慢与离线难题 做地图应用踩过最深的坑不是坐标系搞错而是瓦片加载慢得离谱。之前接了一个带离线巡检需求的项目测试环境一切正常到了用户现场就白屏。打开服务器日志一看同一个区域的瓦片用户在十分钟内反复请求了三次每次几百张流量和响应时间双双失控。后来我在项目里引入了 useTileCache 瓦片缓存工具这个卡点和流量问题才真正解决。简单说useTileCache 是一个面向 Web GIS 的前端瓦片缓存工具核心工作是把地图瓦片tile持久化到浏览器本地第二次进入页面时直接从本地读取不再向瓦片服务器重复请求。它适合用过 Leaflet、OpenLayers、Mapbox 这类前端地图库的项目尤其适合弱网环境、离线巡检、内网 GIS、以及被瓦片带宽卡脖子的地图应用。这篇文章我会从原理、接入、参数、离线落地和排查几个方面展开尽量把实际操作中容易踩的坑一次讲清楚。1. 为什么需要瓦片缓存先算一笔请求账1.1 瓦片加载的真实成本地图瓦片按金字塔模型切分第 z 级别的全球瓦片数量是 4^z。第 1 级 4 张第 10 级约 100 万张到第 17 级直接飙到 170 亿张。虽然正常用户不会看全全球但浏览一个城市的常用缩放级别动辄就是几百上千张瓦片。你打开一个 1440x900 的屏幕tileSize 为 256 像素时同屏大约需要 7x5 共 35 张瓦片。拖动一下地图、缩放一下级别又是几十张新瓦片。如果瓦片服务没有合理的缓存头每次操作都变成真实请求一分钟之内同一个瓦片可能被请求两三次。服务器压力大还是小事用户端白屏、卡顿、耗流量才是致命体验问题。1.2 浏览器自带缓存为什么不够用有人会说浏览器不是有 HTTP 缓存吗确实有但瓦片服务普遍没有配置足够长的 Cache-Control很多 WMS 服务甚至禁用缓存。即使配置了浏览器内存缓存和磁盘缓存也是受限的会被其他资源挤掉刷新页面还可能丢。localStorage 呢容量限制在 5MB 左右存 base64 字符串还会膨胀 33%读取又是同步的瓦片多的时候直接卡死主线程。这就是 useTileCache 这类瓦片缓存工具存在的价值用一个专门的持久化本地数据库来接管瓦片不再依赖浏览器碰运气的 HTTP 缓存。它选择的是 IndexedDB容量大、支持存二进制 Blob、读取是非异步的瓦片这种“按需加载、重复读取、量大但单个体积小”的场景几乎是为它量身定做的。1.3 工具介入后的效果对比我在项目中做了简单的数据对比。接入 useTileCache 之前反复浏览一张城市地图10 分钟内瓦片请求数在 1200 次左右请求流量大约 45MB接入之后只有第一轮访问有网络请求后续拖动、缩放、刷新页面命中缓存的瓦片完全不消耗流量服务器日志里同样的 URL 不再反复出现。这个差距在弱网下尤其明显。用户现场网络不稳定没缓存时一拖动就白屏有了缓存后已经看过的区域秒开只有新区域才需要等网络。对离线巡检的场景来说这就是能不能用的区别。2. useTileCache 的核心功能与底层设计2.1 缓存命中流程拦截、查询、回源、落库理解工具的工作原理比直接抄 API 更重要。useTileCache 的工作流程大致分四步第一步地图框架如 Leaflet 的 TileLayer准备加载某个瓦片时工具会先拿到这个瓦片完整的 URL经过 normalizeURL 处理生成缓存 key。第二步用这个 key 去 IndexedDB 里查询如果命中直接把 Blob 转成 ObjectURL 给图片元素用整个过程不发网络请求。第三步如果没有命中走正常的网络加载通过 fetch 拿到瓦片二进制数据。第四步把拿到的 Blob 写入 IndexedDB再展示给用户。从用户视角看第一次打开某个区域会有一点网络延迟但是看过的区域第二次打开几乎零等待。有一个细节值得注意第二步和第三步的时序是有讲究的必须等查询结果出来后才能决定是否发请求不能先发请求再判断否则缓存就形同虚设。2.2 存储层为什么选 IndexedDB 而不是 Cache API前端存储有几种方案localStorage 前面说过容量和同步读取是硬伤直接排除。还有一类浏览器原生能力叫做 Cache Storage API配合 Service Worker 可以做离线缓存听起来很合适但实际落地有几个问题。Cache API 更偏向静态资源的缓存设计上不会给你精细的 LRU 淘汰、有效期管理、容量统计这些能力你得自己在 Service Worker 里写一堆逻辑。而且 Service Worker 的更新策略、跨域支持、页面生命周期管理都是额外的心智负担。useTileCache 选择 IndexedDB就是为了把淘汰策略、有效期、配额这些控制权完全暴露给开发者。IndexedDB 存瓦片的官方思路是每个瓦片作为一条记录主键是处理后的 URL值是一个包含 Blob、时间戳、瓦片层级、行列号的对象。Blob 不用 base64 编码存的就是原始二进制数据读取的时候可以直接通过 URL.createObjectURL 生成图片地址性能非常好。其实这类工具用起来最顺手的地方在于它不需要你理解太深的浏览器存储原理只需要知道数据库里存了什么、什么时候会淘汰、怎么更新版本就够了。2.3 淘汰策略不只是 LRU 那么简单缓存不可能无限增长IndexedDB 虽大也有配额限制。常见的淘汰策略是按 LRU最近最少使用删除每次写入时检查总量超出 maxSize 后从最久没被访问的瓦片开始删。但真实的地图场景里纯 LRU 是不够的得加一个层级保护。稍微想一下就明白低层级如第 1-7 级的瓦片数量少、体积小但是覆盖面极大一张瓦片代表一大片区域删掉后用户缩放到全国级视角时又是一堆网络请求高层级的瓦片数量多、每张覆盖面积小用户在几个城市间来回切换时旧城市的瓦片删了也无所谓。合理的策略应该是优先淘汰高层级、且长时间未使用的瓦片低层级和当前中心区域附近的瓦片尽量保留。这也是我推荐使用成熟工具而不是自己写的原因之一。自己写一个简单的 Mapurl, blob 很容易但要把层级保护、容量统计、异步删除、失败重试这些细节都做对工作量远超预期。3. 五分钟接入初始化与核心参数3.1 安装与引入方式useTileCache 提供了常规的 npm 包也可以直接在页面中通过 script 标签引入。npm 方式npm install usetilecache然后在代码里引入import { TileCache } from usetilecache;如果你的项目没有构建工具直接从 CDN 引入 dist 目录下的 UMD 文件挂在 window.TileCache 上使用。我个人建议用 npm 方式树摇优化和版本锁定都更方便。3.2 Leaflet 项目的最小接入示例下面是 Leaflet 项目里最常见的接入方式完整保留了缓存逻辑。下面的示例以 v2.x API 为准如果你用的版本接口不同可以对照官方文档做对应调整。import L from leaflet; import { TileCache } from usetilecache; const tileCache new TileCache({ dbName: my-map-cache, maxSize: 500 * 1024 * 1024, // 500MB maxAge: 30 * 24 * 60 * 60 * 1000, // 30天 concurrency: 6, crossOrigin: anonymous, }); const CachedTileLayer L.TileLayer.extend({ createTile(coords, done) { const url this.getTileUrl(coords); const tile document.createElement(img); tileCache.match(url).then((blob) { if (blob) { tile.src URL.createObjectURL(blob); done(null, tile); } else { tileCache.fetchAndCache(url).then((cachedBlob) { tile.src URL.createObjectURL(cachedBlob); done(null, tile); }).catch(() { tile.src url; done(null, tile); }); } }); return tile; }, }); const layer new CachedTileLayer(https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png); map.addLayer(layer);这段代码的核心就一句优先从缓存拿数据拿不到再走网络。注意 catch 分支里我用的兜底逻辑是直接给 img 设原始 URL这样即使 fetch 遇到问题图片元素本身还是能用原生方式加载用户不至于看到一片空白。如果你不想自己扩展 TileLayeruseTileCache 也提供一个封装好的 adapter类似tileCache.createLayer(templateUrl, options)直接返回一个已经接入缓存的图层对象const layer tileCache.createLayer( https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png, { attribution: ... } ); map.addLayer(layer);如果你用的是 OpenLayers原理类似通过 source.setTileLoadFunction 把替换逻辑装进去就行。只要理解了“先用本地 blob再走网络”这两个分支任何地图库都能接进去。3.3 核心参数速查表参数默认值作用建议dbNameusetilecacheIndexedDB 数据库名多项目隔离每个应用单独设置version1数据库版本号结构变更时升级并清库升级 schema 时 1maxSize300MB缓存最大容量按目标和平均瓦片体积估算maxAge7天瓦片过期时间按瓦片更新频率设置concurrency4并发瓦片请求数常规 4-6预下载可到 10crossOrigin无瓦片加载跨域属性tile 服务支持 CORS 时设置normalizeURL无净化 URL生成稳定缓存 key必须处理带 token 的地址whiteList无只允许缓存的域名/前缀多源地图时建议配置blackList无禁止缓存的域名/前缀动态接口地址建议排除3.4 初始化时机和生命周期管理初始化工具本身是异步打开数据库的所以要在地图加载前先执行一次tileCache.init()否则第一次匹配请求时可能数据库还没打开导致吞掉首个瓦片请求。实际项目里我会这样处理await tileCache.init(); map.addLayer(layer);如果是纯页面不涉及动态初始化放在应用启动流程里完成等 init 后再初始化地图是最稳的。初始化之后如果想清空缓存调用tileCache.clear()会删除整个数据库再重建。这个方法在做“退出登录”“切换项目”时很有用。还有一个容易被忽略的点每当调试新功能、改了存储结构之后旧缓存里的数据格式不兼容会出现读取异常但页面不报错、就是瓦片不显示的情况。此时手动把 version 参数加 1工具会自动创建新数据库并清掉旧数据避免脏数据问题。4. 缓存策略与参数配置怎么选才不踩坑4.1 保质期瓦片更新频率决定一切maxAge 是最需要谨慎设置的参数。瓦片是有时效性的但不同类型的瓦片时效差别很大。影像底图如卫星图可能一年更新一次甚至几年不更新maxAge 设成 365 天或更长都没问题。道路路网图、POI 图层这种高频更新的数据一个月内就可能变化设 7-15 天比较合理。如果是实时路况、气象雷达这种分钟级更新的数据根本不适合缓存应该放进 blackList 里禁止缓存。我遇到过一个翻车场景把某个动态叠加层也默认缓存了结果用户打开页面看到的是三小时前的雷达回波图差点出大事。所以我在项目里会把所有调用参数的图层都单独处理要么不缓存要么用很短的有效期尽量不给业务留隐患。4.2 缓存配额先估算目标瓦片量maxSize 设得太小缓存刚存下就被淘汰等于白搭设得太大浏览器配额不够时会写入失败。比较好的方式是先估算你希望覆盖多少常用区域。单张瓦片体积可以实际测一下线划图 256x256 的 PNG 通常是 30-100KB影像瓦片 512x512 的 JPEG 通常是 80-300KB。如果你希望缓存 5000 张平均 120KB 的瓦片大约需要 600MB那 maxSize 可以初设 600MB。这不是拍脑袋而是有公式的建议 maxSize 预期覆盖瓦片数 × 平均单张瓦片大小再乘 1.2 的余量系数。我实际项目里用的 500MB能够覆盖一个中等城市主城区在常用缩放级别下的日常浏览体验和容量达到了平衡。如果你主要跑在低端手机上建议把配额降到 300MB 以下避免存储占用过大拖慢整个应用。4.3 并发数不要一上来就拉满concurrency 参数控制的是同时发起的网络请求数量。它解决的核心问题是如何避免瓦片风暴地图快速拖动时一屏需要几十张瓦片如果全部并发请求服务器瞬间被打满而且浏览器也会出现连接排队、超时、甚至 ERR_NETWORK__CHUNKED。正常浏览场景并发 4-6 就足够。预下载大范围瓦片时可以临时调到 8-10但还是建议对自有服务器先做压测。之前有个项目用的第三方瓦片服务限流规则比较严格并发开大了直接返回 429 状态码瓦片反而加载不出来。后来把并发降到 3问题自然消失。4.4 URL 规范化缓存命中率的分水岭normalizeURL 参数是使用这个工具最容易踩坑的地方也是提升命中率最强的手段。它接收原始 URL返回一个稳定的缓存 key。带时间戳和签名的瓦片 URL每次请求都不一样导致缓存永远无法命中。解决办法是去掉变化参数new TileCache({ normalizeURL(url) { const u new URL(url); u.searchParams.delete(_t); u.searchParams.delete(time); u.searchParams.delete(tk); return u.toString(); }, });例如原始 URLhttps://tiles.example.com/{z}/{x}/{y}.png?_t1699999999缓存 keyhttps://tiles.example.com/{z}/{x}/{y}.png这样时间戳变化不会影响 key缓存才能正常工作。WMS 服务还有一类坑BBOX 参数的浮点值可能因为不同请求的精度抖动而不完全一致导致同一个区域被当成不同瓦片缓存。解决办法是在 normalizeURL 里把 BBOX 的坐标值取固定精度比如保留 6 位小数让同一个网格的瓦片 key 稳定下来。5. 离线场景与业务落地的几个关键问题5.1 离线巡检不能全量下载只能按区域预取很多业务需求看起来是“我要离线地图”实则要的是“我要在弱网/断网环境下看某个区域的底图和业务点”。想一次性把所有缩放级别全部下载是不现实的第 10 级全球瓦片已经上百万张18 级更是天文数字。合理的离线方案是预取指定区域、指定缩放级别的瓦片。useTileCache 提供了预下载方法可以配合中心点坐标、半径、缩放级别范围来规划范围。思路是从低等级到高等级逐层预取const task tileCache.preload({ center: [120.13, 30.26], minZoom: 10, maxZoom: 15, onProgress: (done, total) { console.log(已完成 ${done}/${total}); }, });这个任务建议放到用户空闲时进行比如进入页面后延迟 10 秒再启动避免和首屏瓦片抢占带宽。下载过程中把 didFail 的瓦片记录下来结束时统一补拉能提高预取完整率。5.2 动态服务WMS怎么缓存key 的构造是命门WMS 这类动态瓦片服务有一个特殊问题同一块区域如果在不同参数下请求返回的图片内容不一样但 URL 差别可能非常微妙。比如其中一个请求多了TRANSPARENTTRUE另一个多了STYLESdefault这两个 URL 会形成两条缓存其中一条存到的是透明底图另一条是带样式的地图功能上可能没问题但存了两份重复数据。更麻烦的是同一块物理区域如果 BBOX 的精度不一致瓦片内容可能含半个像素的偏移也会形成大量重复缓存。我在项目里的处理是在 normalizeURL 中把 BBOX 四个浮点值统一截断到 6 位小数并且对图层名、样式、透明度这些关键参数做排序拼接确保同一视觉内容的瓦片只存一份。参数太多时甚至可以直接在方法里把 URL 重新构造一遍只保留你真正关心的参数。5.3 带 Token 的加密瓦片地址缓存与鉴权的平衡现在很多商业瓦片服务会在 URL 里带 access_token 或 signature用于鉴权和防盗链。这种 URL 有一个特点token 会定期过期一天甚至几小时变一次。直接缓存原始 URL 的效果很差因为 token 变了key 就变了缓存几乎每次都会失效。我之前被这个问题坑过明明缓存工具工作正常但是命中率始终为 0排查了很久才发现是 key 里带着每天变化的 token。解决的方案是把 token 过滤掉用不带 token 的 URL 作为 key。需要注意这样会带来跨用户缓存污染的风险不同人的 token 虽然不会出现在 key 里但缓存的图片数据是一样的对一般 GIS 业务没有影响如果服务商对每个用户返回的图片不一致那就不能共享 key只能把 token 保留在 key 里接受低命中率。5.4 CORS 要不要开这是缓存工具的硬性前提。useTileCache 的 fetchAndCache 走的是 fetch APIfetch 受同源策略限制。很多瓦片服务器尤其是老旧的 WMS根本没有配置 CORS 响应头。这意味着 fetch 拿不到响应缓存写入必然失败。我遇到的典型报错是Access to fetch at ... has been blocked by CORS policy。解决办法有三个方向让自己服务器的接口做一层瓦片代理把瓦片 URL 变成同源地址或者要求瓦片服务商开启 Access-Control-Allow-Origin再或者把瓦片加载方式改回 Image 元素加载但那样就拿不到 Blob无法落库。所以结论是要用 useTileCache必须保证瓦片服务允许跨域。如果实在无法改服务端自己写一个简单的代理转发是最稳妥的选择。6. 常见问题与排查技巧实录6.1 现场问题速查表现象原因排查方向瓦片每次重复加载缓存命中率 0%URL 带随机参数导致 key 不稳检查 normalizeURL 是否删除 _t、token 等参数缓存写入正常但图片加载失败瓦片服务不支持 CORS打开控制台看 fetch 是否被 CORS 拦截地图区域显示空白但缓存正常图片 ObjectURL 被过早 revoke检查代码里 createObjectURL 和 revokeObjectURL 的顺序刷新后缓存还在重启浏览器后偶尔丢浏览器存储配额回收看 Application 面板 IndexedDB 大小考虑降 maxSize多项目共用一个数据库dbName 相同导致数据串了各项目设置独立的 dbName调试时禁用缓存没生效工具走 IndexedDB 不走 HTTP 缓存这是正常的不是 bug版本升级后旧数据报错存储结构变化version 参数 1强制重建数据库6.2 一次完整的排查过程说一个真实例子。一个同事接入工具后反馈无效我让他打开 DevTools 的 Network 面板勾选只显示“Img”请求然后拖动地图观察。发现瓦片 URL 每次都带_t1699999999这样的时间戳参数而且每次点开一条请求Request URL 都不同。我当时判断就是 normalizeURL 没配好缓存 key 不稳定。把时间戳去掉后第二次地图加载时Network 面板里完全看不到瓦片请求相关缓存命中正常。另一个常见坑出现在 DevTools 勾选“Disable cache”时。很多开发者以为这会禁用所有缓存实际上它只影响 HTTP 缓存而 IndexedDB 是应用层数据不受这个开关影响。所以调试时明明勾了禁用缓存瓦片还是秒开不要误以为是工具失效。6.3 清理工具改造成自己的调参习惯用了一段时间之后我养成了一个习惯每个项目初始化时都会写一段简短的诊断代码放一个隐藏开关点一下就能看到缓存的总共瓦片数、数据库体积、最近命中率。这不是 useTileCache 的标配能力但可以自己基于其提供的查询方法做个小面板对排查线上问题特别管用。最后分享一个我实际在用的配置模板影像底图 maxAge 设置 180 天以上路网图层设置 7 天动态业务图层直接进 blackList 不缓存生产环境 maxSize 设为 500MB测试环境设为 50MB瓦片服务如果支持跨域就开启 crossOrigin: anonymous不支持就加网关代理。接入工具不是终点把它当成地图工程的一部分长期调优才能真正摆脱白屏和流量焦虑。
返回列表