SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

Railsによるクライアントサイド開発入門

Rails 7でリアルタイムWeb開発! Action Cableの応用とRails 7.1の新機能について

Railsによるクライアントサイド開発入門 第9回

  • このエントリーをはてなブックマークに追加

 連載第8回と第9回では、Action Cableを使ったリアルタイム通信機能を持ったアプリケーションの開発について紹介します。後半となる今回は、第8回で作成したチャットルームアプリの機能強化の事例を紹介します。また、近くリリースが予定されているRails 7.1の新機能について、注目されるものを抜粋して紹介します。

  • このエントリーをはてなブックマークに追加

はじめに

 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に示します。以降の解説は、この図を適宜参照しながら読むことをおすすめします。必要なのはサーバ側の処理のみで、クライアント側の変更は一切必要ありません。

図1:メッセージ保存版チャットルームアプリの構成
図1:メッセージ保存版チャットルームアプリの構成

 アプリケーションは、新規にadv_chatroom_appとしてbasic_chatroom_appと同様に作成し、以降の修正を施していきます。

サーバ側の処理:ビューで保存メッセージを全て表示する

 chatroomコントローラのshowアクションメソッドで、Messageモデルの全レコードを取得して表示するようにします。これで、Webブラウザから初めてチャットルームにアクセスした際に、保存されているメッセージが全て表示されるようになります。メッセージは、送信日時の降順すなわち新しい順で取得します。メッセージの内容はmessagesインスタンス変数に入ります。

リスト app/controllers/chatroom_controller.rb
…略…
def show
  # 全レコードをpublishedフィールドの降順で取得する
  @messages = Message.order('published DESC')
end
…略…

 これを受けて、ビューで@messagesの内容を表示できるようにしておきます。

リスト app/views/chatroom/show.html.erb
<h1>チャットルーム</h1>
…略…
<div id ='messages'>
  <%= render @messages %>		(1)
</div>

 追加するのは1行で、(1)で@messagesの内容が全て表示されるようになります。第8回で作成した非保存版のチャットルームアプリで、DOMに挿入する内容をMessageモデルの部分ビューでレンダリングしているので、同じ見た目でチャット画面に反映できるというわけです。

サーバ側の処理:メッセージを保存してからブロードキャストする

 チャネルでは、クライアントからのメッセージをただちにブロードキャストしていました。これを、いったん保存してからブロードキャストするようにします。このために、まずはチャネルのsendメソッドの処理内容を、ブロードキャストからモデルの保存に変更します。

リスト app/views/chatroom/show.html.erb
…略…
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モデルにコミット後の処理として追加します。

リスト app/models/message.rb
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ファイルが生成されます。ここに、コミット後に実行する処理(ブロードキャスト)を記述します。

リスト 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を導入して認証させるなどすると、より実用的なアプリになるでしょう。

会員登録無料すると、続きをお読みいただけます

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

次のページ
アプリケーションにDockerfileが!? Rails 7.1の新機能について

この記事は参考になりましたか?

  • このエントリーをはてなブックマークに追加
Railsによるクライアントサイド開発入門連載記事一覧

もっと読む

この記事の著者

WINGSプロジェクト 山内 直(WINGSプロジェクト ヤマウチ ナオ)

WINGSプロジェクトについて> 有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表 山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。2018年11月時点での登録メンバは55名で、現在も執筆メンバを募集中。興味のある方は、どしどし応募頂きたい。著書記事多数。 RSS Twitter: @yyamada(公式)、@yyamada/wings(メンバーリスト) Facebook <個人紹介> WINGSプロジェクト所属のテクニカルライター。出版社を経てフリーランスとして独立。ライター、エディター、デベロッパー、講師業に従事。屋号は「たまデジ。」。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

山田 祥寛(ヤマダ ヨシヒロ)

静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。主な著書に「独習シリーズ(Java・C#・Python・PHP・Ruby・JSP&サーブレットなど)」「速習シリーズ(ASP.NET Core・Vue.js・React・TypeScript・ECMAScript、Laravelなど)」「改訂3版JavaScript本格入門」「これからはじめるReact実践入門」「はじめてのAndroidアプリ開発 Kotlin編 」他、著書多数

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/18263 2023/09/14 11:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング