我々が開発している異文化間交流教育支援ツールDialogbookは学生・生徒と教員の間でコメントをやりとりするチャット機能を備えている.ところが,これまでの実装では旧来の「行って来い」型のプロトコルで実装していたため,新しいメッセージが投稿されても他者がリロードしないと画面には現れないというオールドファッションドなものとなっていた.
しかし,イマドキのアプリである以上は,既存のSNSやチャットツールのように,リアルタイムでやりとりしたい.コメントを投稿したら,すぐに相手の画面にも出るようにするのは,もはや当たり前の機能だろう.
そこで,そのようなリアルタイムチャット機能をDialogbookにも実装することにした.DialogbookはRailsアプリとして作られている.イマドキだと,Turbo Streamsというものを用いれば簡単に実装できるらしい.というわけで,まずはそのTurbo Streamsを理解するために,ちょっとしたチャットアプリを作ってみることにした.
準備
まずはRailsアプリを用意する.次の手順でOKである.
- mkdir chat
- cd chat
- bundle init
- Gemfileを編集(# rails ... のコメントアウトを外す)
- bundle install
- bundle exec rails new . -f
続いて,モデルとコントローラを作成する.あくまで習作なので,テキストの本体だけを持つ単純なモデルを作る.
- bin/rails g model message body:text
- bin/rails g controller messages index create
- bin/rails db:migrate
ルーティングも単純なものでよい.config/routes.rb を次のように修正する.
Rails.application.routes.draw do
root "messages#index"
resources :messages, only: [ :index, :create ]
end
ここまでできたら,動作を確認しよう.bin/rails server して,ブラウザからlocalhost:3000にアクセス.問題なければなんらかの画面が出てくるはずである.
コントローラとビューの作成
コントローラのコードは次のようなものである.
app/controllers/ messages_controller.rb を次のようなコードで作成する.
class MessagesController < ApplicationController
def index
@messages = Message.all.order(created_at: :asc)
end
def create
p = message_params
Message.create!(p)
redirect_to root_path
end
private
def message_params
params.require(:message).permit(:body)
end
end
ビューはとりあえず箇条書きで表示するだけにしよう.
app/views/messages/index.html.erb のコードは次のとおりとする.
<h1>Messages</h1>
<ul>
<% @messages.each do |m| %>
<li><%= m.body %></li>
<% end %>
</ul>
テストデータを喰わせて,動作確認しよう.db/seeds.rb に次のコードを追加し,bin/rails db:seed でデータを投入する.
Message.create!(body: "The 1st message.")
Message.create!(body: "The 2nd message.")
Message.create!(body: "The 3rd message.")
再び localhost:3000 にアクセス(あるいは,先ほどのページをリロード)して,メッセージが羅列されることを確認しよう.
フォームの実装
次はメッセージを投稿できるようにする.ビューにフォームを追加する.
app/views/messages/index.html.erb を以下のようにしよう.
<h1>Messages</h1>
<ul>
<% @messages.each do |m| %>
<li><%= m.body %></li>
<% end %>
</ul>
<h2>Add a new message</h2>
<%= form_with model: Message.new do |f| %>
<%= f.text_field :body %>
<%= f.submit "send" %>
<% end %>
投稿後の処理はすでに記述済みなので,コントローラを更新する必要はない.ただし,エラー処理など全くしていないことには注意してほしい.あくまで習作である.
テストは,二つのブラウザからテストする.同じブラウザのタブやウィンドウを2個開き,それぞれからアクセスすればよい.実際にフォームにコメントを入れて送信してみれば,メッセージが追加されることを確認できるだろう.ただし,相手側に投稿したメッセージを表示するには,リロードが必要であることも確認されたい.
Turbo Streamsの追加
ビューに下記を追記する.ハイライトしている部分が追記部分である.Turbo Streamsのmessageというチャネルを使うよという指示(1行目)と,書き換え対象のDOM要素を明示的に指定するためのidの追加(3行目)である.
<%= turbo_stream_from :message %>
<h1>Messages</h1>
<ul id="messages">
<% @messages.each do |m| %>
<li><%= m.body %></li>
<% end %>
</ul>
<h2>Add a new message</h2>
<%= form_with model: Message.new do |f| %>
<%= f.text_field :body %>
<%= f.submit "send" %>
<% end %>
モデルにも細工する.モデルは作っただけで空だったが,モデルのコード(app/models/message.rb)を次のようにする.
class Message < ApplicationRecord
after_create_commit -> {
broadcast_append_to(
:message,
target: :messages,
partial: "messages/message",
locals: { message: self }
)
}
end
モデルのレコードが作成され,データベースにコミットされたらbroadcast_append_toメソッドを実施しなさいという処理である.この引数は,messageというチャネルを介してブロードキャストし,書き換え対象はmessages,書き換える部分的なHTMLはmessages/messageにパーシャルとして用意してあるものを使い,そこでのローカル変数messageとして自分自身のデータを使えというものだ.
したがってパーシャルも用意しなければならない.
app/views/messages/_message.html.erbを用意する.内容は次のようなもの,1行だけである.
<li><%= message.body %></li>
これで準備が整った.二つのブラウザからテストし,メッセージを追加してみよう.追加されたメッセージがリアルタイムに反映されていることを確認できたら,めでたし,めでたし,である.
以前,紹介したActionCableをベタ書きするものよりはるかに簡単な実装手順となっていることがわかるだろう.
サーバにデプロイする際の留意点
今回の実装は,Apache + Passengerでサーバに実装している際に,うまく動かないという問題が顕在化した.解決策としては別ポートでAction Cable専用のRailsアプリのインスタンスをPuma経由で動作させ,Apacheのバーチャルホスト設定でWebSocket関連のパケットをProxy / Reverse Proxyでパススルーさせるというもの.その詳細は本稿では割愛するが,実際に運用する際には,十分に留意されたい.




