关于 rails-hotwire-realtime
rails-hotwire-realtime 是面向 Rails 开发者的 Turbo、Stimulus 与 ActionCable 架构和评审指南。它介绍何时使用 Turbo Streams、何时采用轻量 ActionCable 信号,并涵盖实时更新、连接安全与测试。
主要功能
- 01选择 Streams-only 或 Cable 增强架构
- 02使用 Turbo Streams 更新页面
- 03广播范围按用户与租户隔离
- 04重连后补取更新
- 05Stimulus 控制器清理与协调
- 06实时功能测试模式
适用场景
- 01为 CRUD 应用选择实时更新方案
- 02实现多区域原子页面更新
- 03构建聊天中的在线状态与输入提示
- 04测试多用户实时广播
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ marckohlbrugge/ 37signals-skills --skill rails-hotwire-realtime安装后在 Claude Code 中输入/rails-hotwire-realtime确认已加载。
详细介绍
Rails Hotwire + Realtime
适用于 Turbo/Stimulus/ActionCable 架构与评审。模式取自 Campfire(大量使用 cable 的聊天应用)和 Fizzy(仅使用 streams 的看板)。
选择合适的实时架构
37signals 有两种经过验证的拓扑:
- **仅使用 Streams(Fizzy):**完全不使用自定义 ActionCable 通道。使用
turbo_stream_from、broadcasts_refreshes+ morph,以及带 ETag 的延迟加载 frame。对于 CRUD 类应用,默认采用这种方式。 - **Cable 增强型(Campfire):**仅在轻量 JSON 信号(在线状态、正在输入、未读提醒)场景使用自定义通道,因为在服务端渲染 HTML 会造成浪费。持久的 DOM 状态仍通过 Turbo Streams 传递。
经验法则:任何会更改 DOM 的内容都用 Turbo Streams;裸用 ActionCable 只传递微小的临时信号。
Turbo 默认设置
- 优先使用服务端渲染的局部更新,而非繁重的客户端状态系统。
- 在布局中设置全局 morph 刷新:
turbo_refreshes_with method: :morph, scroll: :preserve。 - 使用 Turbo Streams 进行定向更新;一个操作可以渲染一个多目标 stream 模板,原子化更新所有受影响区域(源列 + 目标列 + 详情窗格)。
- 使用
turbo_frame_tag ..., src:, loading: :lazy延迟加载开销较大的区域,并为 frame 端点设置独立的fresh_whenETag。将频繁变化的片段提取到独立的 frame 中,避免它们导致父级缓存失效。 - 对必须在导航或 morph 后保留的元素(页脚托盘、正在编辑的内容)使用
data-turbo-permanent。 - 通过
turbo:before-morph-attribute+preventDefault(),阻止 morph 覆盖客户端拥有的状态(例如,由 localStorage 驱动的折叠列)。 - 将实时更新频繁的页面排除在 Turbo 页面缓存之外(
turbo_exempts_page_from_cache);改用 frame 级别的 ETag。 - 无需 JS 框架即可实现乐观 UI:服务端渲染带有
$placeholder$标记的<template>局部模板;客户端在提交前克隆它并生成 ID;stream 响应将其替换。
广播模式
- 将广播逻辑放在模型中(
Message::Broadcastsconcern 中的broadcast_create),而不是控制器中。 - 按租户/用户限定每个 stream 名称:
[board.account, :all_boards]、[user, :notifications]——stream 名称就是隔离边界。 - 必要时使用双 stream:用
broadcasts_refreshes面向直接订阅者,并用broadcasts_refreshes_to ->(r) { [r.account, :aggregate_view] }面向账户范围的视图。 - 仅在发生有意义的变化时触发嘈杂的次级广播:在
before_update中,当与预览相关的字段发生变化时设置标记;使用if: :preview_changed?进行广播。 - 在后台任务偶然触碰记录时抑制广播(
Model.suppressing_turbo_broadcasts),例如在 ActiveStorage 分析期间。 - 高效扇出:只调用一次
render_to_string,然后针对每个接收者调用broadcast_replace_to user, ..., html:。 - 同步广播会影响请求延迟时,使用
broadcast_*_later异步变体。 - 广播渲染的局部模板缺少请求上下文:封装附件/URL 辅助方法(
broadcast_image_tag模式),确保 URL 能正确解析。
重连与追赶更新
- 追赶更新优于重新加载:重连/切回标签页时,获取 turbo-stream 差异(
?since=<epoch_ms>);服务端追加新记录并替换已更新的记录。 - 空的
HeartbeatChannel可可靠地触发connected/disconnected回调,用于启动追赶更新和经防抖处理的离线 UI。 - 在 Stimulus
connect()中加入 turbo-preview 检查,避免前进/后退缓存预览打开套接字或请求权限。
Stimulus 默认规范
- 控制器应保持单一职责;在同一元素上组合多个小型控制器。
- 优先使用 targets/values,而不是临时选择器和属性解析。
- 始终在
disconnect中清理计时器、监听器和订阅。对于 cable 订阅,延迟一个动画帧后再取消订阅;如果元素已重新连接,则跳过取消操作(形态变更频繁)。 - 使用控制器之间的事件分发(或 outlets),避免紧密耦合。
- 对于复杂界面,应在 Ruby 辅助方法(
message_area_tag)中组合data-controller/data-action/target wiring,让视图保持声明式,并将契约集中在一处。 - 将页面上下文从
<meta>标签读取到小型window.Current对象中,而不是内联 JSON。 - 通过广播稍后添加的元素也能获得相应行为吗?设计控制器以处理动态插入的子元素(例如
targetConnected回调)。
滚动与交互契约
- 以声明式方式保持滚动位置:在广播上传递自定义属性(
maintain_scroll: true),并在turbo:before-stream-render监听器中统一处理。 - 当 streams 与乐观插入同时发生时,使用 promise 队列串行处理相互竞争的滚动变更。
- 在
turbo:submit-start时移除 stream target 的id,避免后台广播与表单自身的 stream 响应发生竞争。 - 无限信息流:在 turbo-stream 分页中使用 IntersectionObserver 哨兵(移除触发器、追加一批内容、追加新的哨兵);限制 DOM 大小;仅当用户位于最新页面时才自动滚动。
ActionCable / 连接安全
- 在
Connection#connect中使用与 HTTP 相同的身份解析方式进行身份验证;通过所有权限定频道订阅范围(current_user.rooms.find_by(id: params[:room_id]))。 - 远程断开已停用或被封禁用户的连接:
ActionCable.server.remote_connections.where(current_user: user).disconnect。 - 在线状态:使用 TTL 对连接进行引用计数(
connections计数器和connected_at,新鲜度范围为 60s),以应对多标签页和重新连接;对可见性变化进行防抖(约 5s),避免闪烁。 - 在基于路径的多租户模式下,通过自定义 meta 标签辅助方法从
request.script_name输出 cable URL。
缓存 + 实时更新
- 缓存键应与影响输出的因素保持一致(记录、用户、时区、筛选状态);使用
touch: true链,使子项编辑能够使父项缓存失效。 - 在客户端个性化缓存片段:渲染
data-creator-id,让 Stimulus 控制器与Current.user比较并切换可见性——不要因按用户切换可见性而破坏片段缓存。 - 当令牌时效性至关重要时,避免对表单页面进行 HTTP 缓存。
Web Push(适用时)
- 仅向已断开连接的用户推送,并排除发起操作的用户;遵循每位用户的参与级别(全部内容或仅提及)。
- 通过线程池和持久 HTTP 连接发送;在线程中执行发送前解析所有 AR 数据;异步使过期订阅失效。
- 用户登出时,在客户端清理推送订阅。
测试
- 模型广播:使用
assert_turbo_stream_broadcasts([user, :notifications], count: 1) { ... },并在断言无广播时使用assert_no_turbo_stream_broadcasts。 - 控制器响应:使用
as: :turbo_stream+assert_turbo_stream action: :replace, target: dom_id(...)。 - 对广播进行 DOM 级断言时,可使用一个读取
ActionCable.server.pubsub.broadcasts的小型辅助方法,并配合assert_select。 - 系统测试:使用
using_session("OtherUser")测试多用户实时交互,并在断言前等待turbo-cable-stream-source[connected]。
危险信号
- 重复的流 ID/目标导致更新不稳定。
- 广播频道或流名称未按租户/用户划分。
- 在裸 ActionCable 频道中渲染 HTML(请使用 Turbo Streams)。
- Stimulus 控制器在导航后未清理计时器/监听器。
- 对于本应通过流更新的小型交互,替换整个页面。
- 为
turbo_stream_from已经实现的功能自定义 ActionCable 频道。 - Morph 刷新破坏用户正在进行的输入(缺少
data-turbo-permanent)。
评论
支持 Markdown