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


##はじめに

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

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

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

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

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

##開発環境

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

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


##前提条件

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

```ruby: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`
など


##処理の流れ

####１： WebSocketによる通信を行う前に一度HTTP通信を行う
(サーバーを立ち上げた瞬間)
ハンドシェイクという処理

ブラウザ上から「upgrade」というリクエストを行う
↓
サーバー側から「101 Switching Protocols」というレスポンスがくる
↓
これが終わるとそれ以降のデータのやり取りはWebSocketプロトコル上で行われる
	
	
####２： WebSocketコネクションを確立させるか判断する
(connection.rb内でユーザーidを元に、確立させていいか判断している)
		
		
####３： 指定したchannelをsubscribeする
(room_channel内でsubscribedメソッドを実行)
	
subscribedメソッドでは、
サーバー側で受け取った内容をどこに配信(ブロードキャスト)するかを定義している
	
	
####４： 入力フォームにテキストを入力し、エンターキーを押すとイベントが発火する
(room.coffee内)
	
	
####５： room.coffee内のspeakアクションが呼び出される
speakアクションでサーバー側(room_channel.rb)のspeakアクションを呼び出す
	
※紛らわしいので注意
クライアントサイドのspeakメソッドで、サーバーサイドのspeakメソッドを呼び出している


####６： speakアクションでdirect_messageをcreateする
(room_channel.rb内)
	
	
####７： after_create_commitメソッドにより、direct_message_broadcast_job.rbのpeformアクションを呼び出す
(direct_message.rb内)
	
	
####８： subscribedメソッドで決めた場所にデータを配信(ブロードキャスト)する
(direct_message_broadcast_job.rb)
	
	
####９： ブロードキャストを介して、room.coffeeのrecievedメソッドにデータが渡される


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

##専門用語

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

`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
```

##さいごに

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

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