mcpField
R
rails-hotwire-realtime

rails-hotwire-realtime

github.com/marckohlbrugge开发73048 次安装20 周前更新
代码仓库

关于 rails-hotwire-realtime

rails-hotwire-realtime 是面向 Rails 开发者的 Turbo、Stimulus 与 ActionCable 架构和评审指南。它介绍何时使用 Turbo Streams、何时采用轻量 ActionCable 信号,并涵盖实时更新、连接安全与测试。

主要功能

  1. 01选择 Streams-only 或 Cable 增强架构
  2. 02使用 Turbo Streams 更新页面
  3. 03广播范围按用户与租户隔离
  4. 04重连后补取更新
  5. 05Stimulus 控制器清理与协调
  6. 06实时功能测试模式

适用场景

  1. 01为 CRUD 应用选择实时更新方案
  2. 02实现多区域原子页面更新
  3. 03构建聊天中的在线状态与输入提示
  4. 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_when ETag。将频繁变化的片段提取到独立的 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::Broadcasts concern 中的 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