• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 众包
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录

arc3dlab

  • 博客园
  • 联系
  • 订阅
  • 管理

公告

View Post

第六篇:告别 setInputAction_XXX!我们给地球装上“事件总线”

第六篇:告别 setInputAction_XXX!我们给地球装上“事件总线” 本专栏旨在手把手带你从零开始,基于开源三维地球引擎 **Cesium** 封装一套功能完善、可复用的 **WebGIS 增强型 SDK**。内容涵盖核心封装思路、关键代码实现、常用 GIS 功能抽象,以及基于该 SDK 构建的 UI 组件库开发。

📘 专栏说明

本专栏旨在手把手带你从零开始,基于开源三维地球引擎 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!

让我们一起,把它打磨成你真正想用的工具。


🌟 项目开源,欢迎Star✨!

GitHub:https://github.com/jianlei-wang/Arc3DLab_SDK

NPM:https://www.npmjs.com/package/arc3dlab

posted on 2026-01-17 18:21  Arc3DLab  阅读(0)  评论(0)    收藏  举报

刷新页面返回顶部
 
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3