SHOEISHA iD

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

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

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

Rails 7でリアルタイム通信を実現! Action Cableの基本をチュートリアルとともに理解しよう

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

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

基本的なチャットルームアプリを作成してみる

 Action Cableによるリアルタイム通信の動きを理解するために、まずは基本的なチャットルームアプリを作成してみましょう。このアプリは、サーバに接続した複数のクライアント(Webブラウザ)間で、入力した名前とメッセージを共有してやり取りするというものです(図2)。クライアントが送信したメッセージが、チャネルを介して全クライアントに送信されるという流れだけに絞って見るために、メッセージの保存は行いません。メッセージを保存するチャットルームアプリは、この回の後半で紹介します。

図2:基本的なチャットルームアプリの外観
図2:基本的なチャットルームアプリの外観

 アプリケーションの構造を図3に示します。Action Cableによるアプリケーションは構造がやや複雑なので、以降の解説は、この図を適宜参照しながら読むことをおすすめします。

図3:基本的なチャットルームアプリの構成
図3:基本的なチャットルームアプリの構成

アプリケーションの下地を作成する

 まずは下地となるアプリケーションを作成します。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要素をビューに追加します。自動生成された内容を、以下のように書き換えます。

リスト app/views/chatroom/show.html.erb
<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]を入力してサーバを停止しておきます。

図4:チャットルームアプリの初期ページ
図4:チャットルームアプリの初期ページ

チャットメッセージのモデルを作成する

 下地のアプリケーションができたので、ここからチャットルームアプリのためのコードを追加していきましょう。ここでは、サーバ側の処理の一つとして、チャットメッセージを保存するMessageモデルをScaffoldingで作成します。前半で作成するアプリはメッセージを保存しないのでモデルは必須ではありませんが、後半に備えてあらかじめモデルを使ったコードとしておくことにします。モデルを使ってのメッセージのやり取りや、メッセージのレンダリングのために部分ビューを使っていきます。Messageモデルには、送信日時(published:datetime)、送信者(sender:text)、内容(content:text)の3つのフィールドを持たせます。モデルを作成したら、マイグレーションを実行しておきます。

% rails generate scaffold message published:datetime sender:text content:text
% rails db:migrate

 モデルのレンダリングのための既定の部分ビューは縦に長く、チャット画面には向かないので、以下のように横並びで日時も短く表示されるようにしておきます。

リスト app/views/messages/_message.html.erb
<div id="<%= dom_id message %>">
  <p><%= message.published.strftime("%Y/%m/%d %H:%M:%S") %>: 
    <%= message.sender %>: <%= message.content %></p>
</div>

 また、モデルとは直接の関係はないですが、日時が日本時間となるようにタイムゾーンを変更しておきましょう。

リスト config/application.rb
…略…
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ファイル

 コントローラのチャネル版のような位置付けで、サブスクライブ時やアンサブスクライブ時に実行する処理や、クライアントがチャネルに実行させたいアクションを記述します。既定の内容は以下の通りです。

リスト 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ファイル

 クライアント側の定型処理が記述されています。

リスト 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ファイルを見てみます。

リスト 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)以降のクライアントからメッセージを送られるときの処理です。

リスト app/channels/chatroom_channel.rb
…略…
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を、以下のように修正、追加します。

リスト 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の新機能を抜粋して紹介します。

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

  • X ポスト
  • このエントリーをはてなブックマークに追加
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編 」他、著書多数

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

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

この記事をシェア

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

おすすめ

アクセスランキング

アクセスランキング

イベント

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

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

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

メールバックナンバー

アクセスランキング

アクセスランキング