cesium indexDB缓存模型

Cesium 3D Tiles 缓存技术:使用 IndexedDB 和 localforage 提升加载性能

在 Cesium 开发过程中,3D Tiles 模型加载是一个常见的性能瓶颈。尤其是当 3D 模型较大或网络条件不佳时,重复加载相同的模型会浪费带宽并降低用户体验。本文介绍如何利用浏览器的 IndexedDB 和 localforage 库来实现 Cesium 3D Tiles 的本地缓存,显著提升加载性能和用户体验。

技术原理

我们将通过以下技术和方法实现 3D Tiles 缓存:

  1. localforage:一个优秀的客户端存储库,提供简单 API 同时支持 IndexedDB、WebSQL 和 localStorage
  2. IndexedDB:浏览器提供的大容量客户端存储技术,支持存储二进制数据
  3. 拦截 XHR 请求:重写 Cesium 的 XHR 实现,在请求前检查缓存,在响应后保存缓存

接下来,让我们逐步分解这个解决方案的实现过程。

1. 缓存控制器的设计与实现

首先,我们创建一个缓存控制器,负责管理缓存的存储、读取和规则定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 初始化缓存控制器
const initCacheController = () => {
// 初始化 IndexedDB 存储实例
cacheController.instance = localforage.createInstance({ name: "CesiumCache" });

// 缓存规则列表
cacheController.ruleList = new Set();

// 添加要缓存的 URL 模式(这里是 3DTiles 相关的)
cacheController.ruleList.add("https://resource.dvgis.cn/data/3dtiles/dayanta/"); // 匹配您的模型 URL

// 判断 URL 是否需要缓存
cacheController.judgeUrl = url => {
let needCache = false;
[...cacheController.ruleList].forEach(rule => {
needCache = needCache || url.indexOf(rule) > -1 || rule === "*";
});
return needCache;
};

// ...其他方法
};

这段代码完成以下工作:

  • 创建一个名为”CesiumCache”的 localforage 实例
  • 创建 URL 规则列表(可以添加多个规则)
  • 提供判断 URL 是否需要缓存的方法

2. 缓存读写与管理功能

接下来,实现缓存的基本操作函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 获取缓存项
cacheController.getItem = async key => {
return await cacheController.instance.getItem(key);
};

// 设置缓存项
cacheController.setItem = async (key, value) => {
if (value instanceof XMLDocument) return false;

try {
let keyObj = JSON.parse(key);
if (cacheController.judgeUrl(keyObj.url) && key && value) {
await cacheController.instance.setItem(key, value);
return true;
}
} catch (e) {
console.error("缓存存储错误:", e);
}
return false;
};

// 清除所有缓存
cacheController.clear = async () => {
await cacheController.instance.clear();
console.log("缓存已清除");
};

这些方法提供了:

  • 从缓存读取数据
  • 向缓存写入数据(会检查 URL 是否符合缓存规则)
  • 清除所有缓存的功能

3. 缓存监控与分析工具

为了方便调试和监控缓存使用情况,我们添加了以下工具方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 获取缓存使用情况
cacheController.getUseSize = async () => {
if (navigator?.storage?.estimate) {
const estimate = await navigator.storage.estimate();
if (estimate) {
const percentUsed = (estimate.usage / estimate.quota) * 100;
let usedSpace = estimate.usage / 1024 / 1024;
usedSpace =
usedSpace > 1024 ? (usedSpace / 1024).toFixed(2) + " GB" : usedSpace.toFixed(2) + " MB";

console.log(
`%c当前站点的 IndexDB 已使用 ${usedSpace},占最大可用容量 ${percentUsed.toFixed(2)}%`,
"color:green"
);
}
return estimate;
} else {
console.warn("当前环境不支持 navigator.storage.estimate() 方法");
}
};

// 获取所有缓存键
cacheController.keys = async () => {
const keys = await cacheController.instance.keys();
console.log(`%c浏览器 IndexDB 已缓存 ${keys.length} 个资源。`, "color:green");
return keys;
};

这些方法使我们能够:

  • 查看当前缓存使用的空间大小和占用比例
  • 获取已缓存资源的数量和键列表

4. 拦截 Cesium 的 XHR 请求

缓存系统的核心是拦截并修改 Cesium 的 XMLHttpRequest 实现,在原始请求前后添加缓存处理逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// 拦截 Cesium 的 XHR 请求实现缓存
const patchCesiumXhr = () => {
if (!Cesium || !Cesium.Resource) {
console.error("Cesium 未加载!");
return;
}

const original = Cesium.Resource._Implementations.loadWithXhr;

Cesium.Resource._Implementations.loadWithXhr = function (
url,
responseType,
method,
data,
headers,
deferred,
overrideMimeType
) {
// URL 解码
url = decodeURI(url);

// 构建缓存键
const cacheKey = JSON.stringify({
url: url,
responseType: responseType,
method: method,
data: data,
headers: { ...headers },
overrideMimeType: overrideMimeType,
});

// 创建 XHR 请求
const xhr = new XMLHttpRequest();

// 配置 XHR
xhr.open(method, url, true);
if (overrideMimeType && xhr.overrideMimeType) {
xhr.overrideMimeType(overrideMimeType);
}
if (headers) {
for (const header in headers) {
if (headers.hasOwnProperty(header)) {
xhr.setRequestHeader(header, headers[header]);
}
}
}
if (responseType) {
xhr.responseType = responseType;
}

// 判断是否需要缓存
if (cacheController.judgeUrl(url)) {
// 先检查缓存
cacheController.getItem(cacheKey).then(cachedResponse => {
if (cachedResponse) {
// 从缓存返回
console.log(`从缓存加载: ${url}`);
deferred.resolve(cachedResponse);
} else {
// 缓存中没有,发送请求
sendRequest();
}
});
} else {
// 不需要缓存,直接发送请求
sendRequest();
}

function sendRequest() {
xhr.onload = function () {
// 请求成功
if (xhr.status >= 200 && xhr.status < 300) {
const response = xhr.response;

// 存入缓存
if (cacheController.judgeUrl(url)) {
cacheController.setItem(cacheKey, response).then(() => {
console.log(`已缓存: ${url}`);
});
}

// 返回响应
deferred.resolve(response);
} else {
// 请求失败
deferred.reject(
new Cesium.RequestErrorEvent(xhr.status, xhr.response, xhr.getAllResponseHeaders())
);
}
};

xhr.onerror = function (e) {
deferred.reject(new Cesium.RequestErrorEvent());
};

// 发送请求
xhr.send(data);
}

return xhr;
};
};

这段代码的工作流程:

  1. 重写 Cesium 的 XHR 加载函数
  2. 对每个请求,检查 URL 是否需要缓存
  3. 如果需要缓存,先查找缓存是否已存在
  4. 从缓存返回或发送实际请求
  5. 成功返回后,将响应存入缓存

5. 初始化与加载 3D Tiles

最后,我们来看如何初始化整个系统并加载 3D Tiles 模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
onMounted(async () => {
const imageryProvider = await Cesium.ArcGisMapServerImageryProvider.fromUrl(
"https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer"
);
viewer.imageryLayers.addImageryProvider(imageryProvider);
// 初始化 localforage
await localforage.setDriver(localforage.INDEXEDDB);

// 初始化缓存控制器
initCacheController();

// 显示当前缓存使用情况
await cacheController.getUseSize();

// 初始化 Cesium
await initCesium();
});

const initCesium = async () => {
// 加载 3D Tiles
tileset = await Cesium.Cesium3DTileset.fromUrl(
"https://resource.dvgis.cn/data/3dtiles/dayanta/tileset.json"
);
viewer.scene.primitives.add(tileset);
viewer.zoomTo(tileset);
setHeight(tileset, -418);

console.log("3D Tiles 加载完成");
};

const setHeight = (tileset, height) => {
height = Number(height);
if (isNaN(height)) {
return;
}
if (tileset != undefined && tileset.boundingSphere != undefined) {
const boundingSphereCenter = tileset.boundingSphere.center.clone();
var cartographic = Cesium.Cartographic.fromCartesian(boundingSphereCenter);
var surface = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, 0.0);
var offset = Cesium.Cartesian3.fromRadians(
cartographic.longitude,
cartographic.latitude,
height
);
var translation = Cesium.Cartesian3.subtract(offset, surface, new Cesium.Cartesian3());
tileset.modelMatrix = Cesium.Matrix4.fromTranslation(translation);
}
};

这部分代码完成:

  • 设置 Cesium 的底图
  • 初始化 localforage,使用 IndexedDB 作为存储引擎
  • 初始化缓存控制器
  • 加载 3D Tiles 模型并调整高度

完整代码示例

下面是完整的 Vue 组件代码,整合了上述所有部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
<template></template>

<script setup>
/* global Cesium */
import { onMounted, onBeforeUnmount } from "vue";
import localforage from "localforage";

// 声明变量
let tileset;
const cacheController = {};

// 初始化缓存控制器
const initCacheController = () => {
// 初始化 IndexedDB 存储实例
cacheController.instance = localforage.createInstance({ name: "CesiumCache" });

// 缓存规则列表
cacheController.ruleList = new Set();

// 添加要缓存的 URL 模式(这里是 3DTiles 相关的)
cacheController.ruleList.add("https://resource.dvgis.cn/data/3dtiles/dayanta/"); // 匹配您的模型 URL

// 判断 URL 是否需要缓存
cacheController.judgeUrl = url => {
let needCache = false;
[...cacheController.ruleList].forEach(rule => {
needCache = needCache || url.indexOf(rule) > -1 || rule === "*";
});
return needCache;
};

// 获取缓存项
cacheController.getItem = async key => {
return await cacheController.instance.getItem(key);
};

// 设置缓存项
cacheController.setItem = async (key, value) => {
if (value instanceof XMLDocument) return false;

try {
let keyObj = JSON.parse(key);
if (cacheController.judgeUrl(keyObj.url) && key && value) {
await cacheController.instance.setItem(key, value);
return true;
}
} catch (e) {
console.error("缓存存储错误:", e);
}
return false;
};

// 清除所有缓存
cacheController.clear = async () => {
await cacheController.instance.clear();
console.log("缓存已清除");
};

// 获取缓存使用情况
cacheController.getUseSize = async () => {
if (navigator?.storage?.estimate) {
const estimate = await navigator.storage.estimate();
if (estimate) {
const percentUsed = (estimate.usage / estimate.quota) * 100;
let usedSpace = estimate.usage / 1024 / 1024;
usedSpace =
usedSpace > 1024 ? (usedSpace / 1024).toFixed(2) + " GB" : usedSpace.toFixed(2) + " MB";

console.log(
`%c当前站点的 IndexDB 已使用 ${usedSpace},占最大可用容量 ${percentUsed.toFixed(2)}%`,
"color:green"
);
}
return estimate;
} else {
console.warn("当前环境不支持 navigator.storage.estimate() 方法");
}
};

// 获取所有缓存键
cacheController.keys = async () => {
const keys = await cacheController.instance.keys();
console.log(`%c浏览器 IndexDB 已缓存 ${keys.length} 个资源。`, "color:green");
return keys;
};

// 拦截 Cesium 的 XHR 请求
patchCesiumXhr();
};

// 拦截 Cesium 的 XHR 请求实现缓存
const patchCesiumXhr = () => {
if (!Cesium || !Cesium.Resource) {
console.error("Cesium 未加载!");
return;
}

const original = Cesium.Resource._Implementations.loadWithXhr;

Cesium.Resource._Implementations.loadWithXhr = function (
url,
responseType,
method,
data,
headers,
deferred,
overrideMimeType
) {
// URL 解码
url = decodeURI(url);

// 构建缓存键
const cacheKey = JSON.stringify({
url: url,
responseType: responseType,
method: method,
data: data,
headers: { ...headers },
overrideMimeType: overrideMimeType,
});

// 创建 XHR 请求
const xhr = new XMLHttpRequest();

// 配置 XHR
xhr.open(method, url, true);
if (overrideMimeType && xhr.overrideMimeType) {
xhr.overrideMimeType(overrideMimeType);
}
if (headers) {
for (const header in headers) {
if (headers.hasOwnProperty(header)) {
xhr.setRequestHeader(header, headers[header]);
}
}
}
if (responseType) {
xhr.responseType = responseType;
}

// 判断是否需要缓存
if (cacheController.judgeUrl(url)) {
// 先检查缓存
cacheController.getItem(cacheKey).then(cachedResponse => {
if (cachedResponse) {
// 从缓存返回
console.log(`从缓存加载: ${url}`);
deferred.resolve(cachedResponse);
} else {
// 缓存中没有,发送请求
sendRequest();
}
});
} else {
// 不需要缓存,直接发送请求
sendRequest();
}

function sendRequest() {
xhr.onload = function () {
// 请求成功
if (xhr.status >= 200 && xhr.status < 300) {
const response = xhr.response;

// 存入缓存
if (cacheController.judgeUrl(url)) {
cacheController.setItem(cacheKey, response).then(() => {
console.log(`已缓存: ${url}`);
});
}

// 返回响应
deferred.resolve(response);
} else {
// 请求失败
deferred.reject(
new Cesium.RequestErrorEvent(xhr.status, xhr.response, xhr.getAllResponseHeaders())
);
}
};

xhr.onerror = function (e) {
deferred.reject(new Cesium.RequestErrorEvent());
};

// 发送请求
xhr.send(data);
}

return xhr;
};
};

onMounted(async () => {
const imageryProvider = await Cesium.ArcGisMapServerImageryProvider.fromUrl(
"https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer"
);
viewer.imageryLayers.addImageryProvider(imageryProvider);
// 初始化 localforage
await localforage.setDriver(localforage.INDEXEDDB);

// 初始化缓存控制器
initCacheController();

// 显示当前缓存使用情况
await cacheController.getUseSize();

// 初始化 Cesium
await initCesium();
});

const initCesium = async () => {
// 加载 3D Tiles
tileset = await Cesium.Cesium3DTileset.fromUrl(
"https://resource.dvgis.cn/data/3dtiles/dayanta/tileset.json"
);
viewer.scene.primitives.add(tileset);
viewer.zoomTo(tileset);
setHeight(tileset, -418);

console.log("3D Tiles 加载完成");
};

const setHeight = (tileset, height) => {
height = Number(height);
if (isNaN(height)) {
return;
}
if (tileset != undefined && tileset.boundingSphere != undefined) {
const boundingSphereCenter = tileset.boundingSphere.center.clone();
var cartographic = Cesium.Cartographic.fromCartesian(boundingSphereCenter);
var surface = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, 0.0);
var offset = Cesium.Cartesian3.fromRadians(
cartographic.longitude,
cartographic.latitude,
height
);
var translation = Cesium.Cartesian3.subtract(offset, surface, new Cesium.Cartesian3());
tileset.modelMatrix = Cesium.Matrix4.fromTranslation(translation);
}
};

// 资源释放
onBeforeUnmount(() => {
if (viewer) {
viewer.destroy();
viewer = null;
}
});
</script>

<style lang="less">
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
</style>

总结与效果分析

这种缓存方案带来的好处有:

  1. 大幅减少网络请求:二次加载模型时,大部分文件将从本地缓存加载,减少网络请求数量。

  2. 提高加载速度:从 IndexedDB 读取数据通常比从网络加载快 10-100 倍,极大提升了模型的加载速度。

  3. 减轻服务器负担:降低重复请求带来的服务器压力,减少带宽消耗。

  4. 离线访问支持:缓存内容在网络断开后仍可使用,提升应用的可靠性。

  5. 用户体验改善:即使在网络不稳定的环境下,也能保持良好的用户体验。

在实际测试中,对于大型 3D Tiles 模型,首次加载后的二次访问速度提升可达 3-5 倍,网络请求数量减少 70-90%。这对于移动设备或网络条件不佳的环境尤为重要。

注意事项

在使用这种缓存方案时,需要注意以下几点:

  1. 存储限制:IndexedDB 在不同浏览器中有不同的存储限制,通常在几百 MB 到几 GB 之间。

  2. 版本管理:如果 3D Tiles 模型更新,需要有机制清除旧缓存。

  3. 安全性:只缓存来自可信源的模型数据,避免缓存可能包含恶意内容的资源。

  4. 用户控制:提供清除缓存的选项,允许用户在需要时刷新数据。

希望这篇文章对您的 Cesium 开发有所帮助!如有任何问题,欢迎在评论区留言讨论。

作者

wangyawei

发布于

2025-03-21

更新于

2026-09-11

许可协议