关于 action-cable
面向 Rails 开发者的 Action Cable 技能,涵盖 WebSocket 连接、频道、订阅、广播、认证、适配器和测试。它还说明何时应优先使用 Turbo Streams,而非原始 Action Cable。
主要功能
- 01配置 WebSocket 连接与频道
- 02实现订阅和广播
- 03处理连接认证与断开清理
- 04配置 Solid Cable、Redis 等适配器
- 05测试频道和广播
适用场景
- 01为 Rails 应用添加实时聊天
- 02实现在线状态或输入提示
- 03推送作业进度和实时数据
- 04为 Action Cable 频道编写测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill action-cable安装后在 Claude Code 中输入/action-cable确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/adapters-config.mdbroadcasting.mdchannels.mdclient-side.mdconnection-auth.mdpatterns.mdtesting.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"] |
| Devise | env["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。
应避免的反模式
- Turbo Streams 足以满足需求时仍使用 Action Cable — 如果只需要在模型变更时广播并更新 DOM,请在模型上使用
broadcasts_to - 未认证的连接 — 务必实现
connect,并调用reject_unauthorized_connection - 广播整个模型 — 发送最精简的数据或预渲染的 HTML,而不是
model.as_json - 没有
unsubscribed清理逻辑 — 如果在subscribed中跟踪状态,就要在unsubscribed中清理 - 在频道中同步执行繁重工作 — 对于开销大的广播,请使用后台作业
- 在生产环境中使用
async适配器 — 它仅支持单进程;请使用 Solid Cable 或 Redis - 缺少
allowed_request_origins— 生产环境中必须配置,以防止跨源劫持 - 在流名称中使用用户输入进行字符串插值 — 在调用
stream_from "chat_#{params[:room]}"前验证/清理参数 - 未在客户端处理
disconnected()— 连接断开时,用户需要获得直观反馈 - 不使用生成器创建频道 —
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— 在线状态、输入指示器、进度跟踪、错误处理、安全