Cesium 多业务页面切换的资源隔离与架构设计:单 Viewer 复用还是多实例隔离

Cesium 多业务页面切换的资源隔离与架构设计:单 Viewer 复用还是多实例隔离?

一、背景

在实际数字孪生、智慧园区、低空经济和 GIS 可视化项目中,Cesium 往往不是一个独立的地图页面,而是整个业务系统的基础可视化引擎。

例如,一个系统可能包含:

  • 园区三维场景。
  • 视频监控联动。
  • GIS 区域分析。
  • 城市数字孪生
  • 水利流域数字孪生
  • 低空经济无人机管控平台
  • 交通航运管理
  • 热力图和专题图层。

这些业务可能共享同一个地图底座,但在不同页面中又有不同的数据、渲染对象和交互逻辑。

随着业务模块增加,Cesium 的资源管理逐渐成为一个架构问题。

常见问题包括:

  1. 页面切换后,旧业务的 Entity 和 Primitive 没有清理干净。
  2. WebSocket 数据仍然持续更新已经退出的业务场景。
  3. 多个业务模块同时操作同一个 Viewer,容易出现状态冲突。
  4. 地图对象持续创建和销毁,内存占用不断增长。
  5. 不同业务之间的资源互相影响,难以定位性能问题。

那么,对于大型 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 隔离,各自都有适用场景。

对于多数共享地图底座的数字孪生系统,可以优先考虑:

  1. 共享 Viewer,避免频繁重复初始化。
  2. 按业务模块划分资源所有权。
  3. 使用统一资源管理器清理业务资源。
  4. 将数据接入、业务模型和地图视图解耦。
  5. 对实时数据进行增量更新和渲染调度。
  6. 对确实需要独立运行环境的业务,再考虑 Iframe 隔离。

最终目标不是让所有资源都集中在一个管理器中,而是让每种资源都有明确的所有者、生命周期和销毁责任。

一个成熟的 Cesium 工程架构,不应该只关注如何创建地图,更应该关注地图如何长期稳定运行。

相关推荐
律宏阔12 分钟前
微信小程序 + Taro 从 ESLint 迁移到 Oxlint 让项目代码检查速度提升近 12 倍
前端·typescript·eslint
用户308169987648515 分钟前
复制的代码为什么报错?BOM 等 10 种隐形字符 40 组实测
前端
Swot17 分钟前
SvelteKit 踩坑记:+page.js 里忘了 ...data,服务端数据全没了
前端·后端·svelte
律宏阔22 分钟前
微信小程序 + Taro + 多语言实现与国际化检查实录
前端·微信小程序·taro
deli0072 小时前
Redis 缓存穿透、击穿、雪崩:9 组流量实测数据库怎么被打穿
前端
江华森2 小时前
5Mbps 小水管也能扛:K3s 上 k6 压测 + HPA 弹性伸缩实测
前端
shuangyueliao2 小时前
103 个小游戏的商店素材,我一张图都没手动做:截图 / 封面 / 打包 / 提审全自动
前端
ZhengEnCi2 小时前
前端项目为什么需要一份统一的 theme.css:用 CSS 变量管住全局样式与主题切换
前端·css·前端工程化
wordbaby3 小时前
一次微信小程序 `web-view` 跳转原生页回跳后 H5 白屏的排查与修复(Hash 路由污染)
前端