这个开源项目把飞机轨迹、野火、实时股价全塞进一个 WebGL 地球

做实时数据可视化的时候,最头疼的往往不是渲染——而是数据接入:多个 API 频率不同、协议不同(REST / WebSocket / SSE),怎么统一管理?新增一个数据源要不要改核心逻辑?

Crucix 是一个开源的全球情报仪表板,接入了 27 个公开数据源。

GitHub:https://github.com/calesthio/Crucix (opens in a new tab)

1. Globe.gl:比 CesiumJS 更轻量的 WebGL 地球

Crucix 的 3D 地球用的是 Globe.gl (opens in a new tab),不是 CesiumJS。

两者定位不同:

Globe.glCesiumJS
底层Three.js 封装自研渲染引擎
包体积轻量较重(地形/影像加载)
上手难度低,API 简单高,概念多
适合场景数据点/连线/热力图专业 GIS、精确地形
大规模对象一般强(Primitive API)

如果你只是想在地球上显示数据点、飞行路径、热力区域,Globe.gl 是更省力的选择。核心 API 极简:

import Globe from 'globe.gl';
 
const globe = Globe()
  .globeImageUrl('//unpkg.com/three-globe/example/img/earth-night.jpg')
  .pointsData(flightData)
  .pointColor(() => 'orange')
  .pointAltitude('altitude')(document.getElementById('globe'));

学习价值:如果你一直觉得 CesiumJS 太重、入门太慢,Globe.gl 是很好的起点,Crucix 的源码里有完整的真实用法。

2. 模块化数据源设计:可插拔架构的参考实现

Crucix 最值得参考的是它的架构设计——每个数据源是一个独立模块,核心系统不感知数据来自哪里。

目录结构大概是这样:

src/
  modules/
    aviation/        # OpenSky Network 航班数据
    maritime/        # AIS 海事数据
    wildfire/        # NASA FIRMS 野火数据
    radiation/       # 辐射监测数据
    economics/       # FRED/财政部/BLS
    satellite/       # 卫星轨道
    finance/         # Yahoo Finance
    ...(共27个模块)

每个模块实现相同的接口:

// 每个模块导出标准结构
export default {
  name: 'aviation',
  fetchInterval: 5000,        // 刷新频率(各模块不同)
  async fetch() {             // 数据获取逻辑
    const res = await fetch(OPENSKY_API);
    return normalize(res);   // 统一转换为标准格式
  }
}

核心调度器只负责按频率调用 fetch(),把结果推送给前端——完全不关心具体数据源的实现细节。

学习价值:这是"开闭原则"的真实应用。新增一个数据源只需新建一个模块文件,不改任何已有代码。自己做多数据源聚合项目时可以直接参考这个设计。

3. SSE 实时推送:比 WebSocket 更简单的单向实时方案

很多人遇到"需要实时更新"就直接上 WebSocket,但 Crucix 用的是 SSE(Server-Sent Events)

两者的核心差别:

SSEWebSocket
通信方向服务端 → 客户端(单向)双向
协议普通 HTTPws:// 协议升级
实现复杂度
断线重连浏览器自动处理需要手动实现
适合场景实时推送数据更新需要客户端上行数据

情报仪表板只需要服务端持续推数据给浏览器,SSE 完全够用,代码也更简洁:

// 服务端(Express)
app.get('/stream', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
 
  const send = (data) => res.write(`data: ${JSON.stringify(data)}\n\n`);
 
  const interval = setInterval(() => {
    send(getLatestData());
  }, 1000);
 
  req.on('close', () => clearInterval(interval));
});
 
// 客户端
const es = new EventSource('/stream');
es.onmessage = (e) => updateGlobe(JSON.parse(e.data));

学习价值:下次遇到"页面需要实时更新"的需求,先判断是否需要双向通信——如果只是服务端推数据,SSE 比 WebSocket 少写很多代码。

4. 三级告警系统:FLASH / PRIORITY / ROUTINE

Crucix 对接入的数据做了告警分级,根据异常程度自动触发不同级别:

  • FLASH:最高优先级,异常事件(如辐射超标、大规模冲突)
  • PRIORITY:中等优先级,值得关注但不紧急
  • ROUTINE:日常数据更新通知

这套设计值得参考的地方是:告警逻辑写在每个数据模块里,而不是集中在一个判断函数里。各模块自己知道什么情况该触发什么级别,核心系统只负责路由和展示。

同时支持推送到 Telegram 和 Discord Bot,可以用命令查询:

/alerts       # 查看当前活跃告警
/wildfire     # 查询野火状态
/aviation     # 查询航班异常

从哪里开始读代码?

如果你想把 Crucix 作为学习素材:

  1. 先看 src/modules/ 下任意一个模块,理解数据源的标准接口
  2. 看主调度器,理解多模块如何统一调度
  3. 看 Globe.gl 相关代码,理解数据如何映射到 3D 地球上
  4. 看 SSE 推送部分,作为实时推送的实现参考

本地跑起来只需要:

git clone https://github.com/calesthio/Crucix.git
cd Crucix && cp .env.example .env
npm install && npm run dev
# 打开 http://localhost:3117

大多数 Tier 1 数据源(航班、野火、海事)有免费公开 API,不配置 Key 也能看到效果。