10分钟上手 CesiumJS 自定义 Widget:把业务面板装进三维地球
【免费下载链接】cesiumAn open-source JavaScript library for world-class 3D globes and maps :earth_americas:项目地址: https://gitcode.com/GitHub_Trending/ce/cesium
官方时间轴控件只能显示时间刻度,塞不进"设备在线数""任务进度"这类业务字段。CesiumJS 的默认界面(时间轴、图层选择器、场景模式切换按钮)全部由 Widget 组件拼装而成,源码就放在 packages/widgets/Source/,每个 Widget 一个独立文件夹。这意味着你可以用同一套思路,把自家业务面板做成规范的界面扩展。
5分钟搭出第一个自定义 Widget 🧱
一条最短路径:引入资源 → 占个容器 → 写组件类 → 挂进 Viewer。
<!-- 1. 引入构建产物(本地 Build 目录或 node_modules 均可) --> <script src="Cesium.js"></script> <link rel="stylesheet" href="Widgets/widgets.css" /> <!-- 2. 画布 + 自定义 Widget 容器 --> <div id="cesiumContainer"></div> <aside id="taskPanel"></aside>组件本身就是一个普通类,构造时接收容器,内部管理自己的 DOM 和事件:
class TaskPanel { constructor(container, viewer) { this._viewer = viewer; container.innerHTML = '<button id="addMarker" class="cesium-button">打点</button>'; container.querySelector('#addMarker').addEventListener( 'click', () => viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(116.4, 39.9), point: { pixelSize: 12, color: Cesium.Color.YELLOW } }) ); } }挂进应用有两种方式。轻量场景下,实例化即可:
const viewer = new Cesium.Viewer('cesiumContainer'); new TaskPanel(document.getElementById('taskPanel'), viewer);如果你想让"能力"而不是"界面"进入 Viewer(类似官方的Cesium.viewerDragDropMixin),用 Viewer.prototype.extend 注册 mixin。它往 Viewer 原型上挂方法,适合跨组件共享行为;而像按钮面板这种独立 UI,直接实例化更清晰,两者别混用。
Widget 生命周期:出生 → 上班 → 退休 ⚙️
把 Widget 想成一个员工,三个阶段各有分工。
- 出生(constructor):建 DOM、绑事件。只干一次的事都放这里,别放到帧回调里重复执行。
- 上班(update):需要在每帧刷新数据的 Widget(比如跟随相机显示坐标读数),订阅 Scene 的 postRender 事件。场景每渲染完一帧就
raiseEvent一次,你挂在上面就能拿到最新time参数。 - 退休(destroy):反序拆掉一切。
// 上班:帧循环 this._tickId = viewer.scene.postRender.addEventListener((time) => { this._refreshReadout(time); }); // 退休:清理 destroy() { this._tickId.unsubscribe(); this._root.innerHTML = ''; }组件之间通信走事件总线。Cesium 内置的 Event 类 就是现成的:addEventListener订阅、raiseEvent发布、unsubscribe退订。A 组件发布"数据就绪",B 组件订阅后刷新面板——谁也不 import 谁,将来拆模块不心疼。
排坑指南:踩过才信的四个坑 ⚠️
问题 1:面板数据每秒刷几十次,画面却开始卡原因:每帧直接改textContent/样式,浏览器反复触发重排。 对策:值没变就不写 DOM;列表类更新先拼到DocumentFragment里再一次性挂进去。
问题 2:图层列表有 200 项,绑了 200 个 click 监听原因:给每个子节点单独addEventListener。 对策:事件委托——只在父容器绑一次,用event.target.closest('button')判断点了谁。
问题 3:反复打开关闭面板,内存曲线只涨不跌原因:destroy里忘了退订监听,闭包把整个组件实例钉在了内存上。 对策:所有引用(监听器、Event 订阅、定时器)存到this._xxx,销毁时逐个释放。
问题 4:自定义面板把 Cesium 内置控件的样式盖花了原因:自己写了个.panel-btn,和widgets.css里的规则选择器撞车。 对策:给所有自定义类名加前缀命名空间(如.biz-task-panel),尽量不裸选button、div。
项目变大之后,目录怎么分层 📁
组件从 3 个涨到 30 个,散落在index.html里的<script>会变成灾难。建议按职责拆层:
src/ ├── widgets/ # 业务 Widget,一个文件夹一个组件(对齐官方结构) ├── controls/ # 复用的基础控件:下拉、开关、滑杆 ├── events/ # 事件总线:统一的事件名注册与分发 ├── utils/ # 工具函数 └── main.js # 入口:创建 Viewer,实例化各 Widget官方 Widget 的组织方式值得照抄:一个 Widget = 一个目录,JS 逻辑加同名 CSS 放一起,最后由 widgets.css 统一聚合。这样"加一个功能"永远只是"加一个文件夹"。组件数量上来后,回归测试也得跟上,Cesium 自己的单元测试跑起来是这样的:
官方还有 CodingGuide 定义了命名、注释和 API 设计约定,团队开发前读一遍,能省掉大量 review 返工。
你可能想问 🤔
控件想固定在画布右上角,怎么定位?给容器加position: absolute,复用cesium-widget系类名继承字体和层级,right/top贴边即可——这也是官方 Toolbar 的做法。
窗口缩放后控件错位?监听窗口resize,在里面重算依赖画布尺寸的布局,别只在初始化时算一次。
按钮文案要支持多语言?组件内部不要硬编码文案,统一走资源字符串表(可接 i18n 方案),构造时注入 locale。
想找个现成参照物?翻 Sandcastle 示例库,从 hello-world 到各种交互场景,每个例子都展示了一个完整的 Viewer 装配过程。
把 Widget 理解成"有生命周期的独立 UI 单元":出生建 DOM、上班订阅帧事件、退休清监听,通信走事件总线——四个动作守住,面板再多也不会失控。
延伸资源:
- Widget 源码全集:packages/widgets/Source/
- 开发规范:Documentation/Contributors/CodingGuide/README.md
- 场景示例库:packages/sandcastle/gallery/
【免费下载链接】cesiumAn open-source JavaScript library for world-class 3D globes and maps :earth_americas:项目地址: https://gitcode.com/GitHub_Trending/ce/cesium
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考