Posted at

[Rails5]Action Cableのサンプルを読み解いてみる

More than 3 years have passed since last update.

前回([Rails5]Action Cableを試してみる - Qiita)はActionCableのサンプルアプリケーションのインストールから動作を見るところまでやってみました。今回はソースコードから実装方法を見ていきます。

※公式のドキュメント(rails/actioncable)を見て書いていますが、誤りなどありましたらコメントなどでご指摘いただけると幸いです。

※2015/07/14時点のアルファ版の情報ですので、リリース時には変わっている可能性があります。

※書いてあることはかなり怪しい(調べきれてない)です。


前提となる知識


  • RedisのPub/Sub

  • Active Job


サーバサイド


Connectionの設定

まずはApplicationCable::Connectionクラスを使って、認証情報を定義します。

identified_byはコネクションを識別するキーとなるものです。connectメソッドはコネクションの接続時に呼ばれるメソッドでしょうか。ここではコネクションの識別キーとして、ログイン時に設定したCookieからuser_idを取り出しています。

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.signed[:user_id])
verified_user
else
reject_unauthorized_connection
end
end
end
end


Channelの設定

次にApplicationCable::Channelクラスを使ってチャンネルを定義します。

チャンネルに定義したメソッドがクライアントとのインターフェイスになるようです。ここはアプリケーション全体のChannelの設定をする部分で、このサンプルでは特に何もありません。

app/channels/application_cable/channel.rb

module ApplicationCable

class Channel < ActionCable::Channel::Base
end
end

このサンプルで使用するChannelの設定はこちらです。

followメソッドがクライアントから呼び出されると、stream_fromメソッドによりRedisのPubSubのストリーミングが開始されます。

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


メッセージの受信とブロードキャスト

画面から登録されたコメントはCommentsController#createへPOSTされ、Commentモデルのafter_commitにてCommentRelayJobに渡されます。

app/models/comment.rb

class Comment < ActiveRecord::Base

belongs_to :message
belongs_to :user

after_commit { CommentRelayJob.perform_later(self) }
end

そしてCommentRelayJobがActionCable.server.broadcastにより登録されたコメントをクライアントにブロードキャストしています。

app/jobs/comment_relay_job.rb

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のサーバを設定します。

app/assets/javascripts/channels/index.coffee

#= require cable

#= require_self
#= require_tree .

@App = {}
App.cable = Cable.createConsumer 'ws://localhost:28080'


コネクションの開始と受信

App.cable.subscriptions.createにより指定したチャンネルでサーバとの接続(Subscription)を開始しします。

接続が開始されたら@perform 'follow'によりサーバからのストリーミングが開始されます。(若干怪しいです...)

あとはreceivedにてサーバから送信されたデータを受信し、処理を行っています。

app/assets/javascripts/channels/comments.coffee

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つのやり方が提示されたので、実装に迷うことはなくなりそうです。