暂不支持移动端访问

BlogRoom 数据同步、边缘 API 与物理系统

1523 字 8 分钟
AI 摘要

说明 BlogRoom 如何消费 AstroBlog 快照、访问同源边缘 API,并使用 Rapier 实现字形物理。

BlogRoom 的书籍和友链属于构建期数据,GitHub、RSS 和音乐属于运行时数据,玩偶文字则属于 Canvas 内的实时物理状态。三类数据的生命周期不同,代码也分别放在生成脚本、边缘 API 和场景物理层。

AstroBlog 到 BlogRoom 的数据契约#

AstroBlog 构建时执行 scripts/generate-room-data/index.mjs。脚本递归读取 src/content/bangumi/src/content/friends/ 的 Markdown Frontmatter,只保留 category: book、启用的友链和合法 URL,生成:

text
AstroBlog/src/content/bangumi + friends
scripts/generate-room-data/index.mjs
AstroBlog/public/room-data/books.json
AstroBlog/public/room-data/friends.json

输出 envelope 固定为 { version: 1, generatedAt, items }。书籍包含 idtitlecoverdescriptionstatusscoretagsdetailPath;友链包含 idtitleimagedescriptionurltags。生成脚本会按 ID 或 URL 去重,避免内容源中存在重复记录时污染房间。

BlogRoom 构建前运行 scripts/sync-room-data.mjs,先请求 ROOM_DATA_ORIGIN 下的线上 JSON。网络失败时读取 ROOM_DATA_LOCAL_DIR 指向的 AstroBlog 本地快照,再失败才保留 BlogRoom 当前已有快照。每次读取都验证 version、items 数组和必需字段,不接受格式错误的远程响应。

text
线上 AstroBlog JSON
↓ 失败
本地 AstroBlog 快照
↓ 失败
BlogRoom 已有快照
src/config/room-data/*.json

src/config/index.ts 在构建时直接导入这些 JSON,书架和友链组件不会在浏览器再次请求它们。修改共享字段时,必须同时更新生成脚本、BlogRoom 的 validEnvelope() 字段列表、类型定义和卡片使用位置。

运行时 API#

BlogRoom 的浏览器请求统一通过 src/api/http.tsrequestApi()。它创建 8 秒 AbortController 超时,附加 credentials: 'same-origin' 和 JSON Accept,非 2xx 响应解析为带 code、requestId、retryable 和 status 的 ApiError;成功响应必须符合 { data, meta: { cachedAt, requestId, stale } }

本地 Vite 和生产 Pages Function 共用 src/api/edge/router.ts。当前路由是 /api/feeds/api/github/api/health/api/music。router 先拒绝非 /api/ 路径和带静态资源扩展名的请求,再根据 pathname 与 HTTP 方法分派 handler;方法不存在返回 405,路径不存在返回 404。

第三方 token 只在边缘 handler 的环境变量中使用。GitHub handler 返回公开 profile、仓库和贡献数据,RSS handler 解析 feed,音乐 handler 访问博客提供的目录。浏览器收到的是经过 Zod schema 验证的统一结构,不会看到 GitHub token 或上游错误堆栈。

TanStack Query 缓存#

app.tsx 创建的 QueryClient 关闭窗口聚焦自动刷新,失败重试一次,staleTime 为五分钟。feature 面板在打开时执行 query,关闭后保留缓存;再次打开不必重新请求未过期数据。修改 API 响应结构时,要同步更新 feature 的 data schema 和 loading/error/empty 状态,不能让组件直接访问未知 JSON。

Rapier 字形物理#

字形物理位于 src/scene/doll-words/physics.tsx,使用 @react-three/rapierDollWordPhysics 创建独立 physics world,重力为 [0, -200, 0],固定碰撞体由 StaticRoomColliders 声明;每个文字由 Drei Text 渲染,并由 RigidBody + CuboidCollider 承载。

TSX
<Physics colliders={false} gravity={[0, -200, 0]} timeStep={1 / 60}>
{/* 第一次释放字形前先预热字体,避免出现一帧 fallback 字体。 */}
<FontWarmup onReady={onReady} />
{/* 用固定 Rapier 碰撞体表示不会移动的房间结构。 */}
<StaticRoomColliders />
<DollWordBodies />
</Physics>

core.ts 先根据短语、设备宽度和 reduced motion 生成 glyph plan,计算字形宽高、显示时间、释放时间、初始冲量和角速度;物理层再把 plan 转为 Three.js 世界坐标。动态字形释放后施加 impulse 和 angular velocity,睡眠时缩短可见时间,越界时移除。隐藏、清除、held、dynamic 和 clearing 是不同阶段,不能只用一个 boolean 控制。

移动端只预热主字体并减少字形数量;开启 reduced motion 时不释放动态刚体,而是显示短暂的固定文字。新增字形动画时,应先修改 core.ts 的计划和限制,再调整 physics.tsx 的刚体行为,避免把时间线规则写进渲染组件。

修改数据、API 和物理的顺序#

修改书籍或友链:先改 AstroBlog 内容或生成脚本,再运行 BlogRoom pnpm sync:room-data;修改接口:先改 edge handler 和 schema,再改 requestApi() 使用方;修改字形:先改 core.ts 的纯计算,再改 Rapier 层和 reduced-motion 分支;修改第三方凭据:只改 Pages 环境变量和边缘函数,不把 key 放进 Vite 客户端配置。

Terminal windowpowershell
# 从线上数据开始同步,失败时按脚本顺序回退到本地快照。
pnpm sync:room-data
# 检查 API、物理层和场景组件类型。
pnpm typecheck
# 运行当前测试文件。
pnpm test
# 执行同步、类型检查和 Vite 生产构建。
pnpm build

当前构建会转换约 2643 个模块,Rapier 属于较大的运行时依赖。新增场景功能时要关注首屏 bundle、Canvas 是否等待过多资源、无 WebGL fallback 是否可用,以及外部 API 失败时房间是否仍然可以交互。

API 响应和缓存语义#

边缘 handler 的成功响应包含 datametacachedAt 表示数据生成或缓存时间,stale 表示上游不可用时是否使用了旧值,requestId 用于把浏览器错误和边缘日志对应起来。feature 显示错误时应保留 requestId,方便定位;不能只显示“请求失败”而丢弃服务端提供的诊断信息。

健康接口可以检查边缘函数是否发布、运行时版本和上游配置,但不应泄露 token 或完整环境变量。GitHub/RSS/音乐 handler 需要设置超时、限制响应大小并校验上游 JSON/XML,避免一个异常上游把大响应直接传给浏览器。

Rapier 生命周期和性能#

DollWordBodies 只为可见 glyph 创建刚体。hidden 阶段不挂载物理对象,clearing 阶段禁用刚体并等待缩放动画结束,dynamic 阶段按固定帧间隔检查越界。canSleep、线性/角阻尼和软 CCD 参数用于限制刚体长期运行;修改重力或碰撞体时要观察字形是否卡在墙体、是否永不休眠以及清除操作是否泄漏对象。

Rapier 会增加初始 bundle 和 wasm 加载成本,因此 DollWordLayer 需要等待 warmup,并在不支持 WebGL 或 reduced motion 时走非物理分支。不要为了一个短动画把整个房间改成每帧高精度物理模拟。

[ 公告 ]

如果你喜欢,那么欢迎来到我的世界!

了解更多
[ 音乐 ]
封面

音乐

找不到相关结果。
[ 目录 ]
[ 全部文章 ]