「ActionCable と Stimulus の処理の流れを理解する:リアルタイム通信をコードで追う」
こんにちは
プログラミングスクールで Ruby on Rails を学習している、りゅうと言うものです。
卒業制作でWebアプリを作っている中で、ActionCableとStimulusを使ってリアルタイム通信を実装したので、その備忘録として書いていきます。
⚠️ 注意
プログラミング初心者のため、誤りがあるかもしれません。
また、環境によっては同じ手順でもうまくいかない場合があります。
もしお気づきの点があれば、コメントなどで教えていただけると嬉しいです!
🧩 使用環境(バージョン情報)
| ツール | バージョン |
|---|---|
| Ruby | 3.2.3 |
| Rails | 7.2.2.2 |
| PostgreSQL | 17.6 |
| 開発環境 | Docker |
ActionCableの理解
Action Cableは、WebSocketとRailsのその他の部分をシームレスに統合します。Action Cableを導入すると、Rails アプリケーションのパフォーマンスとスケーラビリティを損なわずに、通常のRailsアプリケーションと同じスタイル・方法でリアルタイム機能をRubyで記述できるようになります。Action Cableはフルスタックのフレームワークであり、クライアント側のJavaScriptフレームワークとサーバー側のRubyフレームワークの両方を提供します。Active RecordなどのORMで書かれたドメインモデル全体にアクセスできます。
簡単に言うと、ActionCable という フルスタックのフレームワーク を使えば、
Rails で簡単にリアルタイム通信が実現できるということです。
🧰 用語の簡単説明
-
フレームワークとは?
→便利な機能がたくさんあるドラえもんのポケットみたいなもの -
フルスタックとは?
→ここでは、クライアント側、サーバー側両方の処理を提供していくれるの意味
🚦 ActionCable の通信イメージ
ActionCable は、各ブラウザと WebSocket で常時接続しておき、
あるブラウザから届いたメッセージを 同じ部屋(ストリーム)にいる他のブラウザへ配信する中継役 です。
WebSocket は 「サーバーとブラウザが 双方向 に通信するためのプロトコル(約束事)」 です。
ブラウザA →(WebSocket)→ ActionCable →(WebSocket)→ ブラウザB
🔑 ActionCable を理解する4つのキーワード
1. コネクション
ここではざっくり 「ブラウザと ActionCable が繋がる(購読を開始する)」 イメージです。
2. チャンネル
チャンネルは 「機能の種類」 を表すクラスです。
例:TypingChannel は「タイピングプレビュー用のチャンネル」
3. ストリーム
チャンネルの中で、
どの“部屋(room_id)”と接続するか を明示します。
1つのチャンネルの中に
- 部屋A
- 部屋B
…という複数のストリームが存在するイメージです。
4. ブロードキャスト
特定のストリームを購読している すべてのブラウザへデータを送信 することです。
実際にコードを見ていく
一部抜粋
import { Controller } from "@hotwired/stimulus"
import consumer from "../channels/consumer"
export default class extends Controller {
static values = {
roomId: Number,
}
static targets = [
"word1",
"title",
"word1Preview",
"titlePreview",
]
connect() {
this.subscription = consumer.subscriptions.create(
{
channel: "TypingChannel",
room_id: this.roomIdValue,
},
{
received: (data) => this.updatePreviewFromServer(data),
},
)
}
disconnect() {
if (this.subscription) {
consumer.subscriptions.remove(this.subscription)
}
}
syncFromForm() {
const payload = {}
if (this.hasWord1Target) payload.word1 = this.word1Target.value
if (this.hasTitleTarget) payload.title = this.titleTarget.value
this.subscription.send(payload)
this.applyPreview(payload)
}
updatePreviewFromServer(data) {
this.applyPreview(data)
}
applyPreview(data) {
if (this.hasWord1PreviewTarget && "word1" in data) {
this.word1PreviewTarget.textContent =
data.word1 && data.word1.length > 0 ? data.word1 : "まだありません"
}
if (this.hasTitlePreviewTarget && "title" in data) {
this.titlePreviewTarget.textContent =
data.title && data.title.length > 0 ? data.title : "ないないことわざ"
}
}
}
class TypingChannel < ApplicationCable::Channel
def subscribed
stream_from "typing_channel_#{params[:room_id]}"
end
def receive(data)
ActionCable.server.broadcast("typing_channel_#{params[:room_id]}", data)
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
StimulusとTypingChannelクラスで以上の処理を書いています。
これがどうなっていくか見ていきます。
connectメソッドが発火して、consumer.subscriptions.createが実行されます。ここで、コネクションを確立し始めます。つまり、ブラウザとActionCableが繋がりを持ちます。
その中で、以下のコードがあります。
channel: "TypingChannel",
room_id: this.roomIdValue,
これは、購読するchannelはTypingChannelで、room_idには取得したroomIdValueの値を入れてねと明示しています。
consumer.subscriptions.createが実行されると、railsは自動的にTypingChannelクラスを探して、subscribedメソッドの処理をしてくれます。
def subscribed
stream_from "typing_channel_#{params[:room_id]}"
end
stream_from で、このチャンネル内で購読するストリームを確立します。
TypingChannel の中で「どの room_id のストリームを購読するか」を明示しています。
this.subscription.send(payload)
send をしたら、Rails は自動で
そのチャンネル内の receive(data) を呼び出します。
def receive(data)
ActionCable.server.broadcast("typing_channel_#{params[:room_id]}", data)
end
ここでデータを特定のストリームに送信しています。
さらに、この処理の後に同じストリームを購読している JS の
received: (data) => this.updatePreviewFromServer(data)
が 自動で発火します
ここの処理で実際に画面の文字がリアルタイムで表示される。
まとめ
ActionCableとStimulusの処理の流れを見てきました。
ザックリ理解している部分が多くあります。
もし間違いなどがあれば、教えていただけると幸いです。
参考になった記事
Discussion