前言
在 Web 地图应用中(例如基于 CesiumJS、mars‑3d 或其它 3D 地球/地图库),一个常见性能瓶颈是"底图/地形/瓦片/影像请求过多、网络延迟高、重复请求资源多"。
为了解决这个问题,可以将重要的地图请求结果缓存到浏览器端(使用 IndexedDB),下一次再请求相同 URL 时,优先从缓存返回,从而减少网络请求、加快加载速度、改善用户体验。本文将演示如何使用 localforage(用于 IndexedDB 存储) + ajax‑hook(用于拦截 XHR/Fetch 请求)实现这一功能。
核心思路
-
拦截所有发出的 AJAX 请求(通过 ajax-hook)
-
对符合特定规则(例如天地图、ArcGIS 地形服务等)的请求,先检查浏览器缓存(IndexedDB)是否已有响应数据
-
如果缓存命中,则直接返回缓存数据,避免真实网络请求
-
如果缓存未命中,则让请求继续,响应回来后,再将结果写入缓存,以便下次使用
这样做的好处包括:
-
减少重复请求,减轻服务器和网络负担
-
缩短客户端加载时间,提升性能
-
优化弱网络或移动设备上的用户体验
在地图应用中,这种缓存尤其有意义,因为很多瓦片/地形/影像都是"可复用"的、被多次请求的资源。早在 Cesium 社区就有人讨论过将瓦片存入 IndexedDB 用作离线或加速方案。 Cesium Community+1
代码详解及意义
下面是完整代码(你已提供)并附上详细说明:
javascript
import localforage from 'localforage';
import { proxy } from 'ajax-hook';
const cache = localforage.createInstance({
name: 'mapRequestCache',
driver: localforage.INDEXEDDB,
});
const rules = [
/tianditu\.gov\.cn\/DataServer/,
/elevation3d\.arcgis\.com\/arcgis\/rest\/services\/WorldElevation3D\/Terrain3D\/ImageServer/
];
function isMatchRule(url) {
return rules.some((rule) => rule.test(url));
}
function getKey(url, method, version = 'v1') {
return `${version}-${url}-${method}`;
}
proxy({
onRequest: async (config, handler) => {
const url = config.url;
if (isMatchRule(url)) {
const key = getKey(url, config.method);
const data = await cache.getItem(key);
if (data) {
handler.resolve({
config,
status: 200,
response: data,
header: config.headers
});
return;
}
}
return handler.next(config);
},
onResponse: async (response, handler) => {
const url = response.config.url;
if (response.status === 200 && isMatchRule(url)) {
const key = getKey(response.config.url, response.config.method);
await cache.setItem(key, response.response);
}
return handler.next(response);
}
});
export default cache;
下面按模块逐一解释其意义与作用:
1. 引入 localforage 与 ajax-hook
javascript
import localforage from 'localforage';
import { proxy } from 'ajax-hook';
-
localforage:一个包装 IndexedDB、Web SQL 和 localStorage 的库,提供统一异步 API,用于在浏览器中存储较大数据(例如图片、瓦片、二进制 Blob)。 -
ajax-hook:一个用来拦截 XHR/Fetch 请求的库。它允许你在请求发出前(onRequest)和响应返回后(onResponse)插入逻辑,从而可实现缓存、修改请求/响应、阻止请求等。 GitHub
2. 创建缓存实例
javascript
const cache = localforage.createInstance({
name: 'mapRequestCache',
driver: localforage.INDEXEDDB,
});
-
这里创建了一个
localforage实例,命名为'mapRequestCache',并明确指定driver: localforage.INDEXEDDB,即使用浏览器的 IndexedDB 存储。 -
这意味着你的缓存数据将以键值对形式保存在 IndexedDB 中(比 localStorage 容量更大、性能更好)。
3. 定义要拦截/缓存的 URL 规则
javascript
const rules = [
/tianditu\.gov\.cn\/DataServer/,
/elevation3d\.arcgis\.com\/arcgis\/rest\/services\/WorldElevation3D\/Terrain3D\/ImageServer/
];
function isMatchRule(url) {
return rules.some((rule) => rule.test(url));
}
-
rules数组中定义了若干正则表达式,匹配你希望缓存的地图服务请求 URL(如 天地图 "tianditu.gov.cn/DataServer"、ArcGIS 地形服务 "elevation3d.arcgis.com/...Terrain3D/ImageServer")。 -
isMatchRule(url)函数用于判断一个 URL 是否匹配这些规则。如果匹配,则说明这是一个"值得缓存"的请求。 -
你可以根据自己的项目,扩展或修改这些规则(例如增加瓦片服务、3DTiles、贴图 URL 等)。
4. 生成缓存 Key
javascript
function getKey(url, method, version = 'v1') {
return `${version}-${url}-${method}`;
}
-
每个缓存条目需要一个唯一 key,以便存取。这里用
${version}-${url}-${method}来生成。 -
url:请求的 URL -
method:请求方法(如 GET、POST) -
version:可选版本号,用于在服务更新/格式变更时"失效"旧缓存。 -
使用版本号是一个好习惯:当底图服务、瓦片格式、API 版本发生变动时,通过改变
version值即可让 key 全部不同,从而避免使用过期缓存。
5. 拦截请求(onRequest)
javascript
onRequest: async (config, handler) => {
const url = config.url;
if (isMatchRule(url)) {
const key = getKey(url, config.method);
const data = await cache.getItem(key);
if (data) {
handler.resolve({
config,
status: 200,
response: data,
header: config.headers
});
return;
}
}
return handler.next(config);
}
意义说明:
-
在请求发送之前,这里检查
config.url是否匹配规则(即isMatchRule(url))。 -
如果匹配,则尝试从
cache.getItem(key)获取缓存数据。 -
如果缓存命中(即
data不为空),就 直接返回缓存结果 ,通过handler.resolve()模拟一个成功响应,从而跳过真实网络请求。 -
如果缓存未命中,则调用
handler.next(config),让请求继续发送。
这样就实现了"优先使用缓存"的逻辑。
6. 拦截响应(onResponse)
javascript
onResponse: async (response, handler) => {
const url = response.config.url;
if (response.status === 200 && isMatchRule(url)) {
const key = getKey(response.config.url, response.config.method);
await cache.setItem(key, response.response);
}
return handler.next(response);
}
意义说明:
-
当 真实网络请求返回后 ,首先检查
response.status === 200且url匹配规则。 -
如果满足,则生成缓存 key ,并将
response.response存入缓存。 -
然后调用
handler.next(response)继续处理响应(例如传递给业务逻辑)。
这样就保证了"第一次请求网络 → 写入缓存;以后相同请求 → 直接读缓存"的流程。
7. 导出缓存实例
javascript
export default cache;
-
导出
cache实例,方便在其它模块中对缓存进行管理/清理/查询。 -
比如你可能想在特定场景下清理缓存、查看缓存条目数、打印日志等。
使用步骤与实践建议
下面是你在地图项目中(例如使用 mars-3d/Cesium)建议的使用流程与注意事项:
-
在项目入口处初始化
在项目的最早期(如 main.js、入口 script 中)引入上述缓存模块,使其在页面加载初期即激活拦截器。
javascriptimport './mapRequestCache.js'; -
定义合适的规则(rules)
根据你的项目中所用的地图服务类型(瓦片、地形、影像、3DTiles、模型等)调整
rules正则。例如:javascript/.*tianditu\.com\/DataServer.*/, /.*tileserver\.yourdomain\.com\/tiles\/.*/, /.*3dtiles\.yourservice\.com\/tileset.json/, /.*\.glb$/,这样保证你缓存了对性能影响最大的资源。
-
考虑资源格式与缓存大小
-
场景中可能请求的是 图片瓦片 (PNG/JPG) 、**二进制 ArrayBuffer/Blob **、JSON/3DTiles json 。你需要确认
ajax-hook拦截时response.response是否包含你想缓存的数据(可能是字符串、二进制)。 -
如果资源体量很大(例如高分辨率瓦片、影像、大 3DTiles 包),缓存可能占用大量 IndexedDB 空间。你要考虑:缓存条目数控制 、缓存大小控制 、版本切换机制。
-
在某些场景,浏览器自身的 HTTP 缓存/服务工作者缓存也可能起作用。你要测试确保你的机制是真实起效。
-
-
测试缓存命中 + 加载速度提升
-
使用浏览器 DevTools → Network 面板,观察某个瓦片或地形请求第一次时有网络请求,第二次是否被缓存拦截(无网络请求或网络请求被替换为本地返回)。
-
检测对 mars-3d 场景加载速度是否有显著提升。
-
测试弱网络或手机设备情况,缓存机制通常对它们提升更明显。
-
-
处理版本或服务变动
-
当地图服务底图/瓦片格式/参数发生变更时,你要"让缓存失效",避免服务更新但客户端使用了旧缓存。可通过改变
version参数(如'v2')或清理旧 key 实现。 -
你还可以在缓存模块里加入清理逻辑:例如每次版本变动就
cache.clear()。
-
-
监控 IndexedDB 使用情况
-
定期查看浏览器 Application 面板 → IndexedDB → mapRequestCache 库,观察条目数、存储大小。
-
如果缓存数据过多,可以实现"最少使用淘汰(LRU)"逻辑、或定期清理旧条目。
-
注意事项与风险提示
-
并非所有请求都能被 ajax-hook 成功拦截。例如使用 img 标签、Canvas、WebGL 纹理加载、或者某些 fetch / Resource.fetch 机制可能绕过你的代理。你需要验证在你的地图库(例如 mars-3d)中实际资源加载方式。
-
如果地图服务严格依赖 CORS 、或返回 ArrayBuffer/Blob 数据,缓存读取与返回时可能需要做额外处理(如 Blob 转 ObjectURL、Base64 编码等)。
-
缓存如果使用不当可能导致"旧数据展示"或"风格/内容与服务器不一致"的问题。必须做好版本控制。
-
浏览器不同平台对于 IndexedDB 大小、性能支持不同。在容量较低或私有模式下可能受限。
-
使用第三方地图服务(如 天地图、ArcGIS Online)时,需确认其服务协议允许客户端缓存;部分服务可能限制缓存或离线使用。
总结
通过这段代码,你可以在浏览器中为地图应用构建一个"请求拦截 + 本地缓存"机制,从而显著提升地图加载性能、减少重复网络请求。关键在于:
-
明确哪些资源值得缓存(瓦片、地形、影像、3D 模型等)
-
拦截请求 + 返回缓存 +写入缓存的流程
-
版本控制与缓存管理。
如果你在项目中使用 mars‑3d,建议将这段缓存逻辑与底图/地形图层的加载流程结合起来使用,并在实际设备(尤其是移动设备/弱网络)上做性能测试。
