【Rails6】Action Cableを用いた即時コメント機能の実装

#はじめに

今回はActionCableというフレームワークを用いたコメント機能を実装していきます。

#環境

Ruby on Rails '6.0.0'
Ruby '2.6.5'

#前提

記事投稿機能(articleテーブル)
ユーザー管理機能（userテーブル）
上記を実装済みであり、ユーザーは記事を閲覧できる。

#ActionCableとは

通常のrailsのアプリケーションと同様の記述で、即時更新機能を実装できるフレームワーク。メッセージの保存や送信に必要なRubyのコーディングと、保存したメッセージを即時に表示させるJavaScriptのコーディングが必要。

#ajaxによる非同期通信との違いは？

👉**通信システムの違い**

-Ajax通信
JavaScriptを使って、非同期でサーバーとやり取りをする通信のこと。ブラウザとサーバーの通信とは別に、JavaScriptが通信を行うため、ページ遷移を行わずに情報の更新ができる。しかし、Ajax通信は裏でリクエストを送って、JavaScriptに置き換えているだけのため、リアルタイムなやりとりをするには、常に裏でやりとりをしなければならない。

-WebSocket通信（ActionCable)
サーバー側とユーザー側を常時接続状態にしておき、双方向通信ができるようにする技術。クライアントがリクエストを送信する必要がないため、リアルタイムで情報を更新できる。

やりとりが少ない分、ActionCableを使う方がいいのか？と思いつつ、今回はこちらを採用しました。
では、実装手順を記述していきます。

#①事前準備

簡単なコメント機能を実装していきます。

```ruby:ターミナル
% rails g controller comments
% rails g model comment
```

```ruby:db/migrate/xxxxxxxxxxxxxx_create_comments.rb
class CreateComments < ActiveRecord::Migration[6.0]
  def change
    create_table :comments do |t|
      t.text :text, null: false
      t.references :article, null: false, foreign_key: true
      t.references :user, null: false, foreign_key: true
      t.timestamps
    end
  end
end
```

```ruby:ターミナル
% rails db:migrate
```

**アソシエーションの設定**

```ruby:app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :article
  belongs_to :user

  validates :text, presence: true
end
```

```ruby:app/models/article.rb
class Article < ApplicationRecord
 (省略)
  has_many :comments, dependent: :destroy
end
```

```ruby:app/models/user.rb
class User < ApplicationRecord
 (省略)
  has_many :comments, dependent: :destroy
end
```

**ルーティングの設定**

```ruby:config/routes.rb
Rails.application.routes.draw do
 （省略）
  resources :articles, only: [:index, :show] do
    resources :comments, only: [:new, :create]
  end
end
```

**コントローラーの設定**

```ruby:app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  def new
    @article = Article.find(params[:article_id])
    @comment = Comment.new
    @comments = @article.comments.includes(:user)
  end

  def create
    @comment = Comment.new(comment_params)
  end

  private

  def comment_params
    params.require(:comment).permit(:text).merge(user_id: current_user.id, article_id: params[:article_id])
  end
end
```

**new.html.erbの編集**

```ruby:app/views/comments/new.html.erb
<div class="container">
  <div class="row">
    <div class="offset-sm-2 col-sm-8 offset-sm-2">
      <% if user_signed_in? %>
      <%= form_with model: @comment, url: article_comments_path do |f| %>
          <h5 class='form-header-text text-center'>
            <i class="fas fa-broom fa-lg my-orange"></i>　コメントを投稿する
          </h5>
        <%= render 'layouts/error_messages', model: f.object %>

        <div class="form-group">
          <div class='form-text-wrap'>
            <label class="form-text" for="text">内容</label>
            <span class="badge badge-danger">必須</span>
          </div>
          <%= f.text_area :text, class:"article-input-default", id:"text", autofocus: true %>
        </div>
        <div class='article-btn text-center'>
          <%= f.submit "コメントする" ,class:"btn btn-outline-danger w-50" %>
        </div>
      <% end %>
      <% end %>
    </div>
  </div>
</div>
<div class ="container">
  <div class="row">
    <div class="offset-sm-2 col-sm-8 offset-sm-2">
      <h5 class='form-header-text text-center'>
        <i class="fas fa-broom fa-lg my-orange"></i>　コメント一覧
      </h5>
      <div id ='comments'>
        <% @comments.each do |comment| %>
          <p><%= comment.text %></p>
        <% end %>
      </div>
    </div>
  </div>
</div>
```

イメージとして、上部に投稿フォームがあり、下部に投稿されたコメントが一覧で表示されるものです。（多分逆の方が見栄えがいいと思います。笑）

では簡単なコメント投稿機能ができたので、ここからActionCableを導入していきます。

#②チャネル(Channel)の作成

**channelとは？**
👉即時更新機能を実現するサーバー側の仕組みのこと。データの経路を設定したり、送られてきたデータをクライアントの画面上に表示させる役割。

```ruby:ターミナル
% rails g channel comment
```

上記コマンドを実行すると以下のようにファイルが生成されます。

```ruby:ターミナル
Running via Spring preloader in process 11084
      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
```

生成された、app/channels/comment_channel.rbとapp/javascript/channels/comment_channel.jsが重要なファイルです。

comment_channel.rbの役割
👉クライアントとサーバーを結びつけるためのファイル。

comment_channel.jsの役割
👉サーバーから送られてきたデータをクライアントの画面に描画するためのファイル。

これらを編集しながら実装を進めます。

#③comment_channel.rbの編集

```ruby:app/channels/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
```

**stream_form**
👉ActionCableに用意されている、サーバーとクライアントを関連付けるメソッド。

#④comment_controller.rbの編集

```ruby:app/controllers/comments_controller.rb
class CommentsController < ApplicationController
 （省略）
  def create
    @comment = Comment.new(comment_params)
    if @comment.save
      ActionCable.server.broadcast 'comment_channel', content: @comment
    else
      render :new
    end
  end
 (省略)
end
```

**broadcast**
👉サーバーから送られるデータの経路のことを指す。stream_formメソッドに関連づけられるデータ経路。

追記している部分は、broadcastを通して、'comment_channel'に向けて@commentを送信するということです。
送信された情報は、comment_channel.jsで受け取ります。

#⑤comment_channel.jsの編集

```ruby:app/javascript/channels/comment_channel.js
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.content.text}</p>`;
    const comments = document.getElementById('comments');
    const newComment = document.getElementById('text');
    comments.insertAdjacentHTML('afterbegin', html);
    newComment.value='';
  }
});
```

受け取った情報は、receivedの引数**data**に入ります。このデータをテンプレートリテラルにして、new.html.erbに挿入します。

const html = `<p>${data.content.text}</p>`;
この部分で、受け取ったdataの中にあるcontentのなかのtextを表示しています。
contentはコントローラーのcreateアクション内で指定したcontentからきています。contentは@messageと同義のため、textを取り出すことができます。

以上で即時コメント機能の実装完了です。

#おわりに

Railsにおけるコメント機能の実装手順を見てみると、大半の方ははajaxで実装されているみたいですが、どうなんでしょうか？いろいろ調べながら知見を増やしていきます。