最近有点儿事儿,之前的大屏项目拖了一段时间,现在打算继续开发。原本以为用熟悉的
Cesium能快速搞定,没想到还是踩了几个坑,整理出来和大家分享,避免后续有人走同样的弯路。
页面地球飞入效果采用 Cesium 进行开发,一来 Cesium 作为开源的3D地理信息可视化框架,API 封装完善,开发效率高。
二来个人长期使用该框架,对其核心逻辑比较熟悉,本以为能快速落地,实际开发中却遇到了加载、时机监听、多场景复用等多个问题,逐一排查解决后,才实现了流畅的飞入效果。
实现效果

Cesium 地球初始化完成后,自动触发指定地点的飞入动画,相机从初始视角平滑过渡到目标经纬度对应的视角,过程流畅无卡顿、无图层闪烁。
核心问题
简单的飞入效果之前都是使用的现成的方法,从零开始遇到点问题。
实际开发中需要兼顾加载性能、时机准确性、多场景复用等,具体问题如下:
- Cesium 加载速度问题:在线地图服务加载延迟高,弱网环境下易报错,影响用户体验;
- Cesium 加载完成时机的监听:若监听时机不准确,会导致飞入动画触发时,地图图层、影像未加载完成,出现"空地球"或"图层闪烁"问题;
- 多地点复用问题:大屏项目中可能需要切换多个目标地点,需对飞入逻辑进行封装,实现灵活调用。
解决方案(附完整代码+细节说明)
加载速度优化:解决在线地图加载慢的问题
我最初采用的是 Cesium 官方的 Ion 在线地图服务,毕竟无需额外配置,直接调用即可,但实际测试后发现两个致命问题:
-
加载延迟高:官方服务器位于海外,国内网络环境下,地图影像加载速度极慢,甚至需要10秒以上才能完全渲染,远超用户6~8秒的等待极限;
-
稳定性差:弱网环境下,地图会直接加载失败,控制台报错"影像图层加载超时",导致页面无法正常展示。
在线地图服务受网络环境影响极大,若要实现生产环境的稳定运行,优先替换为本地地图服务(如天地图本地部署、GeoServer 发布的本地影像),从根源上解决加载慢、报错的问题。
由于手头暂无本地地图部署工具,本次开发暂用在线地图过渡,后续会替换为本地服务。
以下是优化后的初始化代码,增加了加载超时处理,提升弱网环境下的容错性:
js
// 导入 Cesium 核心模块
import * as Cesium from 'cesium'
// 引入 Cesium 样式(必须,否则控件和地球样式异常)
import 'cesium/Build/Cesium/Widgets/widgets.css'
// 初始化 Cesium 地球(增加超时处理,优化加载体验)
const initCesium = async () => {
try {
// 配置 Cesium Token(可从 Cesium 官网免费申请,需注册账号)
Cesium.Ion.defaultAccessToken = '你的官网Token';
// 创建 Cesium 视图实例,精简界面控件,提升加载速度
viewer.value = new Cesium.Viewer('cesiumContainer', {
// 隐藏默认控件,适配大屏简洁风格
timeline: false, // 时间轴控件
animation: false, // 动画控件
baseLayerPicker: false, // 底图切换控件
geocoder: false, // 地理编码控件(搜索地点)
homeButton: false, // 首页按钮
infoBox: false, // 信息弹窗(点击要素时显示)
sceneModePicker: false, // 场景模式切换(2D/3D/哥伦布视图)
navigationHelpButton: false, // 导航帮助按钮
// 性能优化配置
scene3DOnly: true, // 仅开启3D模式,减少2D渲染开销
requestRenderMode: true, // 开启请求渲染模式,降低CPU占用
maximumRenderTimeChange: 1 / 60, // 控制渲染帧率,避免卡顿
// 开启地形(如果不需要地形展示,可注释,进一步提升加载速度)
// terrainProvider: Cesium.createWorldTerrain()
});
// 隐藏 Cesium 底部版权信息(可选,根据项目需求调整)
viewer.value._cesiumWidget._creditContainer.style.display = 'none';
// 等待 Cesium 完全加载完成(包括影像图层、场景渲染)
await waitForCesiumFullyLoaded();
// 触发 cesiumReady 事件,通知外部执行飞入等后续操作
emit('cesiumReady', viewer.value);
} catch (error) {
console.error('Cesium 初始化失败:', error);
// 加载失败提示,提升用户体验
ElMessage.error('地球加载失败,请检查网络或刷新页面重试');
}
}
加载时机监听
这是本次开发最容易踩坑的点,在创建 viewer 实例后,直接触发飞入动画,导致动画执行时,地图影像还未加载完成,出现"相机飞向空地球"的尴尬场景。
Cesium 初始化是异步过程,创建 viewer 实例只是第一步,后续还需要加载影像图层、渲染场景、初始化相机等操作,这些操作完成后,才能确保飞入动画的流畅性。
封装两个异步方法,分别监听场景渲染就绪 和影像图层加载完成,只有两个条件都满足,才触发后续的飞入操作,确保时机精准。
代码如下:
js
/**
* 等待 Cesium 完全加载完成(包括场景渲染和影像图层)
* 核心逻辑:先确保场景渲染就绪,再等待影像图层加载完成,双重校验
* @returns {Promise}
*/
const waitForCesiumFullyLoaded = () => {
return new Promise((resolve) => {
const checkSceneReady = () => {
// 先检查 viewer 和 scene 是否存在(避免初始化未完成时调用)
if (!viewer.value || !viewer.value.scene) {
// 每50ms检查一次,避免频繁占用资源
setTimeout(checkSceneReady, 50);
return;
}
// 使用 postRender 事件,确保场景至少完成一帧渲染
viewer.value.scene.postRender.addEventListener(() => {
// 场景就绪后,再等待影像图层加载完成
waitForImageryLoaded().then(resolve);
}, viewer.value.scene);
};
checkSceneReady();
});
}
/**
* 等待影像图层加载完成(单独封装,便于后续扩展)
* 核心逻辑:遍历所有影像图层,检查是否有正在加载的图块
* @returns {Promise}
*/
const waitForImageryLoaded = () => {
return new Promise((resolve) => {
// 若 viewer 或 scene 不存在,直接resolve(容错处理)
if (!viewer.value || !viewer.value.scene) {
resolve();
return;
}
const imageryLayers = viewer.value.imageryLayers;
// 若没有影像图层,直接resolve
if (!imageryLayers || imageryLayers.length === 0) {
resolve();
return;
}
// 循环检查所有影像图层是否加载完成
const checkLoaded = () => {
let allLoaded = true;
for (let i = 0; i < imageryLayers.length; i++) {
const layer = imageryLayers.get(i);
if (layer && layer.imageryProvider) {
// 检查当前图层是否有正在加载的图块(_loading 为Cesium内部属性)
if (layer._loading) {
allLoaded = false;
break;
}
}
}
if (allLoaded) {
// 确保影像加载完成后,场景再渲染一帧,避免闪烁
viewer.value.scene.postRender.addEventListener(() => {
resolve();
}, viewer.value.scene);
} else {
// 每100ms检查一次,平衡性能和准确性
setTimeout(checkLoaded, 100);
}
};
checkLoaded();
});
}
关键注意点:将两个方法拆分开写,是为了后续扩展------比如项目中需要添加3D模型、矢量数据加载。
可直接在 waitForCesiumFullyLoaded 方法中添加对应的等待逻辑,无需大幅修改代码,提升可维护性。
封装飞入方法
大屏项目中,往往需要切换多个目标地点(如从全国视角飞入各省、从省视角飞入各市),若每次切换都重复编写代码冗余。
因此,简单封装一个通用的飞入方法。
js
/**
* 控制 Cesium 相机飞往指定目标地点(通用封装,支持多场景复用)
* @param {Object} options - 飞行配置项(必传参数标注,可选参数有默认值)
* @param {Number} options.longitude - 目标经度(必传,如北京:116.4074)
* @param {Number} options.latitude - 目标纬度(必传,如北京:39.9042)
* @param {Number} options.height - 目标高度 (米,必传,根据场景调整,如大屏常用5000米)
* @param {Number} [options.duration=3] - 飞行时长 (秒,可选,默认3秒,兼顾流畅度和效率)
* @param {Number} [options.heading=0] - 相机朝向 (角度,可选,0 为正北,可根据需求调整)
* @param {Number} [options.pitch=-60] - 俯仰角 (角度,可选,-90 为垂直向下,-60为常用视角)
* @param {Function} [options.onComplete] - 飞行完成回调(可选,如飞行结束后加载区域数据)
* @param {Function} [options.onCancel] - 飞行取消回调(可选,如用户手动中断飞行时的处理)
*/
const flyToLocation = async (options) => {
// 校验 viewer 实例是否存在,避免报错
if (!viewer.value) {
console.warn('Viewer 实例不存在,无法执行飞行操作');
ElMessage.warning('地球未加载完成,无法执行飞入操作');
return;
}
// 解构配置项,设置默认值
const {
longitude,
latitude,
height,
duration = 3,
heading = 0,
pitch = -60,
onComplete,
onCancel
} = options
// 由于 cesiumReady 触发时已确保影像加载完成,这里直接执行飞行
viewer.value.camera.flyTo({
// 将经纬度、高度转换为 Cesium 支持的笛卡尔坐标系
destination: Cesium.Cartesian3.fromDegrees(longitude, latitude, height),
// 相机朝向配置(heading:方位角,pitch:俯仰角,roll:翻滚角)
orientation: {
heading: Cesium.Math.toRadians(heading), // 角度转弧度(Cesium 内部使用弧度)
pitch: Cesium.Math.toRadians(pitch),
roll: 0.0 // 翻滚角,默认0,无需调整
},
duration: duration, // 飞行时长
complete: () => {
console.log('已飞到目标地点!');
// 执行完成回调(若有)
if (onComplete) onComplete();
},
cancel: () => {
console.log('飞行被取消!');
// 执行取消回调(若有)
if (onCancel) onCancel();
},
canInterrupt: true // 允许用户手动中断飞行(如鼠标拖拽相机)
})
}
注意 :项目使用 Vue3 + setup 语法,需通过 defineExpose 将 flyToLocation 方法导出,外部组件才能调用。
总结
Cesium 作为成熟的3D地理可视化框架,本身的 API 封装已经非常完善,实现飞入效果的核心逻辑并不复杂。
但实际开发中,往往是细节问题导致踩坑,总结几点关键经验,供大家参考:
-
加载优化优先选本地地图:生产环境中,务必替换掉官方在线地图,改用本地部署的地图服务(天地图、高德地图本地切片等),彻底解决加载慢、报错的问题;
-
加载时机监听不能省:不要省略
waitForCesiumFullyLoaded方法,否则会出现图层闪烁、空地球等问题,拆分方法便于后续扩展; -
封装逻辑提升复用性:多地点切换场景,一定要封装通用的飞入方法,明确配置项的必传/可选,增加容错处理,减少代码冗余;
-
内存管理要注意:页面卸载时,务必销毁
Cesium实例(包括viewer、事件监听等),避免内存泄漏,导致页面卡顿、崩溃,销毁代码示例如下:
js
// 页面卸载时销毁 Cesium 实例(Vue3 onUnmounted 中调用)
onUnmounted(() => {
if (viewer.value) {
// 销毁 viewer 实例,释放内存
viewer.value.destroy();
viewer.value = null;
}
});
最后,Cesium 的坑大多集中在"加载时机 "和"性能优化"上,只要理清初始化流程、做好细节校验,就能快速实现流畅的交互效果。
后续我会继续更新这个大屏项目中 Cesium 的其他坑点,欢迎大家留言交流,共勉!