十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Cesium Viewer封装实战:从三维地球初始化到3DTiles与性能优化

Cesium Viewer封装实战:从三维地球初始化到3DTiles与性能优化 简介面向Web GIS开发与前端可视化入门者这份基于Cesium与Vue.js的轻量级示例演示了如何在Vue组件中初始化Cesium Viewer实现三维地球渲染与基础交互操作。Cesium本身提供全球地形、卫星影像、KML/GeoJSON矢量数据以及GLTF三维模型支持示例中的vue文件可作为接入这些数据格式的起点帮助开发者理解Viewer实例的创建、DOM容器挂载、默认地图源与控件配置等关键步骤。借助Cesium的丰富API还能进一步实现飞行路径规划、时间动态显示、标记添加与图层控制这些能力均有对应的基础写法可在该示例基础上扩展。包内只有1个vue文件压缩后约3KB结构极为精简适合直接阅读与改造借助Vue的数据绑定与事件机制还可进一步拓展地理数据集成、地图点击交互、自定义控件以及时间动态展示等功能形成一套从初始化到交互的完整实现思路。目前该资源已有162人学习下载对于想在既有Vue工程中快速集成Cesium并搭建3D地图模块的开发者是一份轻量而实用的参考。1. 不是每个项目都需要一个裸 CesiumViewer一个数字孪生项目通常会遇到这种情况十几个业务模块共用同一个三维地球有人往viewer.entities里塞了几千个点有人开着enableLighting让整个场景的色调变暖还有人直接把 viewer 挂在全局window上。三天后新来的同事问“Cesium.Viewer 和 cesiumViewer 是什么关系”——这其实就是标题cesiumViewer_cesium_想表达的核心Viewer 不是new出来就完事它需要被封装、被约束甚至被当作一个生命周期明确的服务来管理。这篇文章会用可执行的代码把 CesiumViewer 的初始化、数据挂载、交互、性能调优和组件化封装讲透。适合正在接手 Cesium 项目的前端开发以及需要给团队定三维地球规范的架构师。2. 初始化 CesiumViewer把构造参数和生命周期管起来Cesium 官方示例里new Cesium.Viewer(container)一行代码就会生成一个带时间轴、动画控件、地理编码框等二十几个控件的完整界面。但在业务系统里这些控件基本都会被放弃因为产品不希望用户拖拽地球时误触到界面外的按钮。所以封装的第一步就是决定哪些默认能力被保留、哪些被关掉。2.1 最小可用的 Viewer 初始化代码我一般会在项目里建一个cesiumViewer.js模块暴露createCesiumViewer和destroyCesiumViewer两个函数这样业务代码不直接去碰Cesium.Viewer构造器。下面是一个最小实现// src/cesium/viewer.js import * as Cesium from cesium; export function createCesiumViewer(containerId, options {}) { const viewer new Cesium.Viewer(containerId, { animation: false, timeline: false, baseLayerPicker: false, geocoder: false, homeButton: false, sceneModePicker: false, navigationHelpButton: false, fullscreenButton: false, scene3DOnly: true, useDefaultRenderLoop: true, ...options, }); // 统一关闭默认光照避免业务视觉不一致 viewer.scene.globe.enableLighting false; return viewer; }这段代码的关键不在viewer本身而在...options之前的那组默认值。animation和timeline分别控制动画控件和时间线窗口这两个控件在大多数业务场景里都是干扰baseLayerPicker决定是否显示图层选择器关闭之后就要自己设置底图。scene3DOnly: true会禁用 2D 和哥伦布视图这能避免后续加载 3DTiles 时出现坐标变换异常代价是用户无法切换到二维地图。如果你的产品还需要 2D 俯视这个值可以改成false但要额外处理viewer.scene.mode变化时的监听。只有这几十行还不算完容器尺寸也是一个常见掉坑点。Cesium 在初始化时会读取容器的高度和宽度如果容器是display: none或者高度为 0canvas 的渲染尺寸会是 0后续即使显示出来也需要手动调用viewer.resize()。所以组件化的第一步是确保容器已经有非零高度。2.2 必调的 5 个构造参数场景、光照、天空盒与默认交互前面那段代码只关了一批 UI 控件有几个参数很多人第一次碰到时根本不知道会影响什么。我把在真实项目里最需要关注的参数列成一张表。参数默认值建议值作用与注意点scene3DOnlyfalsetrue强制只使用 3D 视图减少模式切换时的相机重投影计算requestRenderModefalsefalse改 true 后在渲染循环上省电但动态效果必须手动触发渲染maximumScreenSpaceError216控制 3DTiles 和地形细节层次的屏幕误差值越大几何体越粗糙但加载越快fxaafalsetrue开启快速近似抗锯齿低端显卡上会占用较多着色器globe.enableLightingtrue按需这个不在构造参数里但它直接决定模型上的光照是否实时变化逐个展开。requestRenderMode是一个极具误导性的参数它默认关闭大部分入门项目不会碰它一旦改成true你会碰到“地球点了一下才刷新”的副作用。因为 Cesium 默认每帧都渲染而requestRenderMode: true只在相机移动或场景无效时重绘如果你的业务里有雷达扫描线、热力图必须写一个requestAnimationFrame循环在属性变化后调用viewer.scene.requestRender()否则界面看起来像卡死。maximumScreenSpaceError直接影响 3DTiles 加载质量。默认值 2 意味着屏幕误差很小瓦片会被分割得极细内存里同时存在大量 Tile 对象调到 16 之后远处模型会被合并成一个大块近处细节下降不多但加载数量显著减少。对于城市级数据我一般会把它设置在 8 到 16具体取值要看目标帧率。fxaa可以理解为后期抗锯齿开启后模型边缘不再有毛刺但它是在全屏后处理阶段实现的对像素着色器有额外开销。如果你的机器显卡足以跑满 60 帧就打开否则宁可关闭把资源留给阴影和光照。2.3 销毁与重建单页应用里的内存泄漏要从 destroy 开始Cesium 的Viewer.destroy()在文档里写得很简单但实际使用中很多人直接调用后会发现浏览器 console 出现一堆WebGL context lost错误。原因通常是没有移除事件监听器。Cesium 的ScreenSpaceEventHandler并不是viewer的属性而是自己创建的独立对象viewer.destroy()不会自动清理它。所以封装一个统一的销毁函数非常必要。// src/cesium/destroy.js export function destroyCesiumViewer(viewer, handlerList []) { if (!viewer || viewer.isDestroyed()) return; // 1. 先移除业务数据源和实体 viewer.entities.removeAll(); viewer.dataSources.removeAll(); viewer.scene.primitives.removeAll(); viewer.scene.globe.imageryLayers.removeAll(); // 2. 手动清理已保存的 ScreenSpaceEventHandler handlerList.forEach(handler { if (!handler.isDestroyed()) { handler.destroy(); } }); handlerList.length 0; // 3. 销毁场景顺序不能反 if (!viewer.scene.isDestroyed()) { viewer.scene.destroy(); } viewer.destroy(); }这里的核心顺序是先移除所有业务层的数据源和实体再释放场景中的 primitive最后销毁 viewer。如果你先执行viewer.destroy()再去操作viewer.scene就会抛出DeveloperError: This object was destroyed这在 Vue 路由切换时很常见。还有一个隐藏问题React 18 StrictMode 在开发环境会故意执行两次 effect 挂载和卸载如果你在挂载时创建 viewer、在卸载时销毁第二次挂载会新建一个新的 viewer但如果第一次的 canvas 没有从 DOM 上移除容器会被占用导致第二次new Cesium.Viewer报错。解决办法是在卸载函数里同时清除容器内的 canvas 节点或者给容器加唯一 key。3. 在 CesiumViewer 上挂业务数据从 3DTiles 到热力图数据是三维地球存在的意义否则一个空地球和一个空 div 没有区别。CesiumViewer 提供了一整套数据抽象Entity适合少量、常驻对象Primitive适合大量几何体Cesium3DTileset适合城市级倾斜模型。这一章围绕实际项目中最高频的几种数据接入方法展开。3.1 3DTiles 加载与单体化选对 Cesium3DTileset 的 style 和 pick3DTiles 是 Cesium 生态里最重要的三维数据格式倾斜摄影、BIM、点云都可以通过它承载。加载方式在新版本中已经支持 Promise 风格。export async function loadTileset(viewer, url, options {}) { const tileset await Cesium.Cesium3DTileset.fromUrl(url, options); tileset.style new Cesium.Cesium3DTileStyle({ color: { conditions: [ // 按属性 height 做分级渲染 [${height} 100, rgba(255, 0, 0, 1)], [${height} 50, rgba(255, 165, 0, 1)], [true, rgba(0, 128, 128, 1)] ] } }); viewer.scene.primitives.add(tileset); try { await viewer.zoomTo(tileset); } catch (e) { console.warn(zoomTo failed, e); } return tileset; }这段代码的业务含义是把倾斜模型按建筑高度着色超过 100 米红色超过 50 米橙色其余青色。${height}是 3DTiles 属性表达式如果你在建模时没有写入height属性这个条件会一直不命中。可以用tileset.properties对象来看当前 tileset 有哪些属性比如tileset.properties.height是一个Cesium.Property取值时要用.getValue()。单体化是数字孪生系统里的高频需求鼠标点击一栋楼弹出楼栋信息。关键在于鼠标拾取的 API 选择。export function enableFeaturePick(viewer, callback) { const handler new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); handler.setInputAction((movement) { const picked viewer.scene.pick(movement.position); if (!Cesium.defined(picked)) return; if (picked.primitive instanceof Cesium.Cesium3DTileset || picked.primitive instanceof Cesium.Cesium3DTileFeature) { // 单体化的 feature 其实就是 Cesium3DTileFeature if (picked.feature picked.feature.getProperty) { const id picked.feature.getProperty(id); const name picked.feature.getProperty(name); callback({ id, name, feature: picked.feature, position: movement.position }); } } }, Cesium.ScreenSpaceEventType.LEFT_CLICK); return handler; }注意区分viewer.scene.pick和viewer.scene.drillPick。pick只返回最上层的一个对象drillPick会沿屏幕射线穿透多个对象返回数组用于处理玻璃幕墙后的建筑。单体化在绝大多数情况下用pick就够。另外Cesium3DTileFeature是从picked.primitive上取feature属性不是直接拿picked作为 feature这个细节容易踩坑。3.2 Entity 绘制矩形、标注与动态光照的取舍在实际业务里绘制矩形是选址分析的基础操作。Cesium 的EntityAPI 里rectangle.graphics可以直接接受地理坐标。export function drawRectangle(viewer, west, south, east, north) { return viewer.entities.add({ id: business-rect, rectangle: { coordinates: Cesium.Rectangle.fromDegrees(west, south, east, north), material: Cesium.Color.fromCssColorString(#00a8ff).withAlpha(0.4), outline: true, outlineColor: Cesium.Color.WHITE, height: 0 } }); }这里height: 0表示贴近地表如果不设置矩形可能悬浮在某个默认高度。如果你用的是GroundPrimitive矩形会贴在地形表面但Entity的rectangle不能直接贴地形除非设置classificationType。很多新手问“为什么我的矩形在山体中间”就是因为少了height或没有用Cesium.GroundPrimitive。动态光照是另一个容易产生误解的点。viewer.scene.globe.enableLighting true后地球表面的地形会被太阳光照射形成阴影但Entity的rectangle、polygon大多不参与光照计算它们使用的材质是直接写在片元着色器里的。所以如果你同时开光照和半透明矩形矩形颜色不会随着太阳方位变化视觉上就像贴纸浮在模型上。稳妥的方案是动态光照只适用于 3DTiles 和模型对于地块标注类的 UI 元素最好单独渲染到 canvas 上再叠加避免和光照冲突。3.3 常见数据格式的接入MVT、SVG、雷达与高程数据Cesium 官方并不支持直接加载 MVT 矢量瓦片因为 Cesium 的渲染核心是基于几何体和粒子而不是矢量切片渲染器。常见的方案是后端先把 MVT 转成 GeoJSON再交给Cesium.GeoJsonDataSource.load。如果确实需要前端解析可以使用vt-pbf库解码 MVT然后构造 GeoJSON。这里给一个几何思路import { GeoJsonDataSource } from cesium; export async function loadGeoJSON(viewer, url) { const dataSource await GeoJsonDataSource.load(url, { stroke: Cesium.Color.WHITE, fill: Cesium.Color.fromCssColorString(#1e90ff).withAlpha(0.3), strokeWidth: 2, clampToGround: true }); viewer.dataSources.add(dataSource); return dataSource; }clampToGround: true会把要素贴到地形表面但这个参数在部分版本里仅对 polygon 和 polyline 有效。如果你的 GeoJSON 是点在屋顶上还需要用entity.billboard的heightReference决定高度模式。SVG 的接入相对简单Cesium 的 billboard 可以接收 SVG URL但要注意跨域和大小。超过 100KB 的 SVG 在纹理上传时会明显卡顿建议压缩成 PNG 再传。雷达数据一般不是标准地理数据需要自己用Entity.ellipse模拟扫描范围设置stRotation随时间改变。高程数据指的是把 DEM 转成地形可以用viewer.terrainProvider await Cesium.createWorldTerrainAsync()在线地形离线场景下要准备 .terrain 格式文件。下面这个表总结了不同数据格式的接入方式和注意点。数据格式Cesium 原生支持推荐接入方式注意点MVT否转 GeoJSON 后加载3857 坐标需反算SVG是billboard 图片超过 100KB 建议压缩雷达数据否Entity.ellipse stRotation需要手动 requestRender高程数据是terrainProvider离线需准备 terrain 格式4. CesiumViewer 交互与坐标系从鼠标拾取到 3857 投影的坑一个三维地球的价值一半在数据一半在交互。CesiumViewer 的交互核心是ScreenSpaceEventHandler它把鼠标和触摸事件统一成 Cesium 自己的事件类型。但不少人在处理拾取和绘制时忽略了坐标系变换导致“画出来的矩形歪了”、“点选楼栋定位错误”。这一章从事件绑定开始最后落到 3857 与经纬度的转换。4.1 鼠标事件绑定拾取模型节点与地块别在 handler 里做重计算拾取模型节点的代码在 3.1 里已经出现过这里重点讲事件处理器里的性能约束。很多项目会在handler.setInputAction里直接进行复杂的空间计算例如求交、路径规划鼠标移动时要把所有点重新投影一次卡顿非常明显。正确做法是把 handler 里的事件处理做成“转发”模式先取得极简数据再用requestAnimationFrame批量处理。export function bindClick(viewer, onPick) { let pending null; const handler new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); handler.setInputAction((movement) { if (pending) return; pending requestAnimationFrame(() { pending null; const picked viewer.scene.pick(movement.position); if (!picked || !picked.primitive) return; // 这里只传 primitive 的 id不做重计算 onPick({ id: picked.id || picked.primitive.id, position: movement.position }); }); }, Cesium.ScreenSpaceEventType.LEFT_CLICK); return handler; }这里把movement.position直接传出去真正的拾取和属性读取放在下一帧的requestAnimationFrame里。即使鼠标快速连点也只会每帧处理一次避免堆积事件。另一个技巧是使用viewer.scene.postRender来做“相机停止后重新拾取”因为pick操作本身依赖渲染管线频繁 pick 会导致 CPU 和 GPU 繁忙等待反而造成卡顿。4.2 绘制矩形和鹰眼把 ScreenSpaceEventHandler 和相机同步结合绘制矩形的交互在 3.2 写过一个简单版本这里给出一个更完整的实现包含动态预览和回调export function drawRectangleOnMap(viewer, callback) { const handler new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); let downPosition null; const preview viewer.entities.add({ id: rect-preview }); handler.setInputAction((leftDown) { downPosition viewer.camera.pickEllipsoid(leftDown.position, viewer.scene.globe.ellipsoid); }, Cesium.ScreenSpaceEventType.LEFT_DOWN); handler.setInputAction((mouseMove) { if (!downPosition) return; const current viewer.camera.pickEllipsoid(mouseMove.endPosition, viewer.scene.globe.ellipsoid); if (!current) return; const rect Cesium.Rectangle.fromCartesianArray([downPosition, current]); preview.rectangle.coordinates rect; preview.rectangle.material Cesium.Color.YELLOW.withAlpha(0.3); }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); handler.setInputAction((leftUp) { if (!downPosition) return; const upWorld viewer.camera.pickEllipsoid(leftUp.position, viewer.scene.globe.ellipsoid); const rect Cesium.Rectangle.fromCartesianArray([downPosition, upWorld]); preview.rectangle.coordinates rect; callback(Cesium.Rectangle.toDegrees(rect)); downPosition null; }, Cesium.ScreenSpaceEventType.LEFT_UP); return handler; }这里的pickEllipsoid是沿屏幕射线和地球椭球体求交但如果相机在很低的高度或者地球背面可能返回 undefined所以判断if (!current)是必要的。要注意Rectangle.fromCartesianArray只能处理两个对角点如果鼠标拖动顺序是从右下到左上得到的矩形会包含一个反方向维度需要Cesium.Rectangle.union来合并。另外preview实体如果不清除下一次绘制时还会残留上一次的位置所以最好绘制完成时根据 id 移除。鹰眼小地图的做法和绘制矩形不同它本质上是另一个相机视角的实时预览。不要为鹰眼再创建一个Cesium.Viewer因为同一页面两个 Viewer 会共享 WebGL 上下文虽然可用但性能浪费严重。更优雅的方案是使用一个普通的 canvas在viewer.scene.postRender里把自己的相机参数映射到一个缩小比例的图形上。4.3 3857 数据“飘”的根因Web 墨卡托与经纬度的换算要放在加载前很多团队从 ArcGIS 切图或开源地图拿到的数据是 EPSG:3857 坐标系也就是 Web 墨卡托直接用Cesium.Cartesian3.fromDegrees转换会发现所有要素漂移到海面上。原因很简单Cesium 只理解 WGS84 经纬度而 3857 是投影平面坐标必须先把 3857 坐标反算成经纬度才能交给 Cesium 使用。// 3857 到 WGS84 的反算公式 export function fromWebMercator(x, y) { const lng (x / 20037508.34) * 180; const ot (y / 20037508.34) * 180; const lat 180 / Math.PI * (2 * Math.atan(Math.exp(ot * Math.PI / 180)) - Math.PI / 2); return { lng, lat }; }这里的 20037508.34 是 Web 墨卡托投影在世界范围内的最大横向距离x / 20037508.34 * 180把投影坐标线性映射到经度而纬度需要经过指数和反正切变换。如果你用proj4js可以直接定义源坐标系和目标坐标系避免手写公式。了解了这一层就能看透“cesium 加载 3857 坐标系数据总是飘”的完整解法。正确的加载流程是拿到原始数据 - 统一成经纬度 GCS_WGS_1984 - 再创建 entity 或 primitive。特别是 MVT 瓦片瓦片内的坐标是相对于瓦片左上角的像素偏移如果不做反算直接当成经纬度画面会碎成几百块。这也是为什么很多团队宁可让后端转 GeoJSON也不自己处理 MVT 的原因。下面这个表列出了常见交互事件和对应的坐标系处理方式。事件类型对应动作常用坐标系注意点LEFT_CLICK拾取 feature屏幕坐标 - 笛卡尔坐标用 drillPick 穿透MOUSE_MOVE绘制矩形屏幕坐标 - 椭球面坐标用 pickEllipsoidLEFT_UP结束绘制椭球面坐标 - 经纬度对角线顺序俯视旋转相机控制相机坐标系同步到鹰眼 canvas5. CesiumViewer 性能与崩溃排查滚动地球不再崩如果说初始化是让地球出现数据挂载是让地球有用那性能优化就是让地球在长时间使用中还能保持流畅。Cesium 本身是 WebGL 应用滚动地球导致浏览器崩溃大概率是 GPU 内存或者活动瓦片数量失控。这一章从崩溃开始再聊动态光照和天空盒最后给一个requestRenderMode的实践。5.1 3D 地球滚动崩溃Tile 释放、请求中断和浏览器上限Cesium 的地形和影像数据都是按四叉树切割成 Tile相机移动时新 Tile 进入视野旧 Tile 离开。如果短时间内来回拖动地球Cesium 会同时加载多个层级的 TileGPU 显存瞬间被占满浏览器就只能杀掉页面。解决思路有两个一是限制最大缓存量二是降低屏幕误差。// 限制地形瓦片缓存防止滚动时内存暴涨 viewer.scene.globe.tileCacheSize 200; // 调整屏幕空间误差降低瓦片加载精度 viewer.scene.globe.maximumScreenSpaceError 16;tileCacheSize控制的是地形瓦片默认情况下它可能达到几千对 4K 屏幕来说每张瓦片纹理可能 512x512几百张就是几百 MB。指定值上限后超出部分会被清出缓存代价是快速回看时重新请求瓦片网络会多花一点时间。滚动崩溃还有一个常被忽略的因素viewer.scene.requestRenderMode为false时Cesium 每帧都执行渲染管线即使地球没有变化也会持续加载摄像头范围内的瓦片。可以改成true并在viewer.camera.moveEnd事件里请求一次渲染这样减少不必要的瓦片解析。注意开启后要记得在相机移动过程中手动requestRender否则画面滞后。5.2 动态光照与天空盒用预计算和关闭不必要特效换帧率动态光照看起来很高端但它的运算量非常大。globe.enableLighting true时Cesium 会在着色器里计算太阳光与地形的夹角相当于每个顶点都要做一次光照方程。对城市级倾斜摄影来说这种光照开销完全可以避免因为建筑本身的纹理已经包含光影。国内很多数字孪生项目其实追求的是“亮”而不是“真”所以默认关闭光照是更稳妥的选择。天空盒是渲染背景用的立方体贴图Cesium 默认天空盒有 6 张纹理如果同时开启skyAtmosphereGPU 还要计算大气散射。在室内大屏场景这些背景大概率会被 UI 遮住直接关掉能省出不少帧率。建议按下面的配置调整特效开启时的开销关闭后的影响globe.enableLighting顶点着色器增加光照计算模型失去太阳阴影但贴图更清晰scene.skyBox.show false每帧渲染 6 面天空盒纹理背景变黑白可搭配透明 canvasscene.skyAtmosphere.show false大气散射计算地平线附近不再有蓝色渐变scene.fog.show false距离雾计算远距离物体轮廓更硬如果你确实需要动态光照建议用假光照替代把太阳方向固定在 3DTiles 的style里写一个自定义的color表达式模拟不同朝向面的明暗而不是让 GPU 做逐像素光照。这种预计算方案在城市白膜场景里效果很好。5.3 用 requestRenderMode 和场景状态管理隐藏掉看不见的渲染requestRenderMode的官方文档是英文的翻译成中文就是“只在需要时渲染”。这个参数和maximumRenderTimeChange配合可以指定当场景变化超过多少秒才重绘。const viewer new Cesium.Viewer(container, { requestRenderMode: true, maximumRenderTimeChange: 0.1 }); // 假设你在做雷达扫描动画 function animateRadar(radarEntity) { const rotation radarEntity.ellipse.stRotation.getValue(); radarEntity.ellipse.stRotation new Cesium.ConstantProperty(rotation 0.01); viewer.scene.requestRender(); requestAnimationFrame(animateRadar); }这里的requestRender()是手动标记场景需要重绘只能在动态数据变化时调用。如果你把这段代码忘掉雷达会扫描不动而且整个地球点哪里都没反应看起来像浏览器卡死。所以使用requestRenderMode的团队必须建立一套“场景状态管理器”所有会影响视觉的行为都通过它来触发重绘而不是直接改 Cesium 对象。这个模式也解释了互联网上一些“cesium 中文文档”之外的求助帖为什么我的 requestRenderMode 开了之后相机飞行动画只有最后一帧因为飞行动画是 Cesium 内部在更新相机但你没有监听camera.changed事件来调用requestRender。解决方式是在创建 viewer 后给camera.percentageChanged设一个 0.01 的阈值并监听camera.changed在里面调用viewer.scene.requestRender()。6. 进阶把 CesiumViewer 封装成可恢复的 React 组件到这里你已经能创建、挂数据、处理交互、配置性能。最后要做的是把这些能力收敛到一个组件边界里让 React 生态和 Cesium 生命周期不再互相打架。这里以 React 为例Vue 的做法类似。6.1 组件接口设计viewer 实例放在 ref而不是 stateimport { useEffect, useRef } from react; import * as Cesium from cesium; function CesiumViewer({ onReady, options }) { const containerRef useRef(null); const viewerRef useRef(null); useEffect(() { if (viewerRef.current !viewerRef.current.isDestroyed()) { return; } const viewer new Cesium.Viewer(containerRef.current, { animation: false, timeline: false, ...options, }); viewerRef.current viewer; onReady?.(viewer); return () { viewer.scene.destroy(); viewer.destroy(); viewerRef.current null; }; }, []); return div ref{containerRef} style{{ width: 100%, height: 100% }} /; }viewer不应该放进useState因为 Cesium 的 viewer 是一个长期存在的可变更对象放进 state 会导致 React 重复渲染同时每次渲染都引用同一个对象没有任何数据流价值。ref能直接持有实例且不会触发渲染。外部组件通过onReady拿到 viewer 后建议也存入自己的 ref而不是 state。6.2 热重载与路由切换后的恢复技巧开发环境下React Fast Refresh 会在保存代码后重新挂载组件但 Cesium 的 WebGL context 不能通过 HMR 热替换。如果你发现每次保存后地球白屏说明 viewer 的 canvas 节点被 React 保留但 WebGL context 已经被销毁。这时在组件卸载里除了destroy还要把容器内的 canvas 移除干净。useEffect(() { const canvas containerRef.current.querySelector(canvas); if (canvas) { canvas.remove(); } }, []);路由切换的恢复则相对简单在卸载前保存相机状态重新挂载后再还原。function saveCamera(viewer) { const camera viewer.camera; sessionStorage.setItem(cesium-camera, JSON.stringify({ longitude: camera.positionCartographic.longitude, latitude: camera.positionCartographic.latitude, height: camera.positionCartographic.height, heading: camera.heading, pitch: camera.pitch, roll: camera.roll })); }最后组件上线前对照几个检查点容器高度是否为 03857 数据是否在加载前完成重投影观察requestRenderMode下动态效果是否在刷帧以及ScreenSpaceEventHandler是否在销毁时被回收。这四个点也是团队评审 Cesium 代码时最常看的地方单项不通过时先把对应环节的代码抽出来单独跑一遍。本文还有配套的精品资源点击获取
返回列表