暂不支持移动端访问

BlogRoom 开发与维护指南

2240 字 11 分钟
AI 摘要

从 React、Three.js、状态管理、边缘 API 到数据同步,给出 BlogRoom 的代码级维护入口。

BlogRoom 是一个独立部署的 React + Three.js 个人主页。它的核心不是把网页截图放进 Canvas,而是用 3D 场景提供空间导航,用 React DOM 承载真实的信息界面。书架、电脑、键盘、画像、门和留声机是场景中的交互入口;书籍详情、RSS、GitHub、搜索、音乐播放器和退出提示是普通 React 面板。

项目来源与改造范围#

BlogRoom 的部分场景视觉与交互设计参考并基于开源项目 MmzMing/3D-HOME 的思路继续开发,原作者为 MmzMing。当前项目已经围绕个人主页内容、AstroBlog 数据同步、同源边缘 API、状态管理、部署方式和交互行为重新组织;后续维护应以本仓库的目录、数据契约和运行配置为准。

这篇文档按“以后要修改代码时从哪里开始”的方式组织。当前项目使用 React 19.2.8、React DOM 19.2.8、Vite 8.2.0、Three.js 0.185.1、React Three Fiber 9.7.0、Zustand 5.0.14、TanStack Query 5.101.4、GSAP 3.15.0、@react-three/rapier 2.2.0 和 Zod 4.4.3。运行环境声明为 Node.js >=24.9.0 <25、pnpm >=10.33.0 <12

项目分层#

text
src/app/ 应用根组件、错误边界和主题同步
src/scene/ Canvas、房间对象、相机、后处理和物理
src/features/ 资料、书籍、RSS、GitHub、音乐、搜索等 DOM 面板
src/stores/ room-store、player-store 等全局状态
src/api/ 浏览器请求封装和同源边缘路由
src/config/ profile、主题、运行配置和房间数据快照
src/hooks/ 媒体查询、降级、reduced-motion 等行为
src/types/ 房间对象、面板、API 和数据类型
src/utils/ 命令、存储、音频、数据和 WebGL 工具

这几个目录不是按页面名称划分,而是按运行职责划分。scene 不应该直接请求 RSS,features 不应该操作 Three.js camera,api 不应该修改面板状态。它们之间通过 store、命令和查询结果通信。

应用入口和生命周期#

src/app/app.tsx 创建 QueryClient,并设置 refetchOnWindowFocus: falseretry: 1staleTime: 5 * 60_000AppErrorBoundary 包住整个应用,外部 API 或某个 feature 渲染出错时可以显示错误边界;ThemeSync 把 Zustand 中的主题同步到 DOM 和 Canvas。

RoomExperience 维护房间揭示、Canvas ready、字形 warmup 和 scene ready 等状态。它先调用 supportsWebGL():支持 WebGL 时挂载 RoomCanvas,否则挂载 RoomFallback。房间揭示流程由 Canvas、字形层和 line reveal 回调共同推进,不能通过简单的 setTimeout 删除其中一层,否则在慢设备上可能出现 Canvas 已显示但字体仍未准备好的情况。

Canvas 渲染层#

src/scene/room-canvas.tsx 创建 R3F Canvas:正交相机的初始位置为 [20, 14.5, 22],zoom 为 58,远裁剪面为 140;DPR 在浅色模式最高为 1.75,深色模式最高为 1.5;frameloop="demand" 让没有状态变化时不持续渲染。

Suspense 包住 RoomScene。场景 ready、DollWord 字体 warmup 和 line reveal 都有独立回调,app.tsx 只有在这些条件满足后才把房间视为稳定。Canvas 容器标记为 aria-hidden,所以可访问入口不能只存在于 3D 物件的 pointer handler;RoomFallback 提供静态 SVG 预览,新增核心入口时还要考虑无 WebGL 用户如何到达对应面板。

场景对象#

src/scene/room-scene.tsx 负责装配场景。RoomShell 提供墙面和基础空间,DeskMonitorLaptopKeyboardBookshelfDoorWindowGramophone 等对象分别实现几何和局部动画。对象由箱体、平面、圆柱、线段和基础材质组合,不依赖整体室内模型,因此尺寸和状态都可以在代码中修改。

新增物件时按以下顺序处理:

  1. src/scene/objects/ 创建几何组件,明确坐标、层级、pointer 区域和主题材质。
  2. room-scene.tsx 挂载组件,并确认它所在的 camera zone。
  3. types/room.ts 增加对象 ID、面板 ID 或状态字段。
  4. room-commands.ts 增加 roomObjects 定义和 activateRoomObject() 分支。
  5. 如果有信息面板,在 features/ 创建组件并由 app.tsx 挂载。
  6. room-store.ts 增加状态动作,处理打开、关闭和相机恢复。

不要让家具组件直接 import 某个 Dialog,也不要把 selected ID 作为多个组件之间的临时 prop 链传递。

相机和交互状态#

camera-rig.tsx 把相机状态分为 zone 和 focus。loungeoverviewworkspace 是大范围区域;bookshelfdoorkeyboardlaptopportrait 是局部焦点。桌面端使用 GSAP 沿二次贝塞尔曲线同时插值相机位置、lookAt target 和 zoom,动画中断时从当前相机值继续。prefers-reduced-motion 开启时直接设置终点,不执行补间。

移动端宽度小于 720 时不自动切换焦点,而是显示 MapControls,允许平移、禁止旋转和缩放。新增焦点时必须同时检查移动端,因为移动端不会使用桌面端的 cameraFocuses 自动镜头。

room-store.ts 的状态主要包括:

text
cameraZone / cameraFocus 当前区域和局部焦点
panel / selected*Id 当前 DOM 面板和选中对象
objectState 灯、门、窗帘、显示器等物件状态
theme / isSoundEnabled 主题和声音开关
doorExit* 门出口友链和提示状态
dollWord* 字形短语、清除版本和可见数量

动作函数负责维护互斥关系。例如 openBook() 会清除 feed/link 的选中 ID,关闭门提示并把 panel 设为 booksetCameraZone() 会关闭面板和门状态;restoreCameraFocus() 根据 focusReturnZone 返回原区域。修改一个动作时要检查这些清理逻辑,否则从书架切到门或从弹窗返回房间时会残留旧状态。

命令表#

room-commands.ts 是场景与业务之间的适配层。显示器、电脑、画像、书架、留声机、键盘和门等对象都通过 activateRoomObject(id) 进入统一分发;灯、窗户、风扇、时钟、抽屉、椅子、床和植物只修改 objectState 或触发 pulse。声音由 playRoomSound()playObjectSound() 统一判断 isSoundEnabled

命令表还保存对象标签和所属 zone,可用于桌面端 DOM 导航或调试面板。新增对象如果没有加入 roomObjects,视觉上可能可点击,但键盘导航、对象菜单和相机区域不会知道它存在。

Feature 和 API#

src/features/ 下的面板使用 TanStack Query 获取运行时数据。src/api/http.tsrequestApi() 为请求设置 8 秒超时、同源凭据和 JSON Accept,成功响应必须符合 { data, meta },失败响应必须符合 { error: { code, message, requestId, retryable } }。解析失败会抛出 ApiError,feature 根据它显示可重试或不可重试状态。

本地和生产共用 src/api/edge/router.ts。当前路由是 /api/feeds/api/github/api/health/api/music。router 会拒绝非 /api/ 路径及带静态资源扩展名的请求,未注册方法返回 405,未注册路径返回 404。生产由 Cloudflare Pages Function 进入同一路由,第三方 token 只存在边缘环境变量。

书籍和友链是构建期 JSON 快照,直接由 src/config/index.ts 导入;RSS、GitHub 和音乐是运行时请求。不要为书籍面板增加一次浏览器 fetch,也不要把运行时 API 数据写回构建期配置。

Rapier 字形物理#

字形物理位于 src/scene/doll-words/physics.tsx,使用 @react-three/rapierDollWordPhysics 设置重力 [0, -200, 0]StaticRoomColliders 使用固定 RigidBody 和多个 CuboidCollider 描述地面、墙面、桌面和书架;动态字形使用 RigidBody 和基于字形宽高计算的 CuboidCollider

core.ts 先生成 glyph plan,再由物理层决定世界坐标、字体、初始 impulse、angular velocity、显示时间和释放时间。字形状态包括 hidden、held、dynamic、clearing,动态刚体睡眠或越界时被移除。移动端和 reduced motion 会减少数量或改成固定文字。修改掉落节奏先看 core.ts,修改碰撞和刚体行为再看 physics.tsx

数据同步#

BlogRoom 构建前运行 scripts/sync-room-data.mjs,顺序是线上 AstroBlog 数据、本地 AstroBlog 快照、当前 BlogRoom 快照。脚本验证 version === 1items 数组和必需字段后才写入 src/config/room-data/。修改书籍或友链字段时必须先修改 AstroBlog 的生成脚本,再同步 BlogRoom,并检查书架和链接面板。

修改矩阵#

修改目标首先查看影响范围
新增家具scene/objects/room-scene、对象命令、状态、DOM 导航
修改镜头camera-rig.tsxzone/focus、GSAP、移动端 MapControls
修改弹窗features/app.tsx、PanelId、Query 和关闭后恢复
修改 APIapi/edge/handler、schema、requestApi、Pages Function
修改书籍/友链AstroBlog 生成脚本同步脚本、JSON schema、卡片类型
修改字形物理doll-words/core.tsphysics.tsxRapier、字体 warmup、移动端限制
修改主题或声音ThemeSyncroom-store.tsCSS、Canvas 背景、localStorage、音效

本地运行和质量检查#

Terminal windowpowershell
# 安装项目锁定依赖并启动 Vite 开发服务器。
corepack pnpm install
corepack pnpm dev
# 运行工具函数和 API 相关 Vitest 用例。
corepack pnpm test
# 检查 React、R3F、scene、feature 和 edge 类型。
corepack pnpm typecheck
# 执行 ESLint 与 Stylelint。
corepack pnpm lint
# 同步房间数据并生成 Cloudflare Pages 生产 bundle。
corepack pnpm build

当前源码包含 98 个 TypeScript、TSX 与 CSS 单元,共约 10,252 行;Vitest 有 1 个测试文件、2 个用例。生产构建可完成,当前 format:check 仍需单独处理 src/components/common/object-showcase.tsxsrc/config/room-data/friends.json。这两个格式问题不能被写成“所有质量门禁均已通过”。

修改完成后至少检查桌面端、移动端、无 WebGL、reduced motion、API 失败和面板关闭恢复。对于外部服务,静态构建通过只说明 bundle 正确,不能证明 Cloudflare 环境变量、GitHub token、RSS 上游或音乐 R2 在生产环境可用。

[ 公告 ]

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

了解更多
[ 音乐 ]
封面

音乐

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