第 7 章:React 前端与状态管理

一、前端不是 Rails 的附属脚本

Mastodon 的 Rails 负责认证、页面壳和服务端能力,但登录后的主要交互由 React 完成。前端代码规模大、页面类型多,理解它的关键是先找三类入口:

  1. bootstrap:应用如何 mount、加载 locale、读取初始状态;
  2. route/page:URL 对应哪个页面容器;
  3. 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 让设置、管理和媒体页面不拖慢主时间线。

十、读前端源码的垂直切片

推荐从“通知实时更新”这一条切:

  1. streaming client 如何订阅 user:notification
  2. 找事件转换成哪个 action;
  3. 找 notification reducer/normalized entity;
  4. 找通知列表 selector;
  5. 找 Notification component 如何链接 status/account;
  6. 对照 NotifyServicePushUpdateWorker 的服务端 publish。

这条路径能把 Node、Rails、Redis、Redux、React 串成一个闭环。

十一、源码入口清单

十二、小结

前端的核心是把 REST 的可回放数据和 streaming 的瞬时事件统一到 normalized store,再由 selector 驱动 React。下一章回到服务端异步体系,分析 Sidekiq 如何承接媒体、搜索、通知和联邦之外的副作用。