はじめに
ポートフォリオ作成中の初学者です。
ポートフォリオに__ActionCable__を使った__「チャット機能」__を導入しました。
ネット上に素晴らしい記事がたくさんあり、実装することはできましたが、
なぜ、そのような書き方なのか、どのような処理の順番なのかなど
理解不十分な部分が多々あった為、今回は処理の流れをメインに__備忘録__としてここに残します。
ポートフォリオ作成完了後に時間があれば、まとめなおそうと思っています。
※他の勉強もしたいですが、アウトプットも大事。
知識が乏しく、間違った解釈をしている箇所がありましたら、コメントいただけると幸いです。
開発環境
・ruby: 2.6.3
・rails: 5.2.4.5
・OS: macOS Catalina ver10.15.7
・Cloud9
※最近ではRails6でのActionCable実装記事もあります。
前提条件
・devise導入
・チャットルームとメッセージの名前はこちら
※テーブル名やカラム名は他記事と異なるかもしれません。
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(ストリーム)
:ブロードキャストをサブスクライバーに転送すること
コード一覧
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
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
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()
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
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
さいごに
なぐり書きですが、一人でも誰かの参考になれば嬉しいです。
閲覧ありがとうございました。