はじめに
WebアプリケーションフレームワークのRuby on Railsは、2021年12月にバージョン7となりました。これに伴い、クライアントサイド開発のサポートについても大きな変化を遂げ、多様な選択肢が提供されるようになりました。本連載では、このRails 7にフォーカスし、クライアントサイド開発のためのさまざまな機能を、API開発やリアルタイムWeb開発も絡めながら、紹介していきます。
対象読者
- Ruby on Railsを長らく使ってきた方
- 他のWebアプリケーション開発フレームワークを使ってきた方
- Railsにおけるフロントエンド開発に関心のある方
必要な環境
本記事のサンプルコードは、以下の環境で動作を確認しています。
-
macOS Monterey
- Ruby 3.1.0p0
- Ruby on Rails 7.0.4.2
- Google Chrome 112
メッセージ保存版チャットルームアプリ
第8回で紹介したサンプルは、チャットメッセージをDBなどに保存する仕組みを持たないので、チャットに参加する前のチャット履歴が分からないといった問題があります。そこで、チャットメッセージを保存して、チャット参加時にそれまでのメッセージを復元できるようにしてみましょう。アプリケーションの構造を図1に示します。以降の解説は、この図を適宜参照しながら読むことをおすすめします。必要なのはサーバ側の処理のみで、クライアント側の変更は一切必要ありません。
アプリケーションは、新規にadv_chatroom_appとしてbasic_chatroom_appと同様に作成し、以降の修正を施していきます。
サーバ側の処理:ビューで保存メッセージを全て表示する
chatroomコントローラのshowアクションメソッドで、Messageモデルの全レコードを取得して表示するようにします。これで、Webブラウザから初めてチャットルームにアクセスした際に、保存されているメッセージが全て表示されるようになります。メッセージは、送信日時の降順すなわち新しい順で取得します。メッセージの内容はmessagesインスタンス変数に入ります。
…略… def show # 全レコードをpublishedフィールドの降順で取得する @messages = Message.order('published DESC') end …略…
これを受けて、ビューで@messagesの内容を表示できるようにしておきます。
<h1>チャットルーム</h1> …略… <div id ='messages'> <%= render @messages %> (1) </div>
追加するのは1行で、(1)で@messagesの内容が全て表示されるようになります。第8回で作成した非保存版のチャットルームアプリで、DOMに挿入する内容をMessageモデルの部分ビューでレンダリングしているので、同じ見た目でチャット画面に反映できるというわけです。
サーバ側の処理:メッセージを保存してからブロードキャストする
チャネルでは、クライアントからのメッセージをただちにブロードキャストしていました。これを、いったん保存してからブロードキャストするようにします。このために、まずはチャネルのsendメソッドの処理内容を、ブロードキャストからモデルの保存に変更します。
…略… def talk(data) message = Message.new message.published = Time.now message.sender = data['sender'] message.content = data['content'] Message.create! published: message.published, sender: message.sender, (1) content: message.content end …略…
(1)を、ブロードキャストからモデルの保存に変更しています。これにより、クライアントからのメッセージ送信はいったんモデルに保存されるようになります(ブロードキャストする処理がなくなったので、そこから呼び出していたrender_messageメソッドは不要になり、後述するMessageBroadcastJobクラスに移動することになります)。このままただちにブロードキャストしても良いのですが、モデルの保存が確実になった後に各クライアントに反映されるのが望ましいので、Messageモデルにコミット後の処理として追加します。
class Message < ApplicationRecord after_create_commit { MessageBroadcastJob.perform_later self } (1) end
(1)によって、Createアクションのコミット後に、MessageBroadcastJob.perform_laterメソッド(引数はモデル自身)をコールバックすることが指示されます。MessageBroadcastJobはActive Jobのクラスであり、コミット後に実行する処理を遅延実行するために作成します。このmessage_broadcastジョブを以下のように作成します。
% rails generate job message_broadcast
コマンドの実行で、app/jobs/message_broadcast_job.rbファイルが生成されます。ここに、コミット後に実行する処理(ブロードキャスト)を記述します。
class MessageBroadcastJob < ApplicationJob queue_as :default # メッセージを受け取ってブロードキャストする def perform(message) (1) ActionCable.server.broadcast 'chatroom_channel', {message: render_message(message)} end private def render_message(message) (2) ApplicationController.renderer.render(partial: 'messages/message', locals: { message: message }) end end
(1)は、Messageモデルに記述したafter_create_commitメソッドによって実行されるメソッドとなります。処理の内容は、前述のチャネルにおけるtalkメソッドと基本的には同じで、ここのperformメソッドのようにモデルを受け取るか、talkメソッドのように生成するかの違いだけです。いずれも、モデルをブロードキャストするのは変わりません。
(2)は、(1)でブロードキャストする内容をレンダリングするメソッドで、これも前述のチャネルにおけるものと変わりません。チャネルに記述されていたものをそのまま移動してきただけです。
アプリケーションを実行して動作を追ってみる
ここで、第8回と同様にアプリケーションを起動し、Webブラウザを2個以上開いてチャットを試すことができます。内部的な処理を変更しただけなので、見た目は全く変わりません。そこで、Railsがコンソールに書き出すログを見ながら、処理の流れを追っていきましょう。
「しの」さんが「おひさしぶりです」とWebブラウザから送ってきたので、ChatroomChannelチャネルのtalkメソッドが呼び出されています。
ChatroomChannel#talk({"sender"=>"しの", "content"=>"おひさしぶりです"})
トランザクションの開始からコミットまでです。Message.create!メソッドによって送信されたメッセージが保存されてコミットされました。ここまでが、talkメソッドの終了までに実行されています。
TRANSACTION (0.0ms) begin transaction ↳ app/channels/chatroom_channel.rb:14:in `talk' Message Create (0.6ms) INSERT INTO "messages" ("published", "sender", "content", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["published", "2023-05-15 06:55:46.438370"], ["sender", "しの"], ["content", "おひさしぶりです"], ["created_at", "2023-05-15 06:55:46.438624"], ["updated_at", "2023-05-15 06:55:46.438624"]] ↳ app/channels/chatroom_channel.rb:14:in `talk' TRANSACTION (0.6ms) commit transaction ↳ app/channels/chatroom_channel.rb:14:in `talk'
コミットされましたので、MessageBroadcastJob.perform_laterメソッドがコールバックにより実行されました。以下はMessageBroadcastJob内で実行されている処理になります。冗長なのでジョブIDは省略していますが、全て同一です。ジョブのエンキュー、メッセージの読み出し、エンキューされたジョブの実行、メッセージのレンダリングとブロードキャストが実行されています。
[ActiveJob] Enqueued MessageBroadcastJob (Job ID: 4003378a-…) to Async(default) with arguments: #<GlobalID:0x0000… @uri=#<URI::GID gid://adv-chatroom-app/Message/4>> [ActiveJob] [MessageBroadcastJob] [4003378a-…] Message Load (0.1ms) SELECT "messages".* FROM "messages" WHERE "messages"."id" = ? LIMIT ? [["id", 4], ["LIMIT", 1]] [ActiveJob] [MessageBroadcastJob] [4003378a-…] Performing MessageBroadcastJob (Job ID: 4003378a-…) from Async(default) enqueued at 2023-05-15T06:55:46Z with arguments: #<GlobalID:0x0000… @uri=#<URI::GID gid://adv-chatroom-app/Message/4>> [ActiveJob] [MessageBroadcastJob] [4003378a-…] Rendered messages/_message.html.erb (Duration: 0.1ms | Allocations: 47) [ActiveJob] [MessageBroadcastJob] [4003378a-…] [ActionCable] Broadcasting to chatroom_channel: {:message=>"<div id=\"message_4\">\n <p>2023/05/15 15:55:46: \n しの: おひさしぶりです</p>\n</div>\n"} [ActiveJob] [MessageBroadcastJob] [4003378a-…] Performed MessageBroadcastJob (Job ID: 4003378a-…) from Async(default) in 2.16ms
最後に、ブロードキャストしたメッセージが2回送信されます。これは、チャネルにサブスクライブしているクライアントが2つあるからです。つまり、クライアントが増えれば送信がその分だけ実行されることになります。
ChatroomChannel transmitting {"message"=>"<div id=\"message_4\">\n <p>2023/05/15 15:55:46: \n しの: おひさしぶりです</p>\n</div>\n"} (via streamed from chatroom_channel) …全く同じなので省略…
以上で、Action Cableによるチャットルームアプリの紹介は終わりです。メッセージをWebブラウザから送信してから、チャネルを介して受信するまでの流れがやや複雑で、関連するファイルも多いので難しそうに見えますが、一度押さえてしまえばいろいろなパターンのリアルタイム通信アプリに対応できそうなことがお分かりいただけたのではないでしょうか。今回はユーザ名を手入力するなどしていましたが、diviseを導入して認証させるなどすると、より実用的なアプリになるでしょう。