Cesium 多业务页面切换的资源隔离与架构设计:单 Viewer 复用还是多实例隔离?
一、背景
在实际数字孪生、智慧园区、低空经济和 GIS 可视化项目中,Cesium 往往不是一个独立的地图页面,而是整个业务系统的基础可视化引擎。
例如,一个系统可能包含:
- 园区三维场景。
- 视频监控联动。
- GIS 区域分析。
- 城市数字孪生
- 水利流域数字孪生
- 低空经济无人机管控平台
- 交通航运管理
- 热力图和专题图层。
这些业务可能共享同一个地图底座,但在不同页面中又有不同的数据、渲染对象和交互逻辑。
随着业务模块增加,Cesium 的资源管理逐渐成为一个架构问题。
常见问题包括:
- 页面切换后,旧业务的 Entity 和 Primitive 没有清理干净。
- WebSocket 数据仍然持续更新已经退出的业务场景。
- 多个业务模块同时操作同一个 Viewer,容易出现状态冲突。
- 地图对象持续创建和销毁,内存占用不断增长。
- 不同业务之间的资源互相影响,难以定位性能问题。
那么,对于大型 Cesium 应用,应该采用单 Viewer 复用,还是每个业务页面独立创建 Viewer?
本文从资源隔离、数据管理和工程架构几个方面进行分析。
二、单 Viewer 共享模式
2.1 基本思路
整个应用只创建一个 Viewer,由不同业务页面共享。
markdown
Application
|
└── CesiumManager
|
└── Viewer
|
├── Scene
├── Camera
├── EntityCollection
├── PrimitiveCollection
└── DataSourceCollection
业务页面不负责创建 Viewer,只负责使用地图管理器提供的接口。
例如:
kotlin
class CesiumManager {
constructor(container) {
this.viewer = new Cesium.Viewer(container);
}
getViewer() {
return this.viewer;
}
destroy() {
if (
this.viewer &&
!this.viewer.isDestroyed()
) {
this.viewer.destroy();
}
this.viewer = null;
}
}
业务模块:
javascript
class DeviceModule {
constructor(cesiumManager) {
this.viewer = cesiumManager.getViewer();
}
init() {
// 初始化设备业务
}
destroy() {
// 清理设备业务资源
}
}
2.2 优势
资源复用
Viewer、Scene、Camera 等核心对象不需要在每次页面切换时重新创建。
初始化成本较低
可以避免重复初始化地图容器、底图、地形和部分场景配置。
统一管理
所有业务共享一个地图入口,便于统一管理相机、图层和交互。
业务切换相对流畅
如果只是替换数据和显示图层,可以避免地图整体重新初始化。
2.3 问题
单 Viewer 共享模式最大的问题是资源隔离不足。
例如:
业务 A 添加了 1000 个 Entity,业务 B 添加了 500 个 Primitive。
如果业务切换时只隐藏页面组件,没有清理地图对象,那么这些对象仍然可能存在于共享 Scene 中。
另外,业务 A 注册的事件监听可能仍然在运行,业务 A 的 WebSocket 也可能继续更新地图。
这就意味着:
页面组件的生命周期和 Cesium 场景资源的生命周期并不完全一致。
因此,单 Viewer 共享模式必须建立独立的业务资源管理机制。
三、多 Viewer 独立实例模式
3.1 基本思路
每个业务页面独立创建 Viewer。
css
Application
|
├── Business A
| └── Viewer A
|
├── Business B
| └── Viewer B
|
└── Business C
└── Viewer C
不同业务拥有自己的 Scene、Camera 和渲染上下文。
页面退出时,直接销毁对应 Viewer。
scss
function createBusinessViewer(container) {
return new Cesium.Viewer(container);
}
function destroyBusinessViewer(viewer) {
if (viewer && !viewer.isDestroyed()) {
viewer.destroy();
}
}
3.2 优势
业务隔离更明确
不同 Viewer 的场景对象相互独立。
生命周期更容易管理
业务页面创建 Viewer,页面退出时销毁 Viewer。
降低场景状态耦合
不同业务可以拥有不同的相机、图层、场景配置和交互逻辑。
3.3 问题
多 Viewer 并不意味着性能一定更好。
每个 Viewer 都可能拥有独立的 WebGL 上下文、渲染资源、场景对象和相关缓存。
如果同时存在多个 Viewer,就可能增加:
- GPU 资源占用。
- WebGL 上下文数量。
- 初始化耗时。
- 网络资源请求。
- 浏览器内存占用。
另外,WebGL 上下文数量存在浏览器和设备相关限制,不能无限创建。
如果多个业务之间需要频繁共享地图状态,多 Viewer 也会增加通信和状态同步成本。
因此,多 Viewer 更适合需要明确场景隔离的业务,而不是所有页面都采用这种方式。
四、资源隔离:比 Viewer 数量更重要的设计
无论采用单 Viewer 还是多 Viewer,都需要解决一个核心问题:
如何明确每个业务模块创建的资源由谁管理?
这里可以引入业务资源域的概念。
每个业务模块拥有自己的资源管理器,负责记录该业务创建的地图对象和异步任务。
例如:
markdown
CesiumManager
|
└── Viewer
|
├── DeviceModule
| ├── Entities
| ├── Events
| ├── WebSocket
| └── Timers
|
├── DroneModule
| ├── Primitives
| ├── RouteData
| ├── Events
| └── WebSocket
|
└── AnalysisModule
├── ImageryLayers
├── DataSources
└── Events
每个模块只管理自己创建的资源。
4.1 业务资源管理器
可以设计统一的资源管理器:
kotlin
class BusinessResourceManager {
constructor() {
this.disposers = [];
this.destroyed = false;
}
register(dispose) {
if (this.destroyed) {
dispose();
return;
}
this.disposers.push(dispose);
}
destroy() {
if (this.destroyed) return;
this.destroyed = true;
for (const dispose of this.disposers.reverse()) {
try {
dispose();
} catch (error) {
console.error(error);
}
}
this.disposers.length = 0;
}
}
使用:
kotlin
class DeviceModule {
constructor(viewer) {
this.viewer = viewer;
this.resources = new BusinessResourceManager();
}
init() {
const entity = this.viewer.entities.add({
id: 'device-001',
position: Cesium.Cartesian3.fromDegrees(
116.397,
39.909
),
point: {
pixelSize: 10,
color: Cesium.Color.RED,
},
});
this.resources.register(() => {
this.viewer.entities.remove(entity);
});
}
destroy() {
this.resources.destroy();
}
}
这种方式可以将资源创建和销毁逻辑统一起来。
4.2 图层级别的资源管理
对于大量地图对象,还可以进一步按业务图层管理。
例如:
ini
class MapLayerManager {
constructor(viewer) {
this.viewer = viewer;
this.layers = new Map();
}
createLayer(name) {
const layer = {
entities: [],
primitives: [],
};
this.layers.set(name, layer);
return layer;
}
removeLayer(name) {
const layer = this.layers.get(name);
if (!layer) return;
layer.entities.forEach(entity => {
this.viewer.entities.remove(entity);
});
layer.primitives.forEach(primitive => {
this.viewer.scene.primitives.remove(primitive);
});
this.layers.delete(name);
}
}
实际工程中还应当考虑 DataSource、ImageryLayer、事件、异步任务和其他资源类型。
图层管理的价值在于:业务不再需要直接面对整个 Viewer,而是通过自己的资源域操作地图。
五、数据层与视图层解耦
在复杂实时业务中,仅仅管理 Entity 和 Primitive 还不够。
例如无人机实时轨迹:
yaml
WebSocket
|
↓
实时数据
|
↓
业务数据处理
|
↓
地图 ViewModel
|
↓
Cesium Entity / Primitive
|
↓
Scene Render
如果 WebSocket、业务数据处理和 Cesium 渲染全部写在同一个组件中,会造成严重的耦合。
建议将它们拆分为三个层次。
5.1 数据层
负责:
- WebSocket 连接。
- HTTP 数据请求。
- 数据格式转换。
- 数据缓存。
- 数据排序和过滤。
- 实时数据分发。
数据层不直接依赖 Cesium。
5.2 业务模型层
负责:
- 设备业务状态。
- 无人机位置状态。
- 轨迹数据组织。
- 业务对象与地图对象映射。
- 数据更新策略。
例如:
kotlin
const deviceStore = new Map();
function updateDeviceData(data) {
deviceStore.set(data.id, {
id: data.id,
longitude: data.longitude,
latitude: data.latitude,
height: data.height,
timestamp: data.timestamp,
});
}
5.3 视图层
负责:
- Entity 创建和更新。
- Primitive 创建和更新。
- 图层显示和隐藏。
- 相机交互。
- 地图渲染。
视图层从业务模型中获取数据,而不是直接管理 WebSocket。
这样可以降低业务数据与 Cesium 的耦合程度。
六、实时数据与地图缓存如何协调?
数据层解耦后,还有一个需要重点解决的问题:
如果业务数据被复制到 Store 或缓存中,如何避免地图显示的数据滞后?
这里需要区分数据存储和数据渲染。
对于实时业务,可以采用增量更新机制,而不是每次都将完整数据集复制到地图层。
例如:
scss
function handleDeviceMessage(message) {
const data = normalizeDeviceData(message);
updateDeviceStore(data);
deviceViewModel.update(data);
}
视图层:
kotlin
class DeviceViewModel {
constructor(viewer) {
this.viewer = viewer;
this.entities = new Map();
}
update(data) {
let entity = this.entities.get(data.id);
const position = Cesium.Cartesian3.fromDegrees(
data.longitude,
data.latitude,
data.height
);
if (entity) {
entity.position = position;
} else {
entity = this.viewer.entities.add({
id: data.id,
position,
point: {
pixelSize: 10,
},
});
this.entities.set(data.id, entity);
}
}
destroy() {
for (const entity of this.entities.values()) {
this.viewer.entities.remove(entity);
}
this.entities.clear();
}
}
对于高频实时数据,还可以进一步设计:
- 按设备 ID 合并更新。
- 丢弃过期数据。
- 使用时间戳排序。
- 对渲染更新进行节流。
- 使用 requestAnimationFrame 合并视觉更新。
- 将数据接收频率与地图渲染频率解耦。
例如,WebSocket 每秒接收数百次数据,并不意味着每条数据都必须立即触发一次地图更新。
可以先将最新状态写入缓冲区,再在渲染周期中统一更新。
这样可以减少重复计算和无意义的渲染操作。
七、Iframe 是否可以解决资源隔离问题?
当业务系统越来越复杂时,还有一种更彻底的隔离方案:Iframe。
css
Main Application
|
├── Business A iframe
| └── Cesium Viewer A
|
├── Business B iframe
| └── Cesium Viewer B
|
└── Business C iframe
└── Cesium Viewer C
Iframe 可以提供相对独立的:
- JavaScript 执行环境。
- DOM 环境。
- 全局变量空间。
- 页面生命周期。
当 Iframe 被真正移除时,浏览器可以释放其文档和相关资源,但实际资源回收仍然不是同步且绝对保证的。
同时,Iframe 并不能完全隔离 GPU 资源。
不同 Iframe 仍然运行在同一个浏览器进程或相关 GPU 资源管理环境中,也仍然受到 WebGL 上下文和 GPU 内存限制。
另外还会增加:
- 父子页面通信成本。
- 数据传输成本。
- 组件集成复杂度。
- 共享状态管理成本。
- 页面加载和初始化开销。
因此,Iframe 更适合需要独立运行环境、独立部署或者独立生命周期的复杂业务模块。
不应该仅仅因为 Cesium 内存增长,就直接采用 Iframe 隔离。
八、推荐的工程架构
综合以上几种方式,我更倾向于采用:
单 Viewer 核心实例 + 业务资源域隔离 + 数据视图解耦。
整体结构:
markdown
Application
|
├── Router
|
├── CesiumManager
| |
| └── Viewer
| |
| ├── SceneManager
| ├── LayerManager
| └── CameraManager
|
├── DataManager
| |
| ├── WebSocket
| ├── API
| └── Store
|
└── BusinessManager
|
├── DeviceModule
├── DroneModule
└── AnalysisModule
其中:
- CesiumManager:负责 Viewer 的创建、销毁和统一配置。
- SceneManager:管理场景级别的资源。
- LayerManager:管理业务图层。
- CameraManager:管理相机状态。
- DataManager:负责外部数据接入和分发。
- BusinessManager:负责业务模块生命周期。
- 各业务模块:只操作自己拥有的地图资源。
页面切换流程
yaml
进入业务 A
|
↓
创建业务 A
|
↓
注册资源管理器
|
↓
初始化数据连接
|
↓
创建地图对象
|
↓
业务运行
|
↓
退出业务 A
|
↓
停止数据更新
|
↓
移除事件和定时器
|
↓
清理 Entity / Primitive / Layer
|
↓
释放业务数据引用
|
↓
保留共享 Viewer
|
↓
进入业务 B
这里有一个重要原则:
业务页面销毁,不代表共享 Viewer 也必须销毁。
地图实例和业务模块应当拥有不同的生命周期。
九、总结
Cesium 在大型业务系统中的性能问题,不仅仅是渲染效率问题,也涉及资源管理和架构设计。
单 Viewer 复用、多 Viewer 隔离以及 Iframe 隔离,各自都有适用场景。
对于多数共享地图底座的数字孪生系统,可以优先考虑:
- 共享 Viewer,避免频繁重复初始化。
- 按业务模块划分资源所有权。
- 使用统一资源管理器清理业务资源。
- 将数据接入、业务模型和地图视图解耦。
- 对实时数据进行增量更新和渲染调度。
- 对确实需要独立运行环境的业务,再考虑 Iframe 隔离。
最终目标不是让所有资源都集中在一个管理器中,而是让每种资源都有明确的所有者、生命周期和销毁责任。
一个成熟的 Cesium 工程架构,不应该只关注如何创建地图,更应该关注地图如何长期稳定运行。