第 3 章:Web 请求、REST API 与前端入口
一、路由是协议地图
Mastodon 的 config/routes.rb 不是一张平面表,而是把不同协议分组:网页、OAuth、API、ActivityPub、well-known、管理后台和健康检查。阅读路线应从外往里:
config/routes.rb
├─ config/routes/api.rb
├─ config/routes/activitypub.rb
├─ config/routes/settings.rb
└─ config/routes/admin.rb
同一个业务对象可能有三种外部形状:
- Web 页面:HTML shell + React 路由;
- REST:面向客户端的版本化 JSON;
- ActivityPub:面向联邦节点的 JSON-LD Activity/Object。
它们共享 Model/Service,但不共享输出协议。这个隔离是兼容性和安全性的基础。
二、一次 REST 请求的五层
以 POST /api/v1/statuses 为例:
sequenceDiagram
participant UI as React Compose
participant R as Rails Route
participant C as StatusesController
participant S as PostStatusService
participant DB as PostgreSQL
participant Q as Sidekiq
participant J as JSON Serializer
UI->>R: POST /api/v1/statuses + OAuth token
R->>C: route + authentication
C->>S: call(current_account, params)
S->>DB: transaction: status + relations
S->>Q: enqueue distribution/notification
S-->>C: Status
C->>J: render status for viewer
J-->>UI: REST JSON
1. 路由层
只负责把 URL、HTTP method 和 path parameter 映射到 controller action。复杂的参数校验不应该依赖路由本身。
2. 认证层
API 请求通常经过 OAuth access token 解析,再把 token scopes、resource owner 和 account 放入请求上下文。Streaming 使用相似的 token,但在独立 Node 进程中自己解析和校验必要权限。
3. Controller 层
Controller 的理想职责:
- 读取当前用户/当前账户;
- 允许参数白名单;
- 调用一个 service 或查询对象;
- 选择 serializer 与 HTTP 状态码;
- 把异常转换成 API error。
如果一个 action 开始直接操作多张表、发多个 worker、拼 ActivityPub payload,就应该把阅读焦点转向对应 service。
4. Service 层
PostStatusService 展示了 Mastodon 的典型用例编排:预处理属性、校验媒体、解析提及、保存状态、挂接标签/引用、提交后分发。Service 不只是“胖 model 的替代品”,它把一个用户意图跨越多个模型和副作用的过程聚合起来。
5. Serializer 层
REST serializer 根据 viewer 和请求上下文决定字段:账户关系、媒体、投票、可见性、编辑信息、应用来源等。输出 JSON 不是数据库行的直译;它是稳定的客户端契约。
三、为什么 Controller 不直接返回 Model
直接 render json: @status 会暴露三个问题:
- 内部字段与客户端契约耦合;
- viewer-dependent 字段无法统一裁剪;
- 同一个模型在 REST、HTML、ActivityPub 中需要不同表示。
所以要区分:
Model = 数据与关系
Presenter = 视图所需的组合
Serializer = 对外协议字段
Policy = 当前主体能看/能做什么
四、前端如何进入 Rails
Mastodon 的动态 UI 使用 React、Redux 和 Vite。Vite 负责从 app/javascript 构建 bundle;Rails 仍负责页面 shell、静态资源入口和服务端路由。典型的浏览器导航是:
GET /home
└─ Rails 页面
├─ 注入 instance / current user / locale 等初始信息
└─ 加载 Vite 产物
└─ React mount
├─ route 页面组件
├─ Redux store
└─ API / streaming client
前端的 API client 通常携带 OAuth token 和客户端 headers;服务端把错误映射为用户可见的 toast、表单错误或重新登录。
五、REST 与 ActivityPub 的边界
REST 面向客户端开发者,路径通常是 /api/v1 或 /api/v2,返回 Mastodon API schema。ActivityPub 面向服务器间通信,需要:
- JSON-LD context;
- actor、object、activity 的 URI;
- HTTP Signature / digest;
- inbox/outbox/collection 语义;
- 远端服务器的容错与兼容。
两者可以共享 Account、Status、Follow,但不能复用同一套 serializer。读到 app/serializers/rest 或 app/serializers/activitypub 时,先确认它服务的协议。
六、错误处理的层次
参数错误
例如媒体 ID 不存在、visibility 值非法、投票选项格式不对,应在同步请求中返回 4xx,不入队。
领域冲突
例如不允许引用私密状态、未授权修改别人的状态、提及不符合 allowed_mentions,由 service/policy 抛出可识别异常。
外部依赖错误
远端 HTTP 超时、DNS、5xx 不应让本地发帖失败;这类错误进入 Sidekiq retry 和 delivery failure tracker。
客户端协议错误
Serializer 不能随意改字段名或 null 语义;API 的兼容性本身是一类错误防线。
七、从源码验证一个 API 的方法
以任意 endpoint 为例,按如下顺序做“垂直切片”:
- 在
config/routes找 method/path; - 打开 controller action;
- 找
before_action、OAuth scope 和 current account; - 找 service/query/policy;
- 找 serializer 与 presenter;
- 找对应 request spec;
- 再看前端调用处和 reducer;
- 如果有副作用,沿 worker 到最后的 adapter/HTTP 请求。
这种切法比从某个大目录开始漫游更快,因为每一步都能用测试和路由验证。
八、源码入口清单
config/routes.rbconfig/routes/api.rbapp/controllers/api/v1/statuses_controller.rbapp/services/post_status_service.rbapp/serializers/rest/status_serializer.rbapp/javascript/mastodon
九、小结
Web 层的关键不是 Rails 本身,而是“协议适配器 → 用例 service → 事实模型 → 异步副作用 → 协议 serializer”的边界。下一章进入最有代表性的用例:发帖如何同时触发本地时间线、通知、公共流和远端联邦。