前回([Rails5]Action Cableを試してみる - Qiita)はActionCableのサンプルアプリケーションのインストールから動作を見るところまでやってみました。今回はソースコードから実装方法を見ていきます。
※公式のドキュメント(rails/actioncable)を見て書いていますが、誤りなどありましたらコメントなどでご指摘いただけると幸いです。
※2015/07/14時点のアルファ版の情報ですので、リリース時には変わっている可能性があります。
※書いてあることはかなり怪しい(調べきれてない)です。
前提となる知識
- RedisのPub/Sub
- Active Job
サーバサイド
Connectionの設定
まずはApplicationCable::Connection
クラスを使って、認証情報を定義します。
identified_by
はコネクションを識別するキーとなるものです。connect
メソッドはコネクションの接続時に呼ばれるメソッドでしょうか。ここではコネクションの識別キーとして、ログイン時に設定したCookieからuser_idを取り出しています。
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.signed[:user_id])
verified_user
else
reject_unauthorized_connection
end
end
end
end
Channelの設定
次にApplicationCable::Channel
クラスを使ってチャンネルを定義します。
チャンネルに定義したメソッドがクライアントとのインターフェイスになるようです。ここはアプリケーション全体のChannelの設定をする部分で、このサンプルでは特に何もありません。
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end
このサンプルで使用するChannelの設定はこちらです。
follow
メソッドがクライアントから呼び出されると、stream_from
メソッドによりRedisのPubSubのストリーミングが開始されます。
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
メッセージの受信とブロードキャスト
画面から登録されたコメントはCommentsController#create
へPOSTされ、Commentモデルのafter_commit
にてCommentRelayJobに渡されます。
class Comment < ActiveRecord::Base
belongs_to :message
belongs_to :user
after_commit { CommentRelayJob.perform_later(self) }
end
そしてCommentRelayJobがActionCable.server.broadcast
により登録されたコメントをクライアントにブロードキャストしています。
class CommentRelayJob < ApplicationJob
def perform(comment)
ActionCable.server.broadcast "messages:#{comment.message_id}:comments",
comment: CommentsController.render(partial: 'comments/comment', locals: { comment: comment })
end
end
クライアント側の実装
サーバ設定
まずはじめにWebSocketのサーバを設定します。
#= require cable
#= require_self
#= require_tree .
@App = {}
App.cable = Cable.createConsumer 'ws://localhost:28080'
コネクションの開始と受信
App.cable.subscriptions.create
により指定したチャンネルでサーバとの接続(Subscription)を開始しします。
接続が開始されたら@perform 'follow'
によりサーバからのストリーミングが開始されます。(若干怪しいです...)
あとはreceived
にてサーバから送信されたデータを受信し、処理を行っています。
App.comments = App.cable.subscriptions.create "CommentsChannel",
collection: -> $("[data-channel='comments']")
connected: ->
# FIXME: While we wait for cable subscriptions to always be finalized before sending messages
setTimeout =>
@followCurrentMessage()
@installPageChangeCallback()
, 1000
received: (data) ->
@collection().append(data.comment) unless @userIsCurrentUser(data.comment)
userIsCurrentUser: (comment) ->
$(comment).attr('data-user-id') is $('meta[name=current-user]').attr('id')
followCurrentMessage: ->
if messageId = @collection().data('message-id')
@perform 'follow', message_id: messageId
else
@perform 'unfollow'
installPageChangeCallback: ->
unless @installedPageChangeCallback
@installedPageChangeCallback = true
$(document).on 'page:change', -> App.comments.followCurrentMessage()
まとめ
色々と怪しいところがありますが、最低限の双方向通信のインターフェイスは見えてきました。
- クライアントからサーバへの通知はチャンネル内のメソッドを通じて行う
- サーバからクライアントへの通知はRedisのPub/Subの仕組みを用いて行う(ってこと?チャンネルからもできて良さそうだけどどうなんだろう)
今までRailsでWebSocketを使ったことがないのでこれで楽になるのかはよく分かりませんが、少なくともActionCableにより1つのやり方が提示されたので、実装に迷うことはなくなりそうです。