mcpField
A
action-cable

action-cable

github.com/thinkoodle开发7190 次安装8 周前更新
代码仓库

关于 action-cable

面向 Rails 开发者的 Action Cable 技能,涵盖 WebSocket 连接、频道、订阅、广播、认证、适配器和测试。它还说明何时应优先使用 Turbo Streams,而非原始 Action Cable。

主要功能

  1. 01配置 WebSocket 连接与频道
  2. 02实现订阅和广播
  3. 03处理连接认证与断开清理
  4. 04配置 Solid Cable、Redis 等适配器
  5. 05测试频道和广播

适用场景

  1. 01为 Rails 应用添加实时聊天
  2. 02实现在线状态或输入提示
  3. 03推送作业进度和实时数据
  4. 04为 Action Cable 频道编写测试

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/thinkoodle/rails-skills --skill action-cable

安装后在 Claude Code 中输入/action-cable确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
adapters-config.md
broadcasting.md
channels.md
client-side.md
connection-auth.md
patterns.md
testing.md

详细介绍

Rails Action Cable 专家

使用 Rails 中的 Action Cable 构建实时 WebSocket 功能,包括频道、订阅、广播和 Solid Cable。

关键决策:Action Cable 还是 Turbo Streams

编写任何 Action Cable 代码之前,先问:Turbo Streams 能解决这个问题吗?

在以下情况下使用 Turbo Streams 广播(而非原始 Action Cable):
  • 广播模型变更(创建/更新/销毁),以更新页面片段
  • 简单的“内容有变,更新页面这部分”场景
  • 由 CRUD 驱动的实时更新(聊天消息、评论、通知列表)
  • 希望无需编写 JavaScript 就能自动更新 DOM
# This is Turbo Streams — NOT raw Action Cable. Prefer this.
class Message < ApplicationRecord
  broadcasts_to :room  # That's it. No channel file needed.
end
在以下情况下使用原始 Action Cable:
  • 自定义客户端行为——需要执行超出 DOM 替换范围的 received() 回调逻辑
  • 双向通信——客户端通过 perform()/send() 向服务器发送数据
  • 在线状态/输入状态指示器——跟踪谁在线、谁正在输入
  • 非 HTML 负载——发送 JSON 数据、坐标、游戏状态
  • 进度跟踪——文件上传进度、任务状态更新
  • 外部集成——从外部 WebSocket 源传输数据
  • 精细的流控制——根据用户操作动态订阅/取消订阅

**经验法则:**如果你只是在模型回调后广播 HTML,以替换/追加 DOM 元素,请使用 Turbo Streams。如果需要自定义 JavaScript 处理或双向通信,请使用 Action Cable。

适用场景

  • 设置 WebSocket 连接和频道
  • 实现实时功能(聊天、通知、在线状态、实时仪表板)
  • 配置 Solid Cable、Redis 或 PostgreSQL 适配器
  • 验证 WebSocket 连接
  • 编写客户端频道订阅
  • 测试 Action Cable 频道
  • 在生产环境中部署 Action Cable

操作说明

第 1 步:检查现有设置
# Check if Action Cable is already configured
cat config/cable.yml
ls app/channels/application_cable/

# Check for existing channels
ls app/channels/
ls app/javascript/channels/ 2>/dev/null

# Check if Turbo is present (prefer Turbo Streams if so)
grep -r "turbo-rails" Gemfile
grep -r "broadcasts" app/models/

**遵循项目现有惯例。**如果应用已经使用 Turbo Streams 广播,就不要为 Turbo Streams 能处理的场景引入原始 Action Cable。

第 2 步:设置连接身份验证

始终验证连接。绝不要跳过。

# app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    private
      def find_verified_user
        # Cookie-based auth (most common with Devise/standard Rails auth)
        if verified_user = User.find_by(id: cookies.encrypted[:user_id])
          verified_user
        else
          reject_unauthorized_connection
        end
      end
  end
end

不同设置对应的身份验证模式:

身份验证方式实现方式
Cookie/会话(默认)cookies.encrypted[:user_id] 或 cookies.encrypted["_session"]["user_id"]
Deviseenv["warden"].user 或 cookies.signed["user.id"]
基于令牌(API)通过查询参数传递令牌:createConsumer(\/cable?token=${token}`)`

**常见错误:**让 connect 保持空实现,或不调用 reject_unauthorized_connection。这会让未经身份验证的用户订阅任意频道。

第 3 步:创建频道

使用生成器:

bin/rails generate channel Chat

这会创建:

  • app/channels/chat_channel.rb — 服务端频道
  • app/javascript/channels/chat_channel.js — 客户端订阅(如果使用 importmap/jsbundling)
  • 测试文件

带流的频道:

# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  def subscribed
    # stream_from — string-based stream name (manual)
    stream_from "chat_#{params[:room_id]}"

    # OR stream_for — model-based stream name (preferred for model association)
    # room = Room.find(params[:room_id])
    # stream_for room
  end

  def unsubscribed
    # Cleanup when client disconnects
  end

  # Custom actions callable from client via perform()
  def speak(data)
    Message.create!(
      room_id: params[:room_id],
      user: current_user,
      body: data["body"]
    )
  end
end
第 4 步:选择流类型

stream_from 与 stream_for:

方法适用场景流名称
stream_from "chat_#{id}"手动控制字符串、非模型流由你定义
stream_for room基于模型的流,与 broadcast_to 配合使用根据 GlobalID 自动生成

**当流对应一个模型时,优先使用 stream_for。**它更安全(使用 GlobalID,而不是可猜测的 ID),并且能与 broadcast_to 自然配合。

# stream_for + broadcast_to (preferred for model streams)
class NotificationsChannel < ApplicationCable::Channel
  def subscribed
    stream_for current_user
  end
end

# Broadcasting from anywhere:
NotificationsChannel.broadcast_to(user, { title: "New message", body: "..." })

# stream_from + broadcast (for non-model or shared streams)
class DashboardChannel < ApplicationCable::Channel
  def subscribed
    stream_from "dashboard_#{params[:team_id]}"
  end
end

# Broadcasting from anywhere:
ActionCable.server.broadcast("dashboard_#{team.id}", { metric: "users", value: 42 })
第 5 步:客户端订阅
// app/javascript/channels/consumer.js
import { createConsumer } from "@rails/actioncable"
export default createConsumer()
// app/javascript/channels/chat_channel.js
import consumer from "./consumer"

const chatChannel = consumer.subscriptions.create(
  { channel: "ChatChannel", room_id: roomId },
  {
    // Called when subscription is confirmed by server
    connected() {
      console.log("Connected to chat")
    },

    // Called when WebSocket closes
    disconnected() {
      console.log("Disconnected from chat")
    },

    // Called when server broadcasts data
    received(data) {
      const messagesEl = document.getElementById("messages")
      messagesEl.insertAdjacentHTML("beforeend", data.html)
    },

    // Custom method — calls server-side ChatChannel#speak
    speak(body) {
      this.perform("speak", { body: body })
    }
  }
)

客户端的关键回调:

  • connected() — 订阅已确认,WebSocket 已连接
  • disconnected() — WebSocket 已关闭(网络问题、服务器重启)
  • received(data) — 服务器通过广播发送了数据
  • rejected() — 服务器拒绝了订阅
  • this.perform("action", data) — 调用服务端频道方法
  • this.send(data) — 发送原始数据(需要服务端实现 receive 方法)
步骤 6:广播

从模型/控制器/作业中:

# Using broadcast (string stream name)
ActionCable.server.broadcast("chat_#{room.id}", {
  html: render_to_string(partial: "messages/message", locals: { message: message }),
  sent_by: message.user.name
})

# Using broadcast_to (model stream — pairs with stream_for)
ChatChannel.broadcast_to(room, {
  html: render_to_string(partial: "messages/message", locals: { message: message }),
  sent_by: message.user.name
})

**常见错误:**广播过多数据。只发送客户端需要的内容。优先使用渲染后的 HTML 局部模板或最精简的 JSON,而不是包含关联数据的完整模型属性。

在后台作业中广播,用于繁重的渲染工作:

class MessageBroadcastJob < ApplicationJob
  def perform(message)
    html = ApplicationController.renderer.render(
      partial: "messages/message",
      locals: { message: message }
    )
    ActionCable.server.broadcast("chat_#{message.room_id}", { html: html })
  end
end
步骤 7:配置适配器

Rails 8 默认配置:Solid Cable(基于数据库,无需 Redis)

bin/rails solid_cable:install

这会配置 config/cable.yml 并创建 db/cable_schema.rb。更新 config/database.yml 以添加 cable 数据库。

config/cable.yml — 快速设置:

development:
  adapter: async

test:
  adapter: test

production:
  adapter: solid_cable
  connects_to:
    database:
      writing: cable
  polling_interval: 0.1.seconds
  message_retention: 1.day

**适配器选项:**Solid Cable(默认,基于数据库)、Redis(吞吐量最高)、PostgreSQL(负载限制为 8KB)。完整的适配器对比和配置示例,请参阅 references/adapters-config.md。

步骤 8:生产环境配置
# config/environments/production.rb
Rails.application.configure do
  # Mount path (default: /cable)
  config.action_cable.mount_path = "/cable"

  # Allowed origins (REQUIRED in production)
  config.action_cable.allowed_request_origins = [
    "https://yourdomain.com",
    %r{https://.*\.yourdomain\.com}
  ]

  # Worker pool (match to available DB connections)
  config.action_cable.worker_pool_size = 4

  # URL for standalone cable server (if separated)
  # config.action_cable.url = "wss://cable.yourdomain.com"
end

在布局中添加 meta 标签:


<head>
  <%= action_cable_meta_tag %>
</head>
步骤 9:处理断开连接

服务端清理:

class PresenceChannel < ApplicationCable::Channel
  def subscribed
    stream_from "presence"
    current_user.update!(online: true)
  end

  def unsubscribed
    current_user.update!(online: false)
  end
end

**客户端会自动重新连接。**Action Cable 会以指数退避方式重连。但请处理好 UI 状态:

connected() {
  document.getElementById("status").textContent = "Connected"
},

disconnected() {
  document.getElementById("status").textContent = "Reconnecting..."
}

**常见错误:**在跟踪在线状态或持有资源时,没有实现 unsubscribed。务必做好清理。

步骤 10:测试

频道测试(单元测试):

class ChatChannelTest < ActionCable::Channel::TestCase
  test "subscribes to room stream" do
    subscribe room_id: rooms(:general).id
    assert subscription.confirmed?
    assert_has_stream "chat_#{rooms(:general).id}"
  end

  test "rejects without room_id" do
    subscribe room_id: nil
    assert subscription.rejected?
  end
end

关于连接测试、stream_for 断言和广播断言模式,请参阅 references/testing.md。

快速参考

生成器
bin/rails generate channel ChannelName [action1 action2]
# Example:
bin/rails generate channel Chat speak
服务端 API
# Stream methods (in subscribed)
stream_from "stream_name"                    # String-based stream
stream_for model_instance                    # Model-based stream (GlobalID)

# Broadcasting (from anywhere)
ActionCable.server.broadcast("stream_name", data)   # To string stream
MyChannel.broadcast_to(model_instance, data)         # To model stream

# Connection identifiers
identified_by :current_user                  # In Connection class
current_user                                 # Available in channels

# Reject
reject                                       # Reject subscription (in subscribed)
reject_unauthorized_connection               # Reject connection (in connect)

# Disconnect a user (from anywhere)
ActionCable.server.remote_connections.where(current_user: user).disconnect
客户端 API
// Create subscription
const sub = consumer.subscriptions.create(
  { channel: "ChatChannel", room_id: 1 },
  { connected() {}, disconnected() {}, received(data) {} }
)

// Call server action
sub.perform("speak", { body: "Hello" })

// Send raw data (needs server `receive` method)
sub.send({ body: "Hello" })

// Unsubscribe
sub.unsubscribe()

// Enable logging
ActionCable.logger.enabled = true
Stimulus + Action Cable 模式

完整的 Stimulus + Action Cable 集成模式及控制器示例,请参阅 references/client-side.md。

应避免的反模式

  1. Turbo Streams 足以满足需求时仍使用 Action Cable — 如果只需要在模型变更时广播并更新 DOM,请在模型上使用 broadcasts_to
  2. 未认证的连接 — 务必实现 connect,并调用 reject_unauthorized_connection
  3. 广播整个模型 — 发送最精简的数据或预渲染的 HTML,而不是 model.as_json
  4. 没有 unsubscribed 清理逻辑 — 如果在 subscribed 中跟踪状态,就要在 unsubscribed 中清理
  5. 在频道中同步执行繁重工作 — 对于开销大的广播,请使用后台作业
  6. 在生产环境中使用 async 适配器 — 它仅支持单进程;请使用 Solid Cable 或 Redis
  7. 缺少 allowed_request_origins — 生产环境中必须配置,以防止跨源劫持
  8. 在流名称中使用用户输入进行字符串插值 — 在调用 stream_from "chat_#{params[:room]}" 前验证/清理参数
  9. 未在客户端处理 disconnected() — 连接断开时,用户需要获得直观反馈
  10. 不使用生成器创建频道 — bin/rails generate channel 会正确设置服务端和客户端文件

调试技巧

# Server-side: check active connections
ActionCable.server.connections.length

# Server-side: log tags for debugging
# config/environments/development.rb
config.action_cable.log_tags = [
  -> request { request.env["user_account_id"] || "no-account" },
  :action_cable,
  -> request { request.uuid }
]
// Client-side: enable logging
import * as ActionCable from "@rails/actioncable"
ActionCable.logger.enabled = true

// Check connection state
consumer.connection.isOpen()
consumer.connection.isActive()
# Check cable config
cat config/cable.yml

# Verify WebSocket endpoint
curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: test" \
  http://localhost:3000/cable

详细模式、边界情况和高级配置请参阅 references/ 目录:

  • references/connection-auth.md — 身份验证模式(Devise、令牌、JWT、多标识符)
  • references/channels.md — 频道模式、回调、动态流
  • references/broadcasting.md — 从模型、控制器广播,以及定向/条件广播
  • references/client-side.md — Stimulus 集成、重连、与 Turbo Streams 的对比
  • references/adapters-config.md — Solid Cable、Redis、PostgreSQL 配置和部署
  • references/testing.md — 频道、连接、广播和系统测试模式
  • references/patterns.md — 在线状态、输入指示器、进度跟踪、错误处理、安全

评论

支持 Markdown