事前準備
今回,下記の手順で動作を検証したバージョンは,Ruby,Railsともに次のとおりである.
$ ruby --version
ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [arm64-darwin20]
$ bin/rails --version
Rails 7.0.2.2
$
Railsの雛形を次の手順で用意する.
$ mkdir chatroom
$ cd chatroom
$ rbenv local 3.0.2
$ bundle init
Writing new Gemfile to /private/tmp/chatroom/Gemfile
$ vi Gemfile
Gemfileの # gem 'rails' の行,「# 」を削除してコメントアウトを外す.
$ bundle config set path vendor/bundle
$ bundle install
Fetching gem metadata from https://rubygems.org/...........
Resolving dependencies...
Fetching rake 13.0.6
Installing rake 13.0.6
Fetching minitest 5.15.0
(中略)
Fetching rails 7.0.2.2
Installing rails 7.0.2.2
Bundle complete! 1 Gemfile dependency, 47 gems now installed.
Bundled gems are installed into `./vendor/bundle`
$ bundle exec rails new . -f
exist
create README.md
create Rakefile
identical .ruby-version
create config.ru
create .gitignore
create .gitattributes
force Gemfile
run git init from "."
(中略)
Import Stimulus controllers
append app/javascript/application.js
Pin Stimulus
append config/importmap.rb
$
以上で準備はOK.
チャットルームへの第1歩
roomsコントローラを作る.とりあえず,今回は一つのルームで一つのページしか表示しないSingle Page Application (SPA)なので,showメソッドだけ作ればよい.
$ bin/rails g controller rooms show
create app/controllers/rooms_controller.rb
route get 'rooms/show'
invoke erb
create app/views/rooms
create app/views/rooms/show.html.erb
invoke test_unit
create test/controllers/rooms_controller_test.rb
invoke helper
create app/helpers/rooms_helper.rb
invoke test_unit
$
config/routes.rb を次のように修正する.localhost:3000/ にアクセスすると,Roomsのshowメソッドにルーティングされるように記述する.
Rails.application.routes.draw do
root to: 'rooms#show'
end
データベースを作る.
$ bin/rails db:create
Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'
$
今回はテストなので,デフォルトのSQLite3をそのまま使う.
確認
サーバを起動する.
$ bin/rails server
=> Booting Puma
=> Rails 7.0.2.2 application starting in development
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 5.6.2 (ruby 3.0.2-p107) ("Birdie's Version")
* Min threads: 5
* Max threads: 5
* Environment: development
* PID: 32482
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop
ブラウザを立ち上げて,localhost:3000 にアクセスする.
この画面が出ればOK.
メッセージのモデルを作成
メッセージのモデルを作る.文字列(text)型のbodyカラムを一つだけ持つ,たいへんシンプルなモデルである.
$ bin/rails g model message body:text
invoke active_record
create db/migrate/20220215115613_create_messages.rb
create app/models/message.rb
invoke test_unit
create test/models/message_test.rb
create test/fixtures/messages.yml
$ bin/rails db:migrate
== 20220215115613 CreateMessages: migrating ===================================
-- create_table(:messages)
-> 0.0006s
== 20220215115613 CreateMessages: migrated (0.0007s) ==========================
$
モデルを作成したあとは,マイグレーションを忘れずに.
チャットルームのビューとコントローラを作成
次はルームのビューとコントローラを作成する.既に先のコマンドで雛形が作成されている.
app/controllers/rooms_controllers.rb を次のように修正する.Mssageモデルから,全てを取ってきて(Message.all())@messageに入れている.
class RoomsController < ApplicationController
def show
@messages = Message.all()
end
end
部分レンダリングを使うので,app/views/messages ディレクトリを作成する.部分レンダリングとは,ページの一部分だけをレンダリングするものである.今回は多くのメッセージを順番に表示するので,そのメッセージの表示部分だけを部分テンプレートとして用意しておく.
そのディレクトリに app/views/messages/_message.html.erb を次の内容で作成する.これが部分テンプレートとなる.メッセージのボディ(内容)だけを表示する.
<div class="message">
<p><%= message.body %></p>
</div>
Roomsのビューも作る.app/views/rooms/show.html.erb を次の内容で作成する.
<h1>Chatroom</h1>
<div id ='messages'>
<%= render @messages %>
</div>
render @messages というコマンドで,@messages に含まれるメッセージを,部分レンダリングとして用意したものとしてまとめてレンダリングしてくれる.
テストデータの投入
Railsコンソールから,テストデータを投入する.
$ bin/rails c
Loading development environment (Rails 7.0.2.2)
irb(main):001:0> Message.create! body: '庭には二羽鶏がいる'
(0.7ms) SELECT sqlite_version(*)
TRANSACTION (0.0ms) begin transaction
Message Create (0.6ms) INSERT INTO "messages" ("body", "created_at", "updated_at") VALUES (?, ?, ?) [["body", "庭には二羽鶏がいる"], ["created_at", "2022-02-15 12:25:23.071306"], ["updated_at", "2022-02-15 12:25:23.071306"]]
TRANSACTION (0.5ms) commit transaction
=>
#<Message:0x0000000123e157e8
id: 1,
body: "庭には二羽鶏がいる",
created_at: Tue, 15 Feb 2022 12:25:23.071306000 UTC +00:00,
updated_at: Tue, 15 Feb 2022 12:25:23.071306000 UTC +00:00>
irb(main):002:0>
$
とりあえずは一つ用意しておけばよいだろう(複数,用意すれば,部分レンダリングがきちんと対応していることも確認できる).
確認
サーバを起動する.
$ bin/rails s
ブラウザを立ち上げて,localhost:3000 にアクセスする.
コンソールから入力したデータが表示されればOK.
フォームの作成
フォームを用意する.app/views/rooms/show.html.erb を次のように修正する.
<h1>Chatroom</h1>
<div id ='messages'>
<%= render @messages %>
</div>
<form>
<label>Leave your message:</label><br>
<input type="text" data-behavior="room_speaker">
</form>
Railsのフォーム作成用ヘルパーメソッドも使えるが,今回は割愛.
確認
サーバを起動する.
$ bin/rails s
ブラウザを立ち上げて,localhost:3000 にアクセスする.
フォームが表示されればOK.
ルーム・チャネルの作成
Roomに対するチャネルを次のコマンドで作成する.
$ bin/rails g channel room speak
invoke test_unit
create test/channels/room_channel_test.rb
identical app/channels/application_cable/channel.rb
identical app/channels/application_cable/connection.rb
create app/channels/room_channel.rb
create app/javascript/channels/index.js
create app/javascript/channels/consumer.js
append app/javascript/application.js
append config/importmap.rb
create app/javascript/channels/room_channel.js
gsub app/javascript/channels/room_channel.js
append app/javascript/channels/index.js
$
app/channels/room_channel.rb が今回使用するチャネルの主要部分である.
サーバ側の処理
app/channels/room_channel.rb を次のように編集する.
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from 'room_channel'
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def speak(data)
ActionCable.server.broadcast('room_channel',
{ message: data['message'] })
end
end
subscribedメソッドで’room_channel' からデータを取得すること,speakメソッドで各クライアントにbroadcastすることを,それぞれ記述する.
クライアント側の処理
app/javascript/channels/room_channel.js を次のように編集する.
import consumer from "channels/consumer"
const appRoom = consumer.subscriptions.create("RoomChannel", {
connected() {
// Called when the subscription is ready for use on the server
},
disconnected() {
// Called when the subscription has been terminated by the server
},
received(data) {
// Called when there's incoming data on the websocket for this channel
return alert(data['message']);
},
speak: function(message) {
return this.perform('speak', {message: message});
}
});
window.addEventListener('keypress', function(e) {
if (e.keyCode === 13) {
appRoom.speak(e.target.value);
e.target.value = '';
e.preventDefault();
}
})
この処理で,フォームからサーバに送られたデータがブロードキャストされて,各クライアントのreceived()が受け取り,それぞれでアラートが表示されるようになる.
サーバを起動し,ブラウザのウィンドウを複数立ち上げて,どこか一つからメッセージを投げると,全てのウィンドウでアラートが出ることを確認せよ.
あとは,受け取ったクライアント側で,アラートを出すのではなく,Chatのメッセージを表示するように画面を書き換えれば,リアルタイムチャットアプリの完成である.
データベースに記録するように修正
app/channels/room_channel.rb のspeak(data) を修正.
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from 'room_channel'
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def speak(data)
Message.create! body: data['message']
end
end
データベースに記録するように変更した.チャネルのspeak処理を消してしまったので,データベースにデータをコミットした後に非同期で動作する処理を追加し,そこでspeak処理を行うように修正する.
speak(data) で Message.create! した直後に,そのまま broadcast() してもよいが,クライアント数が多くなったときにそれらへのブロードキャスト対応を別途切り分けて実施することでシステムを安定して動作させることを狙っている.
app/models/message.rb を次のように修正する.
class Message < ApplicationRecord
after_create_commit { MessageBroadcastJob.perform_later self }
end
commitの後で(after_create_commit),次に作るジョブMessageBroadcastJob を実施する(perform_later)という記述である.
ジョブの作成
次の手順でジョブを作成する.
$ bin/rails g job MessageBroadcast
invoke test_unit
create test/jobs/message_broadcast_job_test.rb
create app/jobs/message_broadcast_job.rb
$
app/jobs/message_broadcast_job.rb を次のように修正する.
class MessageBroadcastJob < ApplicationJob
queue_as :default
def perform(message)
ActionCable.server.broadcast('room_channel',
{ message: ApplicationController.renderer
.render(partial: 'messages/message',
locals: { message: message }) })
end
end
ApplicationController.renderer.render() で,message がよしなにレンダリングされたものが,全てのクライアントにbroadcastされる.
クライアント側の修正
サーバから各クライアントに broadcast されたメッセージを,クライアント側のブラウザに表示されているHTMLに反映させるように,JavaScriptによるクライアント側の処理を修正する.
app/javascript/channels/room_channel.js を,次のように変更する.
変更するのは received(data) の部分.
import consumer from "channels/consumer"
const appRoom = consumer.subscriptions.create("RoomChannel", {
connected() {
// Called when the subscription is ready for use on the server
},
disconnected() {
// Called when the subscription has been terminated by the server
},
received(data) {
const messages = document.getElementById('messages');
messages.insertAdjacentHTML('beforeend', data['message']);
},
speak: function(message) {
return this.perform('speak', {message: message});
}
});
window.addEventListener('keypress', function(e) {
if (e.keyCode === 13) {
appRoom.speak(e.target.value);
e.target.value = '';
e.preventDefault();
}
})
以上で完成である.サーバを起動し,複数のブラウザを用意して,それぞれのブラウザからメッセージを入れて同期することを確認せよ.
完成
完成すると,こんな感じで動くよ.
本記事は「【Rails6.0】ActionCableを使用したライブチャットアプリを実装する手順を解説」というブログを参考にした(ただし記事のとおりやっても,Railsの最新版だとうまくいかなかったので,注意.若干の修正を加えている).そちらにも丁寧な解説が示されているので,参考にされたい.
0 件のコメント:
コメントを投稿