把旅行与居住足迹变成可交互的 3D 地球,嵌入博客、主页与简历。 技术标签:WebGL · MapLibre GL JS · Web Component · 独立产品
为什么做这个项目
“足迹可视化”并不是一个新需求。旅行博主想在文章末尾放一张“去过的地方”,开发者想在个人主页加一个“分布地图”,求职者想在简历里展示“团队覆盖的城市”——这些场景的共同特征是:只需要一个可嵌入的、轻量的、不折腾的 3D 地球,而不需要一套 GIS 系统。
但现有方案在“最后一公里”上并不顺手。CesiumJS 是完整的 3D 地球引擎,能力强大,代价是 4.8 MB 的原始包体(约 1.3 MB gzip),把它塞进一篇博客文章的侧边栏,成本太高。Mapbox GL JS 的 globe 投影体验很好,但需要申请 token,对于“贴一段代码就能用”的场景是一道额外的墙。静态截图或 2D 地图组件虽然轻,但失去了“可交互的 3D 地球”带来的空间感和分享欲。
footprint-studio 的切入点很具体:用 MapLibre GL JS 的原生 globe 投影,做一个无 token、隐私优先、核心包 gzip 小于 15 KB 的可嵌入地球组件。它不试图替代任何 GIS 工具,只解决“把个人足迹放进网页”这一件事。
技术选型与架构
语言、框架与关键依赖
项目是一个pnpm + Turborepo 管理的 TypeScript monorepo,核心运行时依赖只有 MapLibre GL JS。选择 MapLibre 而不是其他方案,有三层考虑。
第一,无 token 是硬约束。MapLibre GL JS 是 Mapbox GL JS 的开源 fork,不需要账号、不需要 access token,底图可以使用 OpenFreeMap 等免费矢量瓦片源。配置协议里style默认指向https://tiles.openfreemap.org/styles/positron,maplibreUrl默认从 jsDelivr 加载maplibre-gl@5,用户零配置即可跑起来。对于“嵌入博客”这个场景,少一步申请 key,转化率完全不同。
第二,globe 投影在 MapLibre v5 中已经足够可用。2024 年底发布的 v5.0 正式支持了 globe 投影和地形,MapLibre 团队随后持续优化了 globe 的瓦片剔除和渲染管线(例如“Render halo and glyph in a single pass”带来约 40% 的时间下降)。虽然 CesiumJS 的 3D 地球能力更深,但 MapLibre 的 globe 投影对“个人足迹”这个数据量级(≤1000 个标记)完全够用,而包体只有 CesiumJS 的五分之一左右。
第三,deck.gl 被排除在核心渲染之外。deck.gl 不是地图库,而是叠加在地图之上的 GPU 图层框架,适合大规模点云和分析图层。footprint-studio 的数据规模是“几十到上千个标记”,用 deck.gl 属于杀鸡用牛刀,还会引入额外的依赖负担。项目选择直接用 MapLibre 的 marker 和弹窗机制,把复杂度控制在最低。
在接入形态上,项目提供了三种路径:Web Component(Shadow DOM 样式隔离)、auto-init(一行 script 自动初始化)、iframe(零依赖宿主页),外加 NPM 包供构建工具使用。这不是功能堆砌,而是针对不同用户的分层设计:不会写代码的人用 iframe 或在线 Builder;写 Markdown 博客的人用 auto-init 贴一段 HTML;开发者用 NPM 包做深度集成。
核心模块与数据流
Monorepo 的包结构直接映射了职责边界:
packages/core(footprint-globe):渲染引擎。负责配置校验、数据解析(JSON/CSV/GeoJSON)、MapLibre 实例创建、globe 投影初始化、标记与图例渲染、隐私模式处理,以及 WebGL 不可用时的列表降级。packages/web-component:Web Component 包装层,提供<footprint-globe>自定义元素。核心包 README 提到 web-component 后来被合并进了footprint-globe,说明团队在迭代中发现“包装层”和“引擎层”的边界可以更薄。packages/builder:在线配置工具,用户粘贴城市列表、选主题、复制生成代码。packages/embed:iframe 嵌入页。packages/docs:文档站。
数据流可以概括为:配置对象(FootprintConfig)→ 校验与规范化 → 按数据格式解析为统一的Pin[]→ MapLibre source / layer / marker 渲染 → 交互事件(悬停、键盘聚焦、点击)。配置协议 v1 对数据施加了明确的边界:分类数 ≤ 8,总标记数 ≤ 1000,坐标是[经度, 纬度],且支持privacy.blur(坐标模糊)和privacy.cityOnly(仅显示城市)。这些限制不是随意设定的——1000 个标记是“个人足迹”的合理上限,超过这个量级应该考虑聚合或专门的 GIS 工具,而不是往一个嵌入组件里硬塞。
快速开始
项目推荐的方式是 Web Component,零构建、样式隔离。从 README 中提取最小可运行示例:
方式一:Web Component(零构建)
<scriptsrc="https://cdn.jsdelivr.net/npm/footprint-globe/dist/auto.js"></script><footprint-globe >="./pins.json"height="480px"></footprint-globe>auto.js是自包含产物:它内联了 Web Component、auto-init 逻辑和 maplibre-gl 的 CSS,不需要额外引入样式文件,也不需要 importmap。数据通过data-src从外部 JSON 加载,适合数据量在 2–50 KB 的场景;小于 2 KB 可以直接内联,大于 50 KB 建议用 iframe。
数据格式(JSON):
{"version":1,"categories":[{"id":"travel","label":"旅行","color":"#f97316","pins":[{"name":"2019.04 东京","coords":[139.759,35.682]},{"name":"2022.12 巴黎","coords":[2.32,48.86]}]}]}coords严格为[经度, 纬度],这是配置协议中明确的约束,避免与 GeoJSON 的[lat, lng]习惯混淆。
方式二:NPM(构建工具)
npmi footprint-globe maplibre-glimport{FootprintGlobe}from'footprint-globe';importmaplibregl from'maplibre-gl';import'maplibre-gl/dist/maplibre-gl.css';import'footprint-globe/dist/footprint-globe.css'; const globe=new FootprintGlobe(document.getElementById('globe')!,{config: myConfig, maplibre: maplibregl,});maplibre-gl是 peer dependency,由使用者提供,这样核心包可以保持轻量,也允许用户自托管 MapLibre 或锁定版本。
方式三:iframe(零依赖)
<iframesrc="https://footprint-studio.pages.dev/embed/#data-id=xxx"/>iframe 模式在宿主页面中不引入任何 JS 依赖,适合数据量较大或需要与宿主彻底隔离的场景。
实践中的坑与取舍
坑一:MapLibre globe 投影的 GPU 精度不是“一次适配、永久有效”
这是整个项目中最容易被低估的问题。MapLibre 的 globe 投影在顶点着色器里需要把 Mercator 坐标转换为球面角度,用到atan(exp(...))这样的反三角函数。MapLibre 上游的 PR 明确指出,这个计算在某些 GPU 上(主要是 AMD 和 Nvidia)精度不足,严重到需要 workaround,而且“误差是硬件相关的,未来可能变化,需要在运行时测量”。
对于 footprint-studio 来说,这意味着不能简单地在初始化时调用setProjection('globe')就假定一切正常。项目在fallback模块和map-controller中需要处理的不只是“浏览器是否支持 WebGL”这个二值判断,还要考虑 WebGL 上下文丢失、GPU 驱动差异等运行时异常。README 中“WebGL 不可用时显示列表视图”的优雅降级,在实现层面比听起来要更细:它是一套主动的容错设计,而不是一个简单的if (!WebGLRenderingContext)。
坑二:“自包含”与“可 CDN 加载”之间的张力
项目早期的一个关键重构是“inline maplibre-gl CSS into the auto bundle”。在 Web Component 模式下,如果auto.js不内联 MapLibre 的 CSS,用户就必须手动引入maplibre-gl.css,否则弹窗、控件和标记的样式会错乱——这违背了“粘贴一行 script 就能用”的承诺。但内联 CSS 会增加auto.js的体积,而 MapLibre 的 JS 引擎本身并不内联,而是按需从 CDN 加载(可通过maplibreUrl自托管)。
这里的取舍是:CSS 内联,JS 外部加载。CSS 是渲染正确性的必要前提,体积相对可控;MapLibre JS 约 250–300 KB gzip,如果内联进去,核心包的“< 15 KB gzip”标签就失去意义了。所以auto.js的“自包含”是有限度的自包含:它保证“一行 script 能跑”,但不承诺“不产生任何网络请求”。
坑三:Web Component 合并回核心包——包装层比想象中薄
commit 历史里有一条 “merge @footprint-studio/web-component into footprint-globe”。最初把 Web Component 拆成独立包,大概是出于“关注点分离”的直觉:引擎负责渲染,包装层负责自定义元素和 Shadow DOM。但实际迭代后发现,Web Component 的生命周期(connectedCallback、attributeChangedCallback、disconnectedCallback)与地球实例的创建/更新/销毁高度耦合,拆包带来的收益(独立版本管理)远小于跨包调试的成本。合并之后,FootprintGlobeElement和FootprintGlobe类可以共享类型和内部工具,用户也少了一个需要关心的包。
这不是“架构错误”,而是在项目早期,过早拆包的隐性成本往往高于收益。Monorepo 的价值不在于包越多越好,而在于边界清晰;当边界被证伪时,合并回去是合理的修正。
坑四:数据量的硬限制与“不要用嵌入组件解决所有问题”
配置协议把总标记数限制在 1000 个,分类数限制在 8 个。这不是技术上的硬上限,MapLibre 可以渲染远多于此的 marker,但产品的定位决定了上限:footprint-studio 的目标是“个人足迹”,不是“团队分布图”或“业务地理看板”。超过 1000 个标记,用户真正需要的可能是聚合、热力图或时间轴播放,这些被放在了 v1.1 和 v2 的路线图中,而不是在 v1.0 里用性能优化硬撑。
同样,README 给出的数据量指引(< 2KB 内联,2–50KB 用data-src,更大用 iframe)也是一种主动的取舍:不做一个“万能加载器”,而是明确告诉用户在哪个区间用哪种方式,降低踩坑概率。
性能数据
官方 README 给出的核心数据是:核心包 gzip < 15 KB(不含 MapLibre)。这个数字的关键在于“不含 MapLibre”——MapLibre GL JS 本身的 gzip 体积约 250–300 KB,是真正的大头。但 MapLibre 从 CDN 加载且可被多个地图实例共享,对于博客场景来说,浏览器缓存命中后的边际成本很低。
MapLibre 上游对 globe 投影的性能改进会直接惠及本项目:v5.24 中“Render halo and glyph in a single pass”带来约 40% 的时间下降,globe 的瓦片视锥剔除也在持续优化。项目自身在 1000 标记以内不需要复杂的性能技巧,真正的性能敏感点在于降级路径的触发时机:是等到 WebGL 上下文丢失后才显示列表,还是在初始化阶段就检测到 GPU 不满足 globe 要求时主动降级。前者会造成闪烁和布局跳动,后者需要更谨慎的检测逻辑。项目在fallback模块中的设计倾向于后者,但具体阈值和探测策略是这部分最需要在实际设备矩阵上验证的地方。
相关链接
footprint-studio 项目仓库
文档站
快速开始
配置协议 v1
API 文档
示例库
在线 Builder
footprint-globe on npm
相关项目
- footprint-studio —
本站出品 · 酷站索引 · 技术笔记