基本的なチャットルームアプリを作成してみる
Action Cableによるリアルタイム通信の動きを理解するために、まずは基本的なチャットルームアプリを作成してみましょう。このアプリは、サーバに接続した複数のクライアント(Webブラウザ)間で、入力した名前とメッセージを共有してやり取りするというものです(図2)。クライアントが送信したメッセージが、チャネルを介して全クライアントに送信されるという流れだけに絞って見るために、メッセージの保存は行いません。メッセージを保存するチャットルームアプリは、この回の後半で紹介します。
アプリケーションの構造を図3に示します。Action Cableによるアプリケーションは構造がやや複雑なので、以降の解説は、この図を適宜参照しながら読むことをおすすめします。
アプリケーションの下地を作成する
まずは下地となるアプリケーションを作成します。Action Cableの動作に集中するために、5つの--skip-xxxxオプションを指定して、チャットアプリに不要な機能は外してしまうことにします。
% rails new basic_chatroom_app --skip-hotwire --skip-action-text --skip-action-mailer --skip-action-mailbox --skip-jbuilder
ここでbasic_chatroom_appフォルダに移動し、chatroomコントローラを作成します。chatroomコントローラは、チャットルームの画面を表示するためのもので、表示のためのshowアクションのみを生成します。
% rails generate controller chatroom show
この時点では、showメソッドの中身は空のままでかまいません。
続けて、ビューでチャットメッセージを入力、表示できるようにしておきます。そのためのフォームとdiv要素をビューに追加します。自動生成された内容を、以下のように書き換えます。
<h1>チャットルーム</h1> <!-- チャットメッセージの入力フォーム --> <form> (1) <label for="sender">お名前:</label> <input type="text" id="sender" size="16" /><br /> <label for="content">メッセージ:</label> <input type="text" id="content" size="50" /><br /> </form> <!-- チャットメッセージの表示場所 --> <div id ='messages'> (2) </div>
(1)の入力フォームは、メッセージ一覧の上に置くことにします。フォームの要素は、メッセージの送信者名、メッセージ本体の2つのinput要素です。
(2)は、メッセージの置き場所です。初期状態ではもちろん空です。ここに、JavaScriptコードによってメッセージ受信の都度、動的にメッセージを挿入していきます。
ここでrails serverコマンドでサーバを起動し、アプリケーションを実行してみます。Webブラウザで「http://localhost:3000/chatroom/show」にアクセスして、図4のようにタイトルと入力フォームだけのページが表示されれば、基本部分はきちんと動作しています。確認したら、[Ctrl]+[C]を入力してサーバを停止しておきます。
チャットメッセージのモデルを作成する
下地のアプリケーションができたので、ここからチャットルームアプリのためのコードを追加していきましょう。ここでは、サーバ側の処理の一つとして、チャットメッセージを保存するMessageモデルをScaffoldingで作成します。前半で作成するアプリはメッセージを保存しないのでモデルは必須ではありませんが、後半に備えてあらかじめモデルを使ったコードとしておくことにします。モデルを使ってのメッセージのやり取りや、メッセージのレンダリングのために部分ビューを使っていきます。Messageモデルには、送信日時(published:datetime)、送信者(sender:text)、内容(content:text)の3つのフィールドを持たせます。モデルを作成したら、マイグレーションを実行しておきます。
% rails generate scaffold message published:datetime sender:text content:text % rails db:migrate
モデルのレンダリングのための既定の部分ビューは縦に長く、チャット画面には向かないので、以下のように横並びで日時も短く表示されるようにしておきます。
<div id="<%= dom_id message %>"> <p><%= message.published.strftime("%Y/%m/%d %H:%M:%S") %>: <%= message.sender %>: <%= message.content %></p> </div>
また、モデルとは直接の関係はないですが、日時が日本時間となるようにタイムゾーンを変更しておきましょう。
…略… module CableApp class Application < Rails::Application config.load_defaults 7.0 …略… config.time_zone = "Tokyo" (1) end end
(1)により、タイムゾーンが東京に変更されます。もちろん、実行場所に合わせてもらって構いません。
チャネルを作成する
ここから、Action Cableの機能に踏み込んでいきます。Action Cableによるリアルタイム通信では、前述のとおりチャネルが中心的な役割を果たします。サーバ側の処理として、このチャネルを以下のコマンドでチャネル名をchatroomとして作成します。
% rails generate channel chatroom
これによって、app/channels/chatroom_channel.rbファイルやapp/javascript/channels/chatroom_channel.jsファイルなど、リアルタイム通信に必要な最低限のファイルが作成されます。
app/channels/chatroom_channel.rbファイル
コントローラのチャネル版のような位置付けで、サブスクライブ時やアンサブスクライブ時に実行する処理や、クライアントがチャネルに実行させたいアクションを記述します。既定の内容は以下の通りです。
class ChatroomChannel < ApplicationCable::Channel (1) def subscribed (2) end def unsubscribed (3) end end
(1)でApplicationCable::Channelを継承したChatroomChannelクラスを定義し、(2)で購読開始時の処理を記述するsubscribedメソッド、(3)で購読解除時の処理を記述するunsubscribedメソッドをそれぞれ定義しています。いずれも中身は空なので、購読開始時も購読解除時も何も実行されません。実際に双方向通信を行うためには、ここに必要な処理を記述していきますが、これは後ほど紹介します。
app/javascript/channels/chatroom_channel.jsファイル
クライアント側の定型処理が記述されています。
import consumer from "channels/consumer" (1) consumer.subscriptions.create("ChatroomChannel", { (2) connected() { (3) }, disconnected() { (3) }, received(data) { (4) } })
(1)ではconsumerオブジェクトをインポートしています。文字通りクライアント(コンシューマ)側の処理を受け持つオブジェクトです。
(2)では、consumer.subscriptions.createメソッドによってChatroomChannelチャネルへのサブスクリプションを作成しています。このとき、(3)接続時、(4)切断時、(4)データ受信時のコールバック関数を渡しています。既定ではそれぞれの処理内容は空なので、接続時も切断時もデータ受信時も、何も実行されません。チャネルと同様に、双方向通信のためにはここに必要な処理を追記していくことになります。
[NOTE]Action CableはImport Mapsが前提
上記の(1)に記述されているimport文は、アプリケーションがImport Maps(第2回を参照)で構成されていることを前提としています。このために、config/importmap.rbファイルを見てみます。
pin "application", preload: true pin "@rails/actioncable", to: "actioncable.esm.js" (1) pin_all_from "app/javascript/channels", under: "channels" (2)
(1)はインストールされているAction Cableのモジュールのピン留め、(3)はapp/javascript/channels以下をchannelsとしてピン留めする指定です。特に(2)では、app/javascript/channels/chatroom_channel.js中のimport文で指定されるchannels/consumerがapp/javascript/channels/consumer.jsに解決されるために必要な記述となっています。
サーバ側の処理:チャネルに必要な処理を実装する
サーバ側の処理として、チャネルに処理を実装していきます。必要なのは、(1)の購読開始時の処理、そして(2)以降のクライアントからメッセージを送られるときの処理です。
…略… def subscribed # chatroom_channelからのストリーミングを開始する stream_from 'chatroom_channel' (1) end …略… # クライアントがメッセージ送信に使うメソッド def talk(data) (2) # クライアントからのデータをもとにmessageモデルを組み立てる(現在日時を使用) message = Message.new message.published = Time.now message.sender = data['sender'] message.content = data['content'] # Messageモデルをレンダリングしてchatroom_channelへブロードキャストする ActionCable.server.broadcast 'chatroom_channel', {message: render_message(message)} end private # Messageモデルをレンダリングするプライベートメソッド def render_message(message) (3) ApplicationController.renderer.render(partial: 'messages/message', locals: { message: message }) end …略…
subscribedメソッドに追加した(1)は、chatroom_channelからのストリーミングを開始する、という意味です。これにより、ストリームchatroom_channelにサブスクライブする全てのクライアントが、チャットメッセージの配信を受けられるようになります。なお、ここでストリームに指定されているchatroom_channelは、チャネル名と一致している必要はありません。クライアントがサブスクリプション作成時に指定するオプションによって、クライアントごとに異なるストリームを指定するということもできます。
(2)で定義されているtalkメソッドは、クライアントから呼び出されてチャットメッセージの送信を行います。チャットルームアプリでは、クライアントからのアクションはチャットメッセージの送信だけなので、それに対応するメソッドも1個だけとなっています。
talkメソッドの内容は、チャットメッセージのためのMessageオブジェクトを生成し、それを現在日時、送信者、メッセージ本体で初期化し、ActionCable.server.broadcastメソッドにてchatroom_channelにブロードキャストするというものです。talkメソッドの引数dataはクライアントから送信された連想配列であり、そこには送信者とメッセージ本体が含まれているので、それをMessageオブジェクトにセットして、Messageモデルの部分ビューを使ってレンダリングしたものを最終的にブロードキャストします。このとき、ブロードキャストする内容は配列であり、その内容は(3)で部分ビューを用いてレンダリングされたものであることを押さえておいてください。
クライアント側の処理:購読開始してチャットメッセージを送信・表示する
クライアント側では、app/javascript/channels/chatroom_channel.jsを、以下のように修正、追加します。
…略… const appChat = consumer.subscriptions.create("ChatroomChannel", { (1) …略… received(data) { const messages = document.getElementById('messages'); (2) messages.insertAdjacentHTML('afterbegin', data['message']); }, talk(data) { (3) return this.perform('talk', {data: data}); } }); document.getElementById('content').addEventListener('keyup', (e) => { (4) if (e.key === 'Enter') { const sender = document.getElementById('sender'); appChat.talk({sender: sender.value, content: e.target.value}); e.target.value = ''; e.preventDefault(); } })
(1)では、consumer.subscriptions.createメソッドの戻り値をappChat変数に代入しています。これは、(4)のイベントハンドラ内でappChatオブジェクトを参照するためです。
(2)では、データ受信時の処理を実装しています。ビューに作成したdiv要素(id属性は"messages")を取得し、その内部の先頭にdata['message']をinsertAdjacentHTMLメソッドで挿入しています。dataは配列であり、チャネルのtalkメソッドがmessageキーでブロードキャストしたメッセージ内容に相当します。
(3)は、チャネルのtalkメソッドを呼び出すメソッドです。ここで使われているperformは、クライアントがチャネルのメソッドを呼び出すためのメソッドです。処理としては、チャネルのtalkメソッドをメッセージの配列を引数として呼び出しています。consumer.subscriptions.createメソッドの引数にするのは、このオブジェクトから呼び出す必要があるからです。
(4)は、メッセージ本文でEnterキーが押された場合のイベントリスナーです。送信者名を取得し、上記のtalkメソッドをappChatオブジェクトを通じて呼び出します。これで、送信者名とメッセージ本文がチャネルに送信されます。送信後は、メッセージ本文の入力欄をクリアしてコントロールの既定の動作を無効にしています。
ここで、アプリケーションを実行してみます。サーバを起動後、Webブラウザを2個起動し、それぞれで異なる送信者名を入れてメッセージを送信し合うと、各々にリアルタイムにメッセージが表示されることが図2のように確認できるはずです。
まとめ
今回は、アプリケーションにリアルタイム通信を導入できるAction Cableについて、その概要と基本的なアプリケーションの作成を紹介しました。次回は後半として、アプリをメッセージ保存版にする例を紹介し、近々リリースが予定されているRails 7.1の新機能を抜粋して紹介します。