第 7 章:React 前端与状态管理
一、前端不是 Rails 的附属脚本
Mastodon 的 Rails 负责认证、页面壳和服务端能力,但登录后的主要交互由 React 完成。前端代码规模大、页面类型多,理解它的关键是先找三类入口:
- bootstrap:应用如何 mount、加载 locale、读取初始状态;
- route/page:URL 对应哪个页面容器;
- store/streaming:数据如何进入 Redux、如何触发组件更新。
二、Vite 构建边界
根 package.json 通过 vite、React plugin、legacy plugin 和 config/vite 生成前端 bundle。源码主要在:
app/javascript/
mastodon/
containers/ 页面级容器和 route 入口
features/ 时间线、状态、通知、compose 等功能模块
reducers/ normalized entities 与 UI state
actions/ REST/streaming 触发的状态变化
selectors/ 从 store 派生视图数据
components/ 可复用 UI
packs/ Vite entrypoints
locales/ 国际化资源
Vite 解决的是模块打包和开发热更新,不改变 Rails 的服务端路由。生产构建的静态资源仍由 Rails/反向代理提供。
三、React 页面装配
容器组件通常连接路由参数、认证状态、分页和 dispatch;展示组件负责可视结构。不要从一个 JSX 组件直接推断数据来源,要继续追它的 selector、action 和 reducer。
四、Redux 的 normalized state
状态卡片同时被 home、public、search、notification、profile、thread 使用。若每个页面都保存一份完整对象,编辑、删除或关系更新会产生大量同步问题。因此通常使用实体表:
state.entities.accounts[id]
state.entities.statuses[id]
state.entities.media_attachments[id]
state.entities.polls[id]
state.timelines.home.items = [statusId, statusId, ...]
state.notifications.items = [notificationId, ...]
这样一条 status.update 只需要替换 entities.statuses[id],所有引用它的时间线都会重新渲染。
五、REST 输入和 streaming 输入如何合并
初始加载
页面 dispatch REST action,拿到分页数据,写入实体表和 timeline IDs。
实时追加
Streaming 收到 update,先 upsert entities,再把 status ID 插入对应 timeline。若状态已经存在,则根据 status.update 替换,不重复追加。
删除
delete 事件从 entities 删除或标记失效,并从 home/public/list/search/thread 等引用中移除。
重连
重连后重新请求时间线,解决 Pub/Sub 事件不可回放带来的缺口。
六、Compose Box 是一个小型状态机
发帖编辑器的状态并不只有 text:
idle
├─ typing
├─ uploading media
├─ polling location
├─ selecting visibility
├─ scheduling
├─ submitting
├─ success → reset
└─ error → preserve draft + show validation
后端 PostStatusService 会再次校验所有条件,前端校验只是即时反馈,不是安全边界。尤其是 mention、visibility、media ownership 和 idempotency,必须以服务端结果为准。
七、国际化与主题
Mastodon 的前端还要同时处理 locale、日期格式、复数规则、emoji、RTL 和自定义主题。读取 UI 时不要把文字直接当成静态字符串:
- locale JSON 是构建/运行时资源;
- component 使用 react-intl 等 formatter;
- 服务器会把当前 locale 传给页面或资源加载器;
- CSS/SCSS 与主题变量共同决定展示;
- accessibility label 也需要翻译。
八、前端权限不是后端权限
React 可以隐藏按钮,但不能阻止用户直接调用 API。正确的安全分层是:
前端权限:改善体验,决定显示什么
REST Controller/Policy:决定请求是否接受
Service:对内部调用再次保护
Serializer:决定响应中暴露什么
ActivityPub:决定远端可见范围
遇到“普通用户看到了管理按钮”是 UI 问题;遇到“普通用户能调用管理 API”才是安全问题,两者应分别排查。
九、前端性能的几个关键点
- 时间线使用虚拟化/分页策略,避免渲染无限 DOM;
- entity normalization 减少重复对象更新;
- selector 避免每次 render 创建不稳定引用;
- media 预览和上传采用懒加载/异步状态;
- streaming 消息高峰时要控制批量 dispatch 与重渲染;
- Vite code splitting 让设置、管理和媒体页面不拖慢主时间线。
十、读前端源码的垂直切片
推荐从“通知实时更新”这一条切:
- 找
streamingclient 如何订阅user:notification; - 找事件转换成哪个 action;
- 找 notification reducer/normalized entity;
- 找通知列表 selector;
- 找 Notification component 如何链接 status/account;
- 对照
NotifyService和PushUpdateWorker的服务端 publish。
这条路径能把 Node、Rails、Redis、Redux、React 串成一个闭环。
十一、源码入口清单
app/javascript/mastodonapp/javascript/mastodon/containersapp/javascript/mastodon/reducersapp/javascript/mastodon/actionsapp/javascript/packsconfig/vite
十二、小结
前端的核心是把 REST 的可回放数据和 streaming 的瞬时事件统一到 normalized store,再由 selector 驱动 React。下一章回到服务端异步体系,分析 Sidekiq 如何承接媒体、搜索、通知和联邦之外的副作用。