LoginSignup
1
1

More than 3 years have passed since last update.

Rails ActionCable チャット機能 処理の流れ

Posted at

はじめに

ポートフォリオ作成中の初学者です。

ポートフォリオにActionCableを使った「チャット機能」を導入しました。

ネット上に素晴らしい記事がたくさんあり、実装することはできましたが、
なぜ、そのような書き方なのか、どのような処理の順番なのかなど
理解不十分な部分が多々あった為、今回は処理の流れをメインに備忘録としてここに残します。

ポートフォリオ作成完了後に時間があれば、まとめなおそうと思っています。
※他の勉強もしたいですが、アウトプットも大事。

知識が乏しく、間違った解釈をしている箇所がありましたら、コメントいただけると幸いです。

開発環境

・ruby: 2.6.3
・rails: 5.2.4.5
・OS: macOS Catalina ver10.15.7
・Cloud9

※最近ではRails6でのActionCable実装記事もあります。

前提条件

・devise導入
・チャットルームとメッセージの名前はこちら
※テーブル名やカラム名は他記事と異なるかもしれません。

schema.rb
create_table "direct_messages", force: :cascade do |t|
   t.integer "user_id", null: false
   t.integer "room_id", null: false
   t.string "message", null: false
   t.datetime "created_at", null: false
   t.datetime "updated_at", null: false
end

create_table "rooms", force: :cascade do |t|
   t.string "name", null: false
   t.datetime "created_at", null: false
   t.datetime "updated_at", null: false
end

他には
room.rb
rooms_controller.rb
direct_message.rb
direct_messages.rb
room.coffee
room_channel.rb
direct_message_broadcast_job
など

処理の流れ

1: WebSocketによる通信を行う前に一度HTTP通信を行う

(サーバーを立ち上げた瞬間)
ハンドシェイクという処理

ブラウザ上から「upgrade」というリクエストを行う

サーバー側から「101 Switching Protocols」というレスポンスがくる

これが終わるとそれ以降のデータのやり取りはWebSocketプロトコル上で行われる

2: WebSocketコネクションを確立させるか判断する

(connection.rb内でユーザーidを元に、確立させていいか判断している)

3: 指定したchannelをsubscribeする

(room_channel内でsubscribedメソッドを実行)

subscribedメソッドでは、
サーバー側で受け取った内容をどこに配信(ブロードキャスト)するかを定義している

4: 入力フォームにテキストを入力し、エンターキーを押すとイベントが発火する

(room.coffee内)

5: room.coffee内のspeakアクションが呼び出される

speakアクションでサーバー側(room_channel.rb)のspeakアクションを呼び出す

※紛らわしいので注意
クライアントサイドのspeakメソッドで、サーバーサイドのspeakメソッドを呼び出している

6: speakアクションでdirect_messageをcreateする

(room_channel.rb内)

7: after_create_commitメソッドにより、direct_message_broadcast_job.rbのpeformアクションを呼び出す

(direct_message.rb内)

8: subscribedメソッドで決めた場所にデータを配信(ブロードキャスト)する

(direct_message_broadcast_job.rb)

9: ブロードキャストを介して、room.coffeeのrecievedメソッドにデータが渡される

10: 受け取ったdirect_message(入力されたメッセージ)を(idがdirect_messageの箇所)にappendする(HTML要素を追加する)

(room.coffee内)

専門用語

cousumer(コンシューマ):ユーザーが開くブラウザ1つ1つのこと

connection(コネクション):consumerとサーバーのつながり

subscribe(サブスクライブ):consumerがchannelと繋がること

subscriber(サブスクライバー):channelとつながったconsumerをさす

brodecast(ブロードキャスト):サーバーがsubscriberにデータを送ること

channel(チャネル):コントローラ的役割(機能毎に分ける)

publisher(パブリッシャ):実際に通信を発信するところ(Rails内)

broadcast(ブロードキャスト):パブリッシャが出す通信(ストリームに対して)

stream(ストリーム):ブロードキャストをサブスクライバーに転送すること

コード一覧

connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base

    # 処理①
    # ハンドシェイク後、WebSocket通信を確立させるか判断する
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    protected
    def find_verified_user
      #ユーザーidで認証する
      verified_user = User.find_by(id: env['warden'].user.id)
      # 認証したユーザー(verified_user)出ない限りはreturn
      return reject_unauthorized_connection unless verified_user
      verified_user
    end
  end
end
room_channel.rb
class RoomChannel < ApplicationCable::Channel

  # 処理②
  # サーバー側で受け取った内容をどこに配信するかを定義している
  # room_channel_1,room_channel_2...とparams['room']にはroomのidが入る
  # つまり、部屋毎にその部屋にアクセスした人クライアントに配信している
  def subscribed
    # stream_from "some_channel"
    stream_from "room_channel_#{params['room']}"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  # 処理⑤
  def speak(data)
    direct_message = DirectMessage.create! message: data['direct_message'], user_id: current_user.id, room_id: params['room']
  end

end
room.coffee
document.addEventListener 'turbolinks:load', ->
  if App.room
    App.cable.subscriptions.remove App.room

  # サーバー側のチャネルをcreate
  # 引数のRoomChannel = app/channels/room_channel.rbで指定されるサーバー側のチャネルにクライアント側から接続する
  App.room = App.cable.subscriptions.create { channel: "RoomChannel", room: $('#direct_messages').data('room_id') },

    connected: ->

    disconnected: ->

    # 処理⑧
    # サーバー側から送られてきたデータを引数dataで受け取る
    # 受け取ったdirect_message(入力されたメッセージ)を(idがdirect_messageの箇所)にappendする(HTML要素を追加する)
    received: (data) ->
      $('#direct_messages').append data['direct_message']

    # 処理④
    # room_channel.rbのspeakメソッドを呼び出している
    speak: (direct_message) ->
      @perform 'speak', direct_message: direct_message


  # 処理③
  # data-behavior属性がroom_speakerである入力フォームでのキーボード入力イベントで発火
  $(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
    if event.keyCode is 13
      # 入力されたメッセージを送信する処理をサーバー側に求める
      App.room.speak event.target.value
      # サーバー側に処理をお願いした為、入力フォームを空にする(valueで初期値を空に)
      event.target.value = ''
      # サーバー側に処理をお願いした為、入力フォームでのデータ送信(クライアント側でのフォーム送信)処理は止める
      # preventDefault:直前のイベントをキャンセルという意味
      event.preventDefault()
direct_message.rb
class DirectMessage < ApplicationRecord
  belongs_to :user
  belongs_to :room
  has_many :notifications, dependent: :destroy

  validates :message, presence: true

  # 処理⑥
  after_create_commit { DirectMessageBroadcastJob.perform_later self }
end
direct_message_broadcast_job.rb
class DirectMessageBroadcastJob < ApplicationJob
  queue_as :default

  # 処理⑦
  def perform(direct_message)
    ActionCable.server.broadcast "room_channel_#{direct_message.room_id}", direct_message: render_direct_message(direct_message)
  end

  private

  def render_direct_message(direct_message)
    ApplicationController.renderer.render(partial: 'public/direct_messages/direct_message', locals: { direct_message: direct_message })
  end
end

さいごに



なぐり書きですが、一人でも誰かの参考になれば嬉しいです。

閲覧ありがとうございました。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1