Action Cableを利用してリアルタイムでコメントできる機能を実装します。
今回もレシピアプリを例に作成していきます。
##Action Cableとは
Action Cableは、 WebSocketとRailsのその他の部分をシームレスに統合するためのものです。Action Cable が導入されたことで、Rails アプリケーションの効率の良さとスケーラビリティを損なわずに、通常のRailsアプリケーションと同じスタイル・方法でリアルタイム機能をRubyで記述できます。クライアント側のJavaScriptフレームワークとサーバー側のRubyフレームワークを同時に提供する、フルスタックのフレームワークです。
出典:Railsガイド
なるほど、わからん!
つまりは、通常のRailsのアプリケーションと同様の記述で、即時更新機能を実装できるフレームワークで、メッセージの保存や送信するためにはRubyのコーディングが必要で、保存したメッセージを即時に表示させるためにはJavaScriptのコーディングが必要になります。
##コメント機能の実装
まずは、通常通りコメント機能を実装していきます。
####コメントモデルの作成
rails g model comment
今回、コメントをtext型でtextカラムとしreferences型でuserとrecipeに紐付けます
class CreateComments < ActiveRecord::Migration[6.0]
def change
create_table :comments do |t|
#ここから追記
t.text :text, null: false
t.references :user, foreign_key: true
t.references :recipe, foreign_key: true
#ここまで追記
t.timestamps
end
end
end
rails db:migrate
####アソシエーション、バリデーションの設定
class User < ApplicationRecord
has_many :recipes, dependent: :destroy
has_many :comments, dependent: :destroy #追記
#以下略
class Recipe < ApplicationRecord
#中略
has_many :comments, dependent: :destroy #追記
has_many_attached :images
#以下略
class Comment < ApplicationRecord
#ここから追記
belongs_to :user
belongs_to :recipe
validates :text, presence: true
#ここまで追記
end
####コントローラーの作成、ルーティングの設定
rails g controller comments
コメントはレシピに紐づくのでネストさせます。
Rails.application.routes.draw do
devise_for :users
root to: 'home#index'
resources :recipes do
resources :comments #追記
collection do
get :search
get :result
end
end
end
####コントローラーの編集
コントローラーにコメントを保存するための記述をしていきます。
class RecipesController < ApplicationController
#中略
def show
@recipe = Recipe.find(params[:id])
@comment = Comment.new #追記
@comments = @recipe.comments.order(created_at: :desc) #追記
end
#以下略
end
class CommentsController < ApplicationController
def create
@comment = Comment.new(comment_params)
if @comment.valid?
@comment.save
redirect_to recipe_path(@comment.recipe)
else
redirect_to recipe_path(@comment.recipe)
end
end
private
def comment_params
params.require(:comment).permit(:text).merge(user_id: current_user.id, recipe_id: params[:recipe_id])
end
end
####ビューファイルの編集
#中略
<div class="show-comment">
<div class="recipe-comments text-center">コメント一覧</div>
<div id="comments">
<% @comments.each do |comment| %>
<div class="comment"><%= comment.text %></div>
<% end %>
</div>
<div class="text-center">
<%= form_with model: [@recipe, @comment],local: true do |f| %>
<label class="text-secondary">コメントする</label><br>
<%= f.text_field :text, class: "comment-form" %>
<%= f.submit '送信', class: "btn btn-primary btn-sm" %>
<% end %>
</div>
</div>
これでコメント機能の実装は完了です。
ここまでで一度、挙動を確認してみましょう。
このままでも機能的には問題ないですが、リロードに若干の時間がかかります。
Action Cableを使って即時更新してみましょう。
##Action Cableの実装
####Channel(チャネル)の作成
チャネルとは
チャネル (Channel) は、論理的な作業単位をカプセル化します。通常のMVC設定でコントローラが果たす役割と似ています。Railsはデフォルトで、チャネル間で共有されるロジックをカプセル化するApplicationCable::Channelという親クラスを作成します。
出典:Railsガイド
全くわからん!!!
ざっくりとした説明になりますが、チャネルはAction Cable専用のファイルでMVCのコントローラーと役割が似ています。
チャネルを作成していきましょう。
rails g channel comment
以下のようにファイルが生成されます。
Running via Spring preloader in process 39620
invoke test_unit
create test/channels/comment_channel_test.rb
create app/channels/comment_channel.rb
identical app/javascript/channels/index.js
identical app/javascript/channels/consumer.js
create app/javascript/channels/comment_channel.js
ここで生成されたcomment_channel.rbがクライアントとサーバーを結びつけるためのファイルで、comment_channel.jsがサーバーから送られてきたデータをクライアントの画面に描画するためのファイルになります。
####comment_channel.rbの編集
comment_channel.rbを以下のように編集します。
class CommentChannel < ApplicationCable::Channel
def subscribed
stream_from "comment_channel" #編集
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
####コントローラーの編集
redirect_toの記述を変更します。
class CommentsController < ApplicationController
def create
@comment = Comment.new(comment_params)
if @comment.valid?
@comment.save
ActionCable.server.broadcast "comment_channel", comment: @comment #変更
else
redirect_to recipe_path(@comment.recipe)
end
end
#以下略
broadcastはサーバーから送られてくるデータの経路を指し、comment_channel.rbのstream_fromメソッドと関連づいています。
@commentはcommentに格納されbroadcastを通じcomment_channel.rbのcomment_channelへ送られます。
そして、送られた情報は、comment_channel.jsで受け取ります。
####comment_channel.jsの編集
受け取った情報は、receivedの引数dataに渡されます。
この受け取ったdataを利用してコメントを描画します。
まず、dataをテンプレートリテラルにして定数htmlに格納します。
getElementByIdでコメントを表示させる場所のid "comments"を取得し定数commentsに格納します。
次にgetElementByIdでコメント入力フォームのidを取得し、定数newCommentに格納します。
そして、insertAdjacentHTMLで定数htmlを定数commentsに挿入します。
最後に入力フォームに入力された値をvalue = ''で削除します。
import consumer from "./consumer"
consumer.subscriptions.create("CommentChannel", {
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 html = `<p>${data.comment.text}</p>`;
const comments = document.getElementById('comments');
const newComment = document.getElementById('comment_text');
comments.insertAdjacentHTML('afterbegin', html);
newComment.value = '';
//ここまで追記
}
});
以上で完成になります。