Action Cableの習作: Examplesの更新: 4: Action Cableの利用

今回は、Action Cableの利用です。

1. モデルの作成
2. ユーザの認証
3. メッセージとコメントの表示
4. Action Cableの利用

Action Cableの利用

閲覧中のメッセージに別のユーザがコメントしたら、直ちにコメント一覧に追加されるようにします。

Generate Comments channel
コメントチャンネルを生成します。

bin/rails g channel Comments

Add identified_by :current_user
identified_by(*identifiers)

... Note that anything marked as an identifier will automatically create a delegate by the same name on any channel instances created off the connection.

current_userによって接続を識別できるようにします。

クライアントの接続時、クッキーから検証済みのユーザを見つけます。
見つかった場合、検証済みのユーザをcurrent_userに設定します。
見つからない場合、クライアントの接続を許可しません。

app/channels/application_cable/connection.rb

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
      logger.add_tags 'ActionCable', current_user.name
    end

    protected
      def find_verified_user
        if verified_user = User.find_by(id: cookies.encrypted[:user_id])
          verified_user
        else
          reject_unauthorized_connection
        end
      end
  end
end

Add ActionCable.server.disconnect to unauthenticate_user
ユーザの認証解除で、ActionCableを切断します。

app/controllers/concerns/authentication.rb

    def unauthenticate_user
      ActionCable.server.disconnect(current_user: @current_user)
      @current_user = nil
      cookies.delete(:user_id)
    end

Add stream_from to CommentsChannel#follow
サーバ側の処理です。

コメントチャンネルにfollowメソッドを追加します。
followメソッドは、メッセージのコメントをフォローします。
followメソッドは、引数でmessage_idを受け取ります。

コメントチャンネルにunfollowメソッドを追加します。
unfollowメソッドは、チャンネルに関連づいた全てのストリームを停止します。
上記の処理は、followメソッドの最初でも行います。

app/channels/comments_channel.rb

class CommentsChannel < ApplicationCable::Channel
  def follow(data)
    stop_all_streams
    stream_from "messages:#{data['message_id'].to_i}:comments"
  end

  def unfollow
    stop_all_streams
  end
end

Add followCurrentMessage to CommentsChannel#connected
クライアント(コンシューマ)側の処理です。

接続完了時、下記の処理を行います。
・現在のメッセージのフォローを始める。
・ページ変更時の処理をインストールする。

app/javascript/channels/comments_channel.js

  connected() {
    // FIXME: While we wait for cable subscriptions to always be finalized before sending messages
    setTimeout(() => {
      this.followCurrentMessage()
      this.installPageChangeCallback()
    }, 1000)
  },

  // ...
  },

  collection() {
    return document.querySelector('[data-channel~=comments]')
  },

  messageId() {
    const collection = this.collection()
    if (collection) {
      return collection.getAttribute('data-message-id')
    }
    return null
  },

  followCurrentMessage() {
    const messageId = this.messageId()
    if (messageId) {
      this.perform('follow', { message_id: messageId })
    } else {
      this.perform('unfollow')
    }
  },

  installPageChangeCallback() {
    if (!this.installedPageChangeCallback) {
      this.installedPageChangeCallback = true
      document.addEventListener('turbolinks:load', event => {
        this.followCurrentMessage()
      })
    }
  },

メッセージを表示した時のコンソールの出力例:
[ActionCable] [The Notorious B.I.G.] CommentsChannel#follow({"message_id"=>"1"})
[ActionCable] [The Notorious B.I.G.] CommentsChannel is streaming from messages:1:comments

Add broadcast to Comment#after_commit
コメントが投稿されたら、そのコメントの内容をブロードキャストします。

app/models/comment.rb

class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :message

  after_commit do
    comment = CommentsController.render(
      partial: 'comments/comment',
      locals: { comment: self }
    )
    ActionCable.server.broadcast(
      "messages:#{message_id}:comments",
      comment: comment
    )
  end
end

Add received to CommentsChannel
現在のユーザIDをHTMLのヘッダ部に設定します。
受信したデータ(コメント)が自分のものでなければ、コメントを一覧に追加します。

app/views/layouts/application.html.erb

   <head>
...
   <meta name="current-user" id="<%= @current_user.try(:id) %>">
  </head>

app/javascript/channels/comments_channel.js

  received(data) {
    const collection = this.collection()
    if (!collection) {
      return
    }
    const comment = data.comment
    if (this.userIsCurrentUser(comment)) {
      return
    }
    collection.insertAdjacentHTML('beforeend', comment)
  },

  // ...

  userIsCurrentUser(commentHtmlString) {
    const comment = this.createElementFromHtmlString(commentHtmlString)
    const commentUserId = comment.getAttribute('data-user-id')
    return commentUserId === this.currentUserId()
  },

  createElementFromHtmlString(htmlString) {
    const div = document.createElement('div')
    div.innerHTML = htmlString
    return div.firstElementChild
  },

  currentUserId() {
    return document.getElementsByName('current-user')[0].getAttribute('id')
  },

下記の要領で動作を確認します。

別のクッキーとなるようにしてウェブブラウザを2つ開きます。
それぞれ別のユーザでログインします。
それぞれ同じメッセージのページを開きます。
メッセージにコメントすると、他方のウィンドウにコメントが追加されます。

Generate CommentRelayJob
コメントのブロードキャストを非同期処理にするためにジョブを追加します。

bin/rails g job CommentRelay

Move broadcast to CommentRelayJob
コメントのブロードキャスト処理をジョブに移動します。

app/models/comment.rb

class Comment < ApplicationRecord
  belongs_to :message
  belongs_to :user

  after_commit { CommentRelayJob.perform_later(self) }
end

app/jobs/comment_relay_job.rb

class CommentRelayJob < ApplicationJob
  queue_as :default

  def perform(comment)
    html_string = CommentsController.render(
      partial: 'comments/comment',
      locals: { comment: comment }
    )
    ActionCable.server.broadcast(
      "messages:#{comment.message_id}:comments",
      comment: html_string
    )
  end
end

Action Cableの利用は、以上になります。

この記事が気に入ったらサポートをしてみませんか?