先週の Ruby Weekly で紹介があったYoutube動画『Build a Twitter clone in 10 minutes with Rails, CableReady, and StimulusReflex』を参考に、Rails・StimulusReflex・CableReady を使って簡単なチャット機能を実装してみました。
出来上がりはこんな感じです↓
ソースコードは GitHub をご覧ください。
環境
ruby -v
ruby 2.6.4p104 (2019-08-28 revision 67798) [x86_64-darwin18]
rails -v
Rails 6.0.2.2
用語の整理
StimulusReflex
StimulusReflex は、Ruby on Rails 用の JavaScript フレームワークで、Single Page App (SPA) ライクにリアクティブな挙動をシンプルに実現することを目指しています。
CableReady
CableReady は、ActionCable を使った複数クラアイント間の DOM 更新処理を Ruby で書くためのライブラリです。
手順
Rails・StimulusReflex・CableReady を使った簡単なチャット機能の実装手順を整理していきます。
1. Railsアプリの作成
まず、Railsアプリを新規作成します。オプションで --webpack=stimulus
を指定してください。
rails new --skip-spring --webpack=stimulus app_name
続いて、Redis、CableReady、StimulusReflex をインストールします。
bundle add redis cable_ready stimulus_reflex
bundle install
yarn add cable_ready stimulus_reflex
bundle exec rails stimulus_reflex:install
2. チャットページの作成
StimulusReflexとCableReadyの実装を除く、単純なチャットページの部分から作成します。
スキャフォールドで Messeage
モデルを作成します。
bundle exec rails g scaffold Message username body like_count:integer --no-jbuilder
生成されるマイグレーションファイルを編集して、デフォルト値をセットします。
class CreateMessages < ActiveRecord::Migration[6.0]
def change
create_table :messages do |t|
t.string :username, default: 'Anonymous'
t.string :body
t.integer :like_count, default: 0
t.timestamps
end
end
end
マイグレーションを適用させます。
rails db:migrate
MessagesController のアクションを index
create
のみにし、ロジックを次のように修正します。
class MessagesController < ApplicationController
def index
@messages = Message.all.order(created_at: :desc)
@message = Message.new
end
def create
Message.create(message_params)
redirect_to messages_path
end
private
def message_params
params.require(:message).permit(:username, :body)
end
end
今回は、SemanticUI のCSSスタイルを使うので、レイアウトテンプレートを次のように修正します。
<!DOCTYPE html>
<html>
<head>
<title>StimulusReflexCableReadyDemo</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<div class="ui centered grid">
<%= yield %>
</div>
</body>
</html>
index
のビューテンプレートを調整します。
<div class="fourteen wide ten wide tablet seven wide computer column">
<div class="ui divider hidden"></div>
<h1>Messages</h1>
<%= render 'form' %>
<div id="messages" class="ui comments">
<%= render @messages %>
</div>
<div class="ui divider"></div>
<div class="ui divider hidden"></div>
</div>
<%= form_with(model: @message, class: 'ui form') do |form| %>
<div class="field">
<%= form.label :username, 'Your Name' %>
<%= form.text_field :username %>
</div>
<div class="field">
<%= form.label :body %>
<%= form.text_area :body, rows: 3, placeholder: 'Say something...', id: 'message-form-body' %>
</div>
<div class="actions">
<%= form.submit 'Post', class: 'ui button fluid' %>
</div>
<% end %>
<div class="ui divider"></div>
<div class="comment">
<div class="content">
<div class="author"><%= message.username %></div>
<div class="metadata">
<span class="date"><%= time_ago_in_words message.created_at %></span>
</div>
<div class="text">
<%= message.body %>
</div>
<div class="actions">
<%= link_to "Like #{message.like_count}", '#', class: 'reply' %>
</div>
</div>
</div>
create
を非同期で呼び出した場合の処理も書きます。
document.getElementById('message-form-body').value = ''
最後にルーティングを調整すれば、単純なチャットページの部分は完成です。
Rails.application.routes.draw do
resources :messages, only: %i[index create]
root 'messages#index'
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
挙動を確認します。
rails s
メッセージが新規作成されればOKです。ただし、リロードしない限り、別のブラウザに変更が反映されません。
3. CableReadyでフロードキャストさせる
ブラウザ間の変更がリアルタイムで反映されるように、CableReady を使って ActionCable の処理を書いていきます。
ApplicationCable::Connection
を次のように修正し、 session_id
で各ブラウザを識別できるようにします。
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :session_id
def connect
self.session_id = request.session.id
end
end
end
続いて、Messageチャンネルを作成します。
bundle exec rails g channel Message
unsubscribed
は不要なので削除し、subscribed
で利用する _channel.js
の名前を指定します。
class MessageChannel < ApplicationCable::Channel
def subscribed
stream_from 'message'
end
end
message_channel.js
でウェブソケットが受け取るデータを CableReady に渡します。
import CableReady from 'cable_ready'
import consumer from "./consumer"
consumer.subscriptions.create("MessageChannel", {
received(data) {
if (data.cableReady) CableReady.perform(data.operations)
}
});
MessagesController に include CableReady::Broadcaster
を追加し、CableReady を使ってブロードキャストさせます。
class MessagesController < ApplicationController
include CableReady::Broadcaster
# 略
def create
message = Message.create(message_params)
cable_ready['message'].insert_adjacent_html(
selector: '#messages',
position: 'afterbegin',
html: render_to_string(partial: 'message', locals: { message: message })
)
cable_ready.broadcast
end
# 略
end
- 利用可能な DOM オペレーションの詳細は公式ドキュメントを参照してください。
挙動を確認します。
rails s
メッセージが新規投稿された場合、その変更がブラウザ間でリアルタイムに反映されるようになりました。
4. StimulusReflex で非同期処理を書く
メッセージに対する Like アクションも、ブラウザ間でリアルタイムに反映されるように調整します。
StimulusReflex を使って MessagesReflex を生成します。
bundle exec rails g stimulus_reflex Messages
そして like
アクションを次のように実装します。
# frozen_string_literal: true
class MessagesReflex < ApplicationReflex
include CableReady::Broadcaster
def like
message = Message.find(element.dataset[:id])
message.increment! :like_count
cable_ready['message'].text_content(
selector: "#message-#{message.id}-likes",
text: message.like_count
)
cable_ready.broadcast
end
end
メッセージの Like のアクション部分から MessagesReflex の処理を呼び出すように修正します。
<div class="actions">
<%= tag.div class: 'ui button mini blue label',
data: { reflex: 'click->MessagesReflex#like',
id: message.id,
controller: 'stimulus-reflex',
action: 'click->stimulus-reflex#__perform' } do %>
Like <%= tag.span message.like_count, id: "message-#{message.id}-likes" %>
<% end %>
</div>
(補足) data-controller
と data-action
は、通常省略できます(data-reflex
の値を受けてそれぞれ描画時に自動生成されるため)。今回は、別ブラウザにブロードキャストした同要素にこのデータアトリビュートが追加されなかったので、このように明記することで解決しました。もっと良いやり方があれば教えてください。
以上で Like の部分もリアルタイムで更新されるようになるはずです。
挙動を確認します。
rails s
それぞれの変更が更新されればOKです!