🎃

「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. ブロードキャスト

特定のストリームを購読している すべてのブラウザへデータを送信 することです。

実際にコードを見ていく

app/javascript/controllers/typing_controller.js
一部抜粋
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 : "ないないことわざ"
    }
  }  
}
app/channels/typing_channel.rb
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の処理の流れを見てきました。
ザックリ理解している部分が多くあります。
もし間違いなどがあれば、教えていただけると幸いです。

参考になった記事

https://railsguides.jp/action_cable_overview.html

https://codezine.jp/article/detail/17960?utm_source=chatgpt.com

Discussion