第六篇:告别 setInputAction_XXX!我们给地球装上“事件总线”
📘 专栏说明
本专栏旨在手把手带你从零开始,基于开源三维地球引擎 Cesium 封装一套功能完善、可复用的 WebGIS 增强型 SDK。内容涵盖核心封装思路、关键代码实现、常用 GIS 功能抽象,以及基于该 SDK 构建的 UI 组件库开发。如果你更关注结果而非实现过程,也可直接使用已发布的成果:
🌟 GitHub仓库 📦 NPM 包 ✨ 公众号:经纬码客(欢迎关注)
💡 建议:即便你打算直接使用 SDK,也推荐订阅本专栏 -- 理解设计思路,才能更灵活地扩展属于你自己的专属 GIS 能力!
由于作者需兼顾全职工作,更新主要安排在晚间或节假日,无法保证高频发布,但会持续迭代,直至 SDK 达到实际项目落地标准。届时将完整开源所有源码,供学习与商用(遵循许可证协议)。
大家好,我是 Cesium 酱(也可以叫我“本猿”),一名在 WebGIS 领域摸爬滚打多年的前端开发者。前几期,我们封装了 Viewer,加了帧率、截图、底图控制。
但还有一个痛点没解决:事件处理。
Cesium 原生写法是这样的:
handler.setInputAction((movement) => {
console.log('左键点击:', movement.position)
}, Cesium.ScreenSpaceEventType.LEFT_CLICK)
问题很明显:
- 事件类型是魔法数字(
LEFT_CLICK) - 不支持同时注册多个回调
- 移除监听很麻烦
- 没有 TypeScript 类型提示
今天,我们就亲手解决它!
目标:实现一个 EventEmitter 类,让事件绑定像这样:
viewer.EventHandler.on('leftClick', (e) => { ... })
viewer.EventHandler.off('leftClick', callback)
准备好了吗?打开你的编辑器,我们从零开始!
🧱 第一步:定义事件名称映射表(语义化)
首先,在 src/utils/DefineObject.ts 中,创建一个人类友好的事件名到 Cesium 原生类型的映射:
// src/utils/DefineObject.ts
import { ScreenSpaceEventType } from "cesium"
/**
* Cesium 事件类型映射表
* 将原生常量转为字符串键名,便于记忆和使用
*/
export const EventNameMap = {
leftDown: ScreenSpaceEventType.LEFT_DOWN,
leftUp: ScreenSpaceEventType.LEFT_UP,
leftClick: ScreenSpaceEventType.LEFT_CLICK,
leftDblClick: ScreenSpaceEventType.LEFT_DOUBLE_CLICK,
rightDown: ScreenSpaceEventType.RIGHT_DOWN,
rightUp: ScreenSpaceEventType.RIGHT_UP,
rightClick: ScreenSpaceEventType.RIGHT_CLICK,
middleDown: ScreenSpaceEventType.MIDDLE_DOWN,
middleUp: ScreenSpaceEventType.MIDDLE_UP,
middleClick: ScreenSpaceEventType.MIDDLE_CLICK,
mouseMove: ScreenSpaceEventType.MOUSE_MOVE,
wheel: ScreenSpace.EventType.WHEEL,
pinchStart: ScreenSpaceEventType.PINCH_START,
pinchEnd: ScreenSpaceEventType.PINCH_END,
pinchMove: ScreenSpaceEventType.PINCH_MOVE,
}
✅ 现在,'leftClick' 比 ScreenSpaceEventType.LEFT_CLICK 好记多了!
📦 第二步:定义 TypeScript 类型(安全第一)
在 src/types/index.ts 中,声明可用的事件类型和回调存储结构:
// src/types/index.ts
import { EventNameMap } from "../utils/DefineObject"
/** 可用的事件类型(即 EventNameMap 的所有 key) */
export type EventType = keyof typeof EventNameMap
/** 事件回调列表:每个事件名对应一个函数数组 */
export type Events = Map<EventType, Function[]>
💡
EventType 会自动推导出 'leftClick' | 'rightClick' | ...联合类型这样你在写
on('xxx')时,输错事件名会直接报错!
⚡ 第三步:实现 EventEmitter 核心类
新建 src/core/EventEmitter.ts,这是今天的主角:
// src/core/EventEmitter.ts
import { ScreenSpaceEventHandler, Viewer } from "cesium"
import { Events, EventType } from "../types"
import { EventNameMap } from "../utils/DefineObject"
class EventEmitter {
/** 原生 Cesium 事件处理器 */
private handler = new ScreenSpaceEventHandler(this.viewer.canvas)
/** 存储所有事件及其回调函数 */
private events: Events = new Map()
constructor(public viewer: Viewer) {}
/**
* 绑定事件回调
* @param eventName 事件名称,如 'leftClick'
* @param callback 回调函数
*/
on(eventName: EventType, callback: Function): void {
// 如果该事件还没注册过,先向 Cesium 注册原生监听
if (!this.events.has(eventName)) {
this.events.set(eventName, [])
// 当事件触发时,遍历所有回调并执行
this.handler.setInputAction((...args: any[]) => {
const callbacks = this.events.get(eventName)
callbacks?.forEach(cb => cb(...args))
}, EventNameMap[eventName])
}
// 将新回调加入列表
this.events.get(eventName)!.push(callback)
}
/**
* 移除事件回调
* @param eventName 事件名称
* @param callback 要移除的特定回调(不传则移除全部)
*/
off(eventName: EventType, callback?: Function): void {
if (!this.events.has(eventName)) return
if (callback) {
const callbacks = this.events.get(eventName)!
const index = callbacks.indexOf(callback)
if (index !== -1) {
callbacks.splice(index, 1)
}
} else {
// 移除该事件所有回调
this.events.delete(eventName)
}
}
/** 清空所有事件监听 */
clear(): void {
this.events.clear()
this.handler.destroy() // 销毁原生处理器,避免内存泄漏
}
}
export default EventEmitter
✅ 关键设计亮点:
- 支持同一事件注册多个回调
- 自动管理原生
ScreenSpaceEventHandler生命周期 - 类型安全:事件名写错?TS 直接报错!
🔌 第四步:把 EventEmitter 绑定到 Viewer
回到 src/core/Viewer.ts,在构造函数中初始化它:
// src/core/Viewer.ts
import EventEmitter from "./EventEmitter"
export class Viewer extends Cesium.Viewer {
/**
* Cesium事件发射器实例
* @type {EventEmitter}
*/
public EventHandler: EventEmitter = new EventEmitter(this)
constructor(container: Element | string, options?: Option) {
super(container, { /* ... */ })
this.initBaseConfig()
// 注意:EventHandler 已在属性初始化阶段创建,无需额外调用
}
}
✨ 由于 public EventHandler = new EventEmitter(this) 是类属性初始化,
它会在 super() 之后、constructor 函数体之前执行,此时 this 已指向正确实例
🧪 第五步:本地测试 —— 试试新事件系统!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>002-鼠标事件</title>
<script src="./Cesium/Cesium.js"></script>
<link rel="stylesheet" href="./Cesium/Widgets/widgets.css" />
<script src="./lib/arc3dlab.umd.js"></script>
<script src="./assests/dat.gui.min.js"></script>
<style>
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
#cesiumContainer {
width: 100%;
height: 100%;
position: relative;
}
#gui-box {
position: absolute;
left: 5px;
top: 5px;
z-index: 99;
}
</style>
</head>
<body>
<div id="cesiumContainer">
<div id="gui-box"></div>
</div>
<script>
const viewer = new Arc3DLab.Viewer("cesiumContainer", {
fpsShow: true,
mapboxController: true,
})
function startMouseEvent() {
const handler = viewer.EventHandler
handler.on("leftDown", (e) => console.log("鼠标左键摁下:", e))
handler.on("leftUp", (e) => console.log("鼠标左键抬起:", e))
handler.on("leftClick", (e) => console.log("鼠标左键单击:", e))
handler.on("leftDblClick", (e) => console.log("鼠标左键双击:", e))
handler.on("rightDown", (e) => console.log("鼠标右键摁下:", e))
handler.on("rightUp", (e) => console.log("鼠标右键抬起:", e))
handler.on("rightClick", (e) => console.log("鼠标右键单击:", e))
handler.on("middleDown", (e) => console.log("鼠标中键摁下:", e))
handler.on("middleUp", (e) => console.log("鼠标中键抬起:", e))
handler.on("middleClick", (e) => console.log("鼠标中键单击:", e))
handler.on("mouseMove", (e) => console.log("鼠标移动:", e))
handler.on("wheel", (e) => console.log("鼠标滚轮:", e))
handler.on("pinchStart", (e) => console.log("两指触摸开始:", e))
handler.on("pinchEnd", (e) => console.log("两指触摸结束:", e))
handler.on("pinchMove", (e) => console.log("两指移动:", e))
}
function endMouseEvent() {
viewer.EventHandler.clear()
}
initGui()
function initGui() {
const params = { message: "GUI面板-鼠标事件", bool: false }
const gui = new dat.GUI({ autoPlace: false })
const customContainer = document.getElementById("gui-box")
customContainer.appendChild(gui.domElement)
gui.add(params, "message")
const boolEvent = gui.add(params, "bool")
boolEvent.onFinishChange((val) => {
val ? startMouseEvent() : endMouseEvent()
})
}
</script>
</body>
</html>
刷新页面,点击地球——
✅ 控制台输出鼠标响应!
✅ 代码简洁、语义清晰!
❤️ 写在最后
今天我们没有依赖任何第三方库,
只是用 TypeScript + Cesium 原生能力,
就实现了一个类型安全、多回调支持、易于管理的事件系统。
这正是 Arc3DLab 的初衷:
不重复造轮子,但要把轮子装得更稳、更好用。
而这个 EventEmitter 还可以继续进化:
- 支持
once()一次性监听? - 添加
hasListener(eventName)查询? - 集成 RxJS 流式处理?
你的想法,就是下一行代码的方向。
欢迎来 GitHub 提 Issue 或 PR!
让我们一起,把它打磨成你真正想用的工具。
本专栏旨在手把手带你从零开始,基于开源三维地球引擎 **Cesium** 封装一套功能完善、可复用的 **WebGIS 增强型 SDK**。内容涵盖核心封装思路、关键代码实现、常用 GIS 功能抽象,以及基于该 SDK 构建的 UI 组件库开发。
浙公网安备 33010602011771号