Rails6で作成したポートフォリオにAction Cableを利用したコメント機能を実装しました。
備忘録のため、実装方法を記載します。
開発環境
- MacOS Catalina 10.15.7
- Ruby 2.6.5
- Ruby on Rails 6.0.3.4
Action Cableとは
そもそもAction Cableとは、Railsガイドによると
WebSocketとRailsのその他の部分をシームレスに統合するためのもの
とのこと。
WebSocketは通信規格の一種。
Webアプリケーションにおいて、クライアント/サーバ間のデータ通信を実現するための規格。
現時点では、リアルタイムの双方向通信をイイ感じに実現してくれる規格とでも覚えておきます。
今回実装する要件
既存アプリの概要は、記事を投稿・閲覧するアプリ。
今回は既存アプリの詳細画面上で、コメントを入力・閲覧できる機能を実装します。
コメント閲覧は誰でも可能。
コメント入力はログイン済みのユーザーのみ可能。
画面遷移図
赤い「コメント入力機能」の部分を実装していきます。
ER図
既存のテーブルは、ユーザー管理を行う「users」と、投稿内容を記録していく「records」。
今回、コメント機能用に「comments」テーブルを新規作成します。
いざ実装
1.Commentsテーブルの作成
最初に、Commentsテーブルやモデル周りを実装していきます。
ターミナルを開きrails g modelコマンドでモデルやマイグレーションファイルを生成します。
rails g model comment
次に、マイグレーションファイルを修正。
コメント投稿機能では、記事の内容と、コメント投稿するユーザーを紐づける必要があったため、既存の外部キーをそれぞれ設定します。
class CreateComments < ActiveRecord::Migration[6.0]
def change
create_table :comments do |t|
# 追記ここから
t.references :record, null: false, foreign_key: true
t.references :user, null: false, foreign_key: true
t.text :comment_text, null: false
# 追記ここまで
t.timestamps
end
end
end
そしてmodelファイルを修正。
まずは新しく作成したcommentモデルを修正します。
recordモデルやuserモデルとは1対他の関係となるため、アソシエーションはbelongs_to(commentが属する側)で設定しました。
コメント内容が何も無いときは投稿できないように、バリデーションはpresence: trueを記述します。
class Comment < ApplicationRecord
# アソシエーション
belongs_to :record
belongs_to :user
# バリデーション
validates :comment_text, presence: true
end
次に、既存モデル(recordとuser)にもアソシエーションを追記します。
class Record < ApplicationRecord
# アソシエーション
belongs_to :user
has_many :comments # add コメント機能
# (省略)
end
class User < ApplicationRecord
# アソシエーション
has_many :records
has_many :comments # add コメント機能
# (省略)
end
ターミナルでdb:migrateを実行し、データベースに変更内容を適用します。
rails db:migrate
これでデータベースにcommentテーブルが作成されたはず。
念のため、本当にcommentテーブルが作成されたか確認します。
なおデータベースはMySQLを使用しています。
データベース名は仮でhoge_developmentとしてあります。
mysql -h localhost -u root # データベースに接続
# 以下、MySQLに接続した後、hoge_developmentのテーブル一覧を表示
mysql> show tables hoge_development;
+---------------------------------------+
| Tables_in_hoge_development |
+---------------------------------------+
| # (省略) |
| comments |
| records |
| users |
+---------------------------------------+
# 想定通りの構成になっているか、commentsテーブルのカラムも確認
mysql> show columns from comments from hoge_development;
+--------------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+-------------+------+-----+---------+----------------+
| id | bigint(20) | NO | PRI | NULL | auto_increment |
| record_id | bigint(20) | NO | MUL | NULL | |
| user_id | bigint(20) | NO | MUL | NULL | |
| comment_text | text | NO | | NULL | |
| created_at | datetime(6) | NO | | NULL | |
| updated_at | datetime(6) | NO | | NULL | |
+--------------+-------------+------+-----+---------+----------------+
# 確認できたのでMySqlからログアウト
mysql> exit
Bye
想定通りにテーブル作成できたことが確認できました。
以上で、コメントを保存するためのテーブル作成は完了です。
2.ルーティング
次にルーティングを設定。
コメントは各投稿に紐づく形になるので、投稿機能であるrecordsコントローラにネストさせます。
recordsのshowアクションで表示されたViewに対しコメント入力欄を表示させます。
そのため、commentsにはnewアクションは作成しません。
コメント入力後に送信ボタンが押されたタイミングで、コメント投稿処理を行うようcreateアクションのみ記述します。
Rails.application.routes.draw do
devise_for :users
root to: 'records#index'
resources :records, only: [ :index, :new, :create, :show, :edit, :update ] do
resources :comments, only: :create # add コメント機能追加
end
end
記述後、ルーティングを確認。
これで、投稿記録のrecord_idに紐づくコメントを作成するコントローラーとアクション(comments#create)を設定できました。
rails routes
# 以下、実行結果
Prefix Verb URI Pattern Controller#Action
record_comments POST /records/:record_id/comments(.:format) comments#create
# (省略)
3.コメント表示の作成
コメントの入力・閲覧機能のViewは詳細画面(records#show)に同居させるため、コメント機能用のViewファイルは生成不要です。
コメント欄を部分テンプレート化して作成し、records#showで読み込むことにしました。
records/show.html.erbに、部分テンプレートを読み込むコードを書きます。
<%# コメント欄 %>
<%= render "shared/comment" %>
部分テンプレート化に対応するViewファイルを作成します。
部分テンプレートをまとめている「shared」というディレクトリ内に「_comment.html.erb」というファイルを作成しました。
続いてコメント一覧表示欄と入力フォームを書いていきます。
コメント一覧の表示部分は、登録されたコメントを「コメント【コメント書いたユーザーニックネーム】コメント登録日時」のように表示できるように記述しました。
<div>
<h3>コメント</h3>
<div id="comments">
<% if @comments.length != 0 %>
<% @comments.each do |comment| %>
<p><%= comment.comment_text %> 【<%= comment.user.nickname %>】<%= comment.created_at %></p>
<% end %>
<% else %>
<p>まだコメントはありません。</p>
<% end %>
</div>
<div>
投稿内容に紐づくコメント(recordに紐づくcomment)データを取得するため、records#showに追記。
def show
@record = Record.find(params[:id])
@comments = @record.comments # add コメント機能のため追記
end
これでコメント一覧を表示できるようになりました。
続けて、コメント入力欄を作成します。
コメント投稿を行う処理は「comments#create」に記述するので、form_withで指定するurlは「record_comments_path(@record.id)」を指定しました。
<div>
<h3>コメント</h3>
<%# (コメント一覧表示部分は省略) %>
<%# コメント入力欄 ここから #%>
<%# ログイン済みならコメント投稿フォームを表示 %>
<% if user_signed_in? %>
<%= form_with model: @comment, url: record_comments_path(@record.id), method: :post, local: true do |f| %>
<%= f.text_field :comment_text, placeholder:"(ここにコメントを記入してください)" %>
<%= f.submit "コメントを投稿する" %>
<% end %>
<% end %>
<%# コメント入力欄 ここまで %>
<div>
実は上記のform_withの記述に1点誤りがあります。
それは後ほど気づいて直すことになります…
records#showにコメント入力欄に関するコードを書きます。
def show
@record = Record.find(params[:id])
@comments = @record.comments
# add コメント入力欄を以下に追加。
if user_signed_in?
@comment = Comment.new
end
end
これで表示部分の基礎は完成。
次からコメント登録処理を作成していきます。
4.Commentsコントローラの作成
対応するControlerとアクションを作成していきます。
先ほどrecords#showに対応するViewを作成済みなので、Commentsコントローラーの生成時にhtml.erbやscssファイルが作成されないよう、skipオプションを使いました。
rails g controller comments --skip-template-engine --skip-assets
# 以下、作成されたファイル類
Running via Spring preloader in process 6189
create app/controllers/comments_controller.rb
invoke rspec
create spec/requests/comments_request_spec.rb
invoke helper
create app/helpers/comments_helper.rb
invoke rspec
create spec/helpers/comments_helper_spec.rb
コメント入力フォームの情報だけを受け取れるよう、privateメソッドにストロングパラメータを定義していきます。
class CommentsController < ApplicationController
def create
@record = Record.find(params[:record_id])
@comment = @record.comments.build(comment_params)
end
private
def comment_params
params.require(:comment).permit(:comment_text, :record_id, :user_id).merge(user_id: current_user.id)
end
end
ここまでで、コメント投稿を行う基礎ができました。
次からやっと今回の本題であるAction Cableの実装に入ります。
5.Action Cableの実装
Action Cable用のファイルをrails g channelコマンドで生成します。
今回はcommentというチャネル名で作成しました。
rails g channel comment
# 以下、実行結果
Running via Spring preloader in process 10355
invoke rspec
create spec/channels/comment_channel_spec.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
サーバからクライアントにデータを渡すために、ストリーム名を記述。
class CommentChannel < ApplicationCable::Channel
def subscribed
stream_from "comment_channel" # add
end
def unsubscribed
end
end
「コメントを投稿する」ボタンを押した後、comments#createで処理が行われます。
commentsテーブルへのInsert処理が成功したとき、ActionCableで処理します。
使用するストリーム名と、受け渡すデータを記述しました。
class CommentsController < ApplicationController
def create
@record = Record.find(params[:record_id])
@comment = @record.comments.build(comment_params)
# 追記 ここから。commentsテーブルへの書き込みが成功したらActionCableを利用
if @comment.save
ActionCable.server.broadcast 'comment_channel', content: @comment, user: @comment.user, date: @comment.created_at
end
# 追記 ここまで
end
private
# (省略)
end
クライアント側にデータが受け渡った後、コメント一覧の表示が更新されるように「received(data)」部分に追記します。
import consumer from "./consumer"
consumer.subscriptions.create("CommentChannel", {
connected() {
},
disconnected() {
},
received(data) {
// Called when there's incoming data on the websocket for this channel
// (=このチャネルのWebSocketに受信データが入ってきた場合に呼び出されます)
const html = `<p>${data.content.comment_text} 【${data.user.nickname}】 ${data.date}</p>`;
const comments = document.getElementById('comments');
const newComment = document.getElementById('comment_comment_text');
// <div id="comments">欄に追記する。
comments.insertAdjacentHTML('beforeend', html);
// コメント入力欄を空にする。
newComment.value='';
}
});
これでひとまず完成。
あとは動作確認です。
6.動作確認
ローカル環境で動作確認したところ、
コメントを投稿するとリロードすることなく、内容が即時にコメント一覧に反映されるようになりました!
しかし一点、問題が発覚…
一度ボタンを押して投稿すると、再度ボタンが押せなくなる事象が発生…
リロードすればもちろん再度コメント投稿できるのですが、それでは意味がない。
原因は、コメント入力欄のform_withの設定にありました。
form_withのオプションで「local: true」としてしまったことが原因でした。
localオプションは、デフォルトではfalse。
非同期通信を行う設定をformタグに追記します。
falseで非同期通信がONとなる模様。
非同期通信させようとしていたのに、true=非同期通信OFFのような感じになってたのだろうか。
「local: false」に直したところ、リロードしなくても何度でもコメント投稿できるようになりました。
<%# コメント入力欄 ここから #%>
<% if user_signed_in? %>
<%# localをtrueからfalseに修正 #%>
<%= form_with model: @comment, url: record_comments_path(@record.id), method: :post, local: false do |f| %>
<%= f.text_field :comment_text, placeholder:"(ここにコメントを記入してください)" %>
<%= f.submit "コメントを投稿する" %>
<% end %>
<% end %>
<%# コメント入力欄 ここまで %>
最終的には、以下の動画のような感じにコメント機能を実装できました。
最後に
本番環境でAction Cableを運用するには、config/cable.ymlへの設定など、他にも確認することがありました。
AWSやHerokuなど、デプロイする環境によっても設定は変わるようです。
プログラミングの際は、環境による違いにも注意しながらプログラミングする必要性を再認識しました。