在Cesium开发中,我们经常需要全局唯一的地球实例(Cesium.Viewer)—— 一方面,地球渲染本身消耗大量资源,多实例容易导致性能问题;另一方面,业务逻辑(如添加实体、切换图层)通常需要基于同一个地球上下文操作。单例模式恰好能解决“全局唯一实例+统一访问入口”的需求,本文就来解析如何用单例模式封装Cesium,以及背后的设计思路。
一、单例模式与Cesium的适配性
单例模式的核心是“确保一个类只有一个实例,并提供一个全局访问点”。这与Cesium的使用场景高度匹配:
-
资源唯一性:
Cesium.Viewer初始化时会占用Canvas、WebGL上下文等资源,多实例可能导致冲突; - 操作统一性:业务中添加图层、绘制实体等操作,必须基于同一个地球实例才能生效;
- 简化调用:全局统一入口,避免在不同模块中重复初始化或传递实例。
二、封装结构解析
一个典型的Cesium单例封装会包含以下核心模块,我们通过代码示例(伪代码)来拆解:
class CesiumSingleton {
// 1. 私有静态变量:存储唯一实例
static #instance = null;
// 2. 实例属性:地球实例、容器DOM等
#viewer = null; // Cesium.Viewer实例
#containerId = null; // 地球容器ID
#defaultOptions = { // 默认配置
terrainProvider: Cesium.createWorldTerrain(),
imageryProvider: new Cesium.ArcGisMapServerImageryProvider({
url: 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer'
}),
// 其他默认配置(如是否显示控件、动画等)
animation: false,
timeline: false
};
// 3. 私有构造函数:防止外部new实例
constructor(containerId, options = {}) {
if (CesiumSingleton.#instance) {
throw new Error('Cesium实例已存在,请通过getInstance获取');
}
this.#containerId = containerId;
this.#initViewer(options); // 初始化地球
}
// 4. 初始化地球实例
#initViewer(customOptions) {
const container = document.getElementById(this.#containerId);
if (!container) {
throw new Error(`未找到ID为${this.#containerId}的容器`);
}
// 合并默认配置与用户配置
const options = { ...this.#defaultOptions, ...customOptions };
this.#viewer = new Cesium.Viewer(this.#containerId, options);
// 初始化后的默认操作(如隐藏logo、设置初始视角等)
this.#viewer._cesiumWidget._creditContainer.style.display = 'none';
}
// 5. 静态方法:获取唯一实例(核心)
static getInstance(containerId, options) {
if (!CesiumSingleton.#instance) {
// 首次调用时创建实例
CesiumSingleton.#instance = new CesiumSingleton(containerId, options);
}
// 非首次调用时,若传入新容器ID,可做容错提示
if (containerId && CesiumSingleton.#instance.#containerId !== containerId) {
console.warn(`Cesium实例已绑定到容器${CesiumSingleton.#instance.#containerId},新容器ID无效`);
}
return CesiumSingleton.#instance;
}
// 6. 封装常用API:对外提供简化接口
// 获取原生Viewer实例(方便调用未封装的原生方法)
getViewer() {
return this.#viewer;
}
// 添加实体(简化原生addEntity操作)
addEntity(entityOptions) {
if (!this.#viewer) return null;
return this.#viewer.entities.add(entityOptions);
}
// 切换底图
setImageryProvider(provider) {
if (!this.#viewer) return;
this.#viewer.imageryLayers.removeAll();
this.#viewer.imageryLayers.addImageryProvider(provider);
}
// 7. 生命周期管理:销毁实例
destroy() {
if (this.#viewer) {
this.#viewer.destroy();
this.#viewer = null;
}
CesiumSingleton.#instance = null; // 重置单例,允许重新初始化
}
}
三、核心设计思路
1. 确保实例唯一性
- 通过私有静态变量
#instance存储实例,外部无法直接访问; - 构造函数
constructor为私有(或通过判断阻止重复创建),强制通过getInstance获取实例; -
getInstance方法中判断实例是否存在:不存在则创建,存在则直接返回,避免重复初始化。
2. 配置与初始化分离
- 预设
#defaultOptions:包含常用默认配置(如地形、底图、控件显示状态),减少重复代码; - 支持用户传入
customOptions:通过对象合并覆盖默认配置,兼顾灵活性与规范性; - 初始化逻辑封装在
#initViewer私有方法中,包含容器校验、实例创建、默认操作(如隐藏Cesium logo),确保初始化过程可控。
3. 简化API调用
- 对外暴露
addEntity、setImageryProvider等常用方法,屏蔽Cesium原生API的复杂性(如viewer.entities.add); - 提供
getViewer方法:当需要调用未封装的原生功能时(如特殊相机操作),可直接获取Viewer实例,避免封装过度导致的局限性。
4. 生命周期管理
-
destroy方法:不仅销毁Viewer实例释放资源,还重置单例变量,支持在特殊场景下重新初始化(如页面切换时销毁旧地球,返回时重建); - 容错处理:如重复传入不同容器ID时给出警告,避免无意识的错误操作。
四、使用示例
封装后,在项目中使用会非常简洁:
// 首次初始化(传入容器ID和自定义配置)
const cesiumInstance = CesiumSingleton.getInstance('cesiumContainer', {
infoBox: false // 覆盖默认配置,隐藏信息框
});
// 在其他模块中获取实例(无需重复初始化)
const sameInstance = CesiumSingleton.getInstance();
// 添加一个点实体
cesiumInstance.addEntity({
position: Cesium.Cartesian3.fromDegrees(116, 39, 1000),
point: { color: Cesium.Color.RED, pixelSize: 10 }
});
// 切换底图为天地图
cesiumInstance.setImageryProvider(new Cesium.WebMapTileServiceImageryProvider({
url: 'http://t0.tianditu.gov.cn/img_w/wmts?service=wmts&request=GetTile&version=1.0.0&LAYER=img&tileMatrixSet=w&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles',
layer: 'img',
style: 'default',
format: 'image/jpeg'
}));
// 页面销毁时释放资源
cesiumInstance.destroy();
五、优缺点与适用场景
优点:
- 全局统一:避免多实例冲突,所有操作基于同一个地球上下文;
- 简化使用:封装后无需重复编写初始化代码,降低新人上手成本;
- 便于维护:核心配置和方法集中管理,修改时只需改单例类,无需全局搜索。
缺点:
- 灵活性有限:不适合需要同时显示多个地球的场景(如左右分屏对比);
- 单例依赖:过度依赖单例可能导致模块间耦合度升高(可通过注入方式缓解)。
适用场景:
- 绝大多数单地球场景(如数字孪生、GIS应用、三维地图展示);
- 中小型项目,追求开发效率和简单性。
六、总结
用单例模式封装Cesium的核心是“以最小成本解决全局唯一实例问题”:通过限制实例数量避免资源冲突,通过封装API降低使用门槛,通过统一配置提升项目规范性。
如果你的项目不需要多地球实例,这种封装方式能显著减少重复代码,让团队更专注于业务逻辑而非Cesium的基础配置。当然,实际开发中可根据需求扩展(如添加事件监听封装、相机控制简化等),让单例类更贴合项目场景。