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


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

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

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

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

# 前提となる知識

- RedisのPub/Sub
- Active Job

# サーバサイド

## Connectionの設定

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

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

```ruby: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の設定をする部分で、このサンプルでは特に何もありません。

```ruby:app/channels/application_cable/channel.rb
module ApplicationCable
  class Channel < ActionCable::Channel::Base
  end
end
```

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

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

```ruby: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に渡されます。

```ruby: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`により登録されたコメントをクライアントにブロードキャストしています。

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

```CoffeeScript: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`にてサーバから送信されたデータを受信し、処理を行っています。

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