这个项目我 8 月就见过------当时它叫 God's Eye View,登了 GitHub Trending 日榜和周榜双第一,作者是拍"上帝视角"视频出身的 Bilawal Sidhu,那套视频在 YouTube 上五百多万播放。当时我以为就是个视觉玩具:浏览器里开一个军用 HUD 风格的 3D 地球,配 OpenAI 语音,看着唬人。
今天早上看Trending,它还在榜上,单日又涨了 1895 颗星,总量 5.4 万。一个上线两个多月的项目热度这样持续,有点不寻常,我就把 README 认真读了一遍。真正让我决定花一个下午的,是两句承诺:一句是 "A spy satellite simulator in your browser, except the data is real" ,另一句更具体------20 类实时图层里,17 类完全不需要 API key。 这话要么是真的,要么就是营销话术。好在它整个项目是 MIT 开源,数据源全写在 DATA_SOURCES.md 里,每一层都能自己验证。我就这么干了。
Node 22 装得上,doctor 先给你一巴掌
我的沙盒是最低配:1 核 2G,Node 22.23.2。clone 下来先看 package.json,engines 写得很死:
json
"engines": {
"node": ">=24.14.0 <25 || >=26 <27"
}
22 不在支持列表里。不过 npm 对 engines 默认只警告不拦路,npm install --registry=https://registry.npmmirror.com 走国内镜像,173 个包 43 秒装完,中途只在 @meri-imperiumi/eccodes-wasm(要求 Node 24+)上报了 EBADENGINE 警告。 装完我按习惯跑它自带的体检脚本 npm run doctor,这个脚本会把你各个图层的数据源状态挨个列出来。结果第一行就是:
vbnet
[ERROR] Node 22.23.2: too old; install Node 24.14 or newer
[OK] npm 10.9.8
[OK] dependencies installed
Map: Esri World Imagery (keyless satellite basemap) with keyless terrain
Flights: OpenSky keyless anonymous access (rate-limited)
Vessels: off until an AISStream key is added
Fires: off until a FIRMS key is added
Missions: Launch Library 2 public access
按这个输出,我的环境"需要处理才能启动"。我本来都准备去装 Node 26 了,转念一想------engines 是给用户装 Pinokio 一键包看的那种约束,vite 自己未必管这个。直接 npm run dev 试试:
arduino
VITE v6.4.3 ready in 786 ms
➜ Local: http://localhost:4173/
786 毫秒就绪,页面 HTTP 200 正常返回,dev server 稳定吃 433MB 内存------在 2G 的机器上占了三分之一,不算温柔但也没崩。所以结论是:Node 22 能跑起来,但 eccodes-wasm(浏览器里解气象 GRIB2 文件用的)这类要求 Node 24 的依赖,在构建链路外可能有暗坑,正经部署还是老老实实上 24 以上。doctor 说得没错,只是没它说的那么致命。
三个 curl,把"免 key"验了个底朝天
接下来是重头戏:README 说的 17 层免 key,我挑了最有代表性的三层,直接用 curl 打它们的上游。
卫星轨道,CelesTrak:
rust
curl 'https://celestrak.org/NORAD/elements/gp.php?GROUP=stations&FORMAT=tle'
HTTP 200,1.66 秒,3024 字节,返回的就是标准的两行轨道根数(TLE),第一组就是国际空间站,历元是 2026 年第 285 天------昨天的数据,够新鲜。这个源没有任何鉴权,连注册都不用。
实时航班,OpenSky 匿名接口:
rust
curl 'https://opensky-network.org/api/states/all?lamin=25&lomin=115&lamax=45&lomax=135'
HTTP 200,0.9 秒,42KB。返回的 JSON 里是一架架真实飞机:ICAO 地址 87455c,呼号 ANA251,注册国 Japan,位置北纬 33.6 东经 130.4,地速 129 米每秒------就是全日空从福冈附近飞过的那架。没有 key,只有匿名的频率限制。 地震,USGS 就不贴输出了,all_day.geojson,一秒出头,172KB,全球 24 小时地震目录,公共领域数据。
三层全通。加上 README 里明说的 Esri 卫星底图、NASA 山火周界、Launch Library 2 发射任务(匿名限 15 次/小时)、十几个国家的交通摄像头目录------"免 key 能看到半个地球的实时态势"这句宣传,经得起验证。真正要 key 的是三类:Google 3D 城市(写实瓦片)、OpenAI 语音、AISStream 船舶这类有配额或鉴权设计的源,而且它们全走服务端代理,浏览器拿不到 key 本身。
数据是真的,那轨道算得对吗
数据源真还不算完------拿到 TLE 之后,得靠 SGP4 算法把两行根数外推成某一时刻的卫星位置,这一步算错了,地球上的卫星就是错的。项目用的是 satellite.js 6.0.2,我干脆用它 + 刚才拉的真 TLE,手动算一次 ISS 的当前位置:
javascript
import { twoline2satrec, propagate, gstime, eciToGeodetic } from 'satellite.js';
const rec = twoline2satrec(tle1, tle2); // CelesTrak 刚拉回来的
const pv = propagate(rec, new Date());
const geo = eciToGeodetic(pv.position, gstime(new Date()));
// 输出:
// 纬度 20.39°,经度 6.18°
// 高度 423.5 km,速度 7.659 km/s
423.5 公里------ISS 就该在 400 公里出头的轨道上;7.659km/s 也对得上第一宇宙速度量级;从 TLE 里的平均运动 15.4883 圈/天换算,轨道周期 92.97 分钟,教科书数字。中间还有个小插曲:我第一版脚本把高度又除了个 1000,算出"0.4 公里",差点以为是 TLE 有诈------查了半天发现是我自己的单位换算错了,eciToGeodetic 返回的高度本来就是公里。工具没错,错的是我。
顺带说一句,项目对"轨道环跟着卫星走"这事有个挺讲究的处理。SGP4 算出来的是惯性系位置,画轨道环的时候如果不管地球自转,环和卫星几分钟就错开了。它的做法在 src/layers/satellites/orbits.js 里:轨道环烘焙时记下当时的格林尼治恒星时,显示时用时间差做一个绕 Z 轴的刚体旋转补偿:
ini
const deltaGmst = gstime(nowDate) - gmstAtBake;
const rotation = Cesium.Matrix3.fromRotationZ(-deltaGmst, ...);
环是预计算的(省性能),但靠这一个旋转量始终锁死在卫星脚下。README 里那句 "no drift, no per-second flicker" 是有对应实现的,不是文案。
源码里最有价值的一段:它替你省 API 配额
数据链路验完,我本来准备收工了,结果在 server/providers/aircraft/opensky.js 里看到一段注释,比功能本身有意思多了。 背景是这样:OpenSky 匿名接口有频率限制,认证后按天给配额,全局快照一次要花 4 个 credit,一天大约 4000 个。而这个项目的地图端每 30 秒轮询一次航班------注释里写了真实发生的事故:有部署者把应用开一整天,8 小时把当天配额烧光,然后图层硬死了 48 小时(OpenSky 超限后的封禁期),"rate limited for 48h",owner report 原话,鉴权本身没有任何问题,纯粹是烧太快。 它的修复是三层的,核心思路是把"缓存多久"变成一个随剩余预算伸缩的量:
javascript
// >2400 credits 剩余 → 30s TTL,全天最多 ~3h 新鲜度损失
// 再往下 → 90s(约省出 5h),再往下 → 300s(约省出 8h)
// 全天连续使用 18+ 小时配额也烧不完
function openskyAdaptiveTtlMs(remaining) { ... }
OpenSky 每次响应都带 X-Rate-Limit-Remaining 头,代理读到剩余量,预算越薄就把缓存 TTL 拉得越长。配额充裕时数据 30 秒一刷,快见底时宁可让数据变旧也不让图层死。更绝的是降级姿态:就算上游彻底 429 了,它也不返回错误,而是把最后一次成功的航班数据继续吐给前端,只额外塞一个 X-OpenSky-Stale-Seconds 头告诉客户端"这批数据已经旧了多少秒"。源码注释里那句 "last-good planes beat a dead layer"------旧数据好过死图层------我觉得很值得抄回自己的项目里。
同类的设计散布在整条服务端:CelesTrak 的 TLE 走内存+磁盘双层缓存(注释里说以前每次 dev 热重载都把所有分组重新拉一遍,被 CelesTrak 限流过才加的);带 key 的源全部走服务端代理,且有同源校验(Sec-Fetch-Site、反向代理头都查,纯函数实现带单测)。这个项目 5.4 万星,我原以为星数是给那身军事 HUD 皮肤的,读完这部分源码我改主意了------皮肤只是门面,值钱的是这套"怎么让免费公开数据长期稳定地喂一个重前端"的工程治理。
没验到的部分
得说清楚:我的沙盒没有 GPU 也没有显示器,CesiumJS 的 WebGL 渲染这条线我完全没看到------那身 FLIR 热成像、NVG 夜视的 GLSL 效果, cockpit 视角,全是 README 和官方 GIF 里的说法,我无法背书。我验证的是数据链路:源可免 key 直连、轨道计算物理正确、配额治理有真实事故和对应修复。3D 画面是不是像视频里那么帅,得你自己找台像样的机器跑。
就写到这吧。项目在 GitHub 搜 gods-eye-view 就有,Node 版本先看一眼 engines,别学我。