Aqutras Members' Blog

株式会社アキュトラスのメンバーが、技術情報などを楽しく書いています。

Rails5のActionCableで簡易チャットの作成 ~モデルに応じたチャンネルを聴講する方法~

はじめに

お久しぶりです。vitaminです。
2016年2月に、Rails5のbeta3が公開されましたね。
Riding Railsによると、ActionCableやvalidationが改善されたような感じでしょうか。

自分の印象としては、Rails5の見どころはやはりWebsocketのライブラリActionCableとAPI機能をデフォルトで取り入れたところですね。
その他、ARにorメソッドが追加されていたり、railsコマンドでrakeが叩ける用になっていたりと、便利な機能が追加されていました。
今回はその中でも、ActionCableに注目してみたいと思います。

websocket-railsとActionCable

※ ここの記述は、あくまで自分の調査と理解から書いている部分ですので、完全に正しいとは限りません。

websocket-railsでは、ブラウザからWebsocketサーバに対し、どのチャンネルをsubscribe(聴講の申込み)するか指定していました。
そして、Websocektサーバからチャンネルへメッセージを送信しました。

f:id:viatmin:20160427165032p:plain f:id:viatmin:20160427165059p:plain

ActionCableでは、Railsサーバ内にChannel(Websocket通信を行うControllerのようなもの)を定義します。
ブラウザからは、Channelにconnectします。
Channel内では、streamを監視し、自身が監視しているstreamにメッセージが流されたとき、ブラウザ側へメッセージを送信します。
websocket-railsのchannelが Channelとstreamに分割されたような感じでしょうか。

f:id:viatmin:20160427170305p:plain f:id:viatmin:20160427170313p:plain

今回作るチャット

今回は、Rails5 beta3を利用して、簡易チャットを作って見たいと思います。
ログインとチャット画面をつくり、チャット画面では全員への投稿と、個人へのDMの2つの投稿方法を実装します。
チャット自体はいろんな方がブログ記事にしてくださっていますが、DMに関してはあまり記事が見つからなかったので、誰かの参考になれば幸いです。

プロジェクトの作成まで

今回の作業環境は以下です。特に、Rails5はRuby2.2.2以上でないと動作しないので、その点お気をつけ下さい。

  • Ubuntu14.04
  • Rails5 beta3
  • Ruby 2.3.0
  • PostgreSQL 9.3.11

まず、Rails5 beta3を導入しますが、自分のサーバ自体にはRails4.2が入っています。この環境を変えたくは無いので、bundleでrails5を入れます。
適当なディレクトリを作り、 bundle init し、 Gemfile に以下を記述し、 bundle install --jobs 4 --path .bundle を実行します。

gem "rails", '5.0.0.beta3'

そして、Rails開発のいつもの儀式をします。

bundle exec rails new -BT -d PostgreSQL sample_chat

Gemfile には、以下を記述します。 redisは、 productionでActionCableを動作させるときに必要になるので入れてしまいましょう。slim は必要ではありませんが、便利なので入れておきます。

gem 'slim'
gem 'redis'

そして、再び bundle install

dbの構築は割愛します。今回は、 User モデルに name だけをつけておきます。
動作テスト用に、 tarohanako でユーザを作っておきます。

ログインとチャット画面の作成

ログインは、パスワード認証無しで、ユーザ名だけでログインするようにします。(面倒なので)
ユーザ名がDBに存在しなければ作成します。
また、ログインユーザの idsession に格納しておきます。

channelの作成

まず、 config/routes# mount ActionCable.server => '/cable' をアンコメントします。
そして、 app/assets/javascript/cable.coffee の以下の記述をアンコメントします。

@App ||= {}
App.cable = ActionCable.createConsumer()

最後に、 app/config/initializers/action_cable.rb を作成し、Websocket通信を許可するアドレスを指定します。

Rails.application.config.action_cable.allowed_request_origins = ['http://192.168.56.102:3000']

これで、ActionCableを利用する準備は完成です。

channelでcurrent_userの取得

次は、Websocket通信を制御する、channelを作成していきます。

channel内でログインしてる current_user を取得したい所ですが、channelからは session 変数を使うことが出来ません。 そのため、cookieから取得をします。 app/channels/application_cable/connection.rb を以下のように編集します。 identified_by は、README によると、Websocketコネクション自体の識別子のようなものらしいです。これがあると、同じ識別子のコネクションが同時に複数存在したりしないようにしてくれる。。のだと思われます。 connectメソッドは、コネクションが確立した時に呼ばれるようなので、connectメソッド内でcurrent_userを定義します。

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user
    
    def connect
      self.current_user = find_verified_user
    end

    protected

    def find_verified_user
      if verified_user = User.find_by(id: session['user_id'])
        verified_user
      else
        reject_unauthorized_connection
      end
    end
   
    def session
      cookies.encrypted[Rails.application.config.session_options[:key]]
    end
  end
end

chatチャンネルの作成

次は、投稿を制御するchatチャンネルを作成していきます。
まず bundle exec rails g channel chat でchatチャンネルを作成します。
chatチャンネルは以下のように記述しました。
subscribed内で、allというstreamを監視しています。create_message内では、allstreamに向けて、メッセージをブロードキャストしています。

class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'all'
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def create_message(data)
    ActionCable.server.broadcast 'all', {name: current_user.name, content: data['message']}
  end
end

Viewの作成

次に、メッセージを受け取るView側 app/views/chats/index.html.slim を作成します。
コードは、以下のようになっています。
coffeeで、 App.chat という、Websocketの送受信を行うクラスを定義しています。 connecteddisconnectedは、接続の確立と切断の際の処理を記述します。○○さんが入室しましたのような通知を行うことが出来ます。
receivedでは、サーバから送信されたメッセージをどう処理するかを記述します。今回は、table#chat-table にメッセージを挿入しています。
new_messageでは、サーバへメッセージを送信しています。@perform で、サーバ側のどのメソッドに処理を投げるか記述しています。

h1 = "#{@user.name }のタイムライン"
                                                                                                                                                                        input#message-box[type="text"]
input#send-btn[type="submit" value="送信"]

table#chat-table

coffee:

  App.chat = App.cable.subscriptions.create "ChatChannel",
    connected: ->

    disconnected: ->

    received: (data) ->
      $('#chat-table').prepend("<tr><th>#{data.name}</th><td>#{data.content}</td></tr>")

    new_message: (message) ->
      @perform 'create_message', {message: message}

  $('#send-btn').on 'click',  ->
    App.chat.new_message $('#message-box').val()

以上で、ログインしている全員が見れる掲示板のようなチャットが完成しました。

DMの実装

次は、ユーザ毎にダイレクトにメッセージを送れるようにします。 メッセージの最初に、@ + 'ユーザ名' で対象のユーザにメッセージを送るようにします。

app/channels/chat_channel.rbsubscribestream_for current_user を追記します。
Userモデル自体をstreamとして監視します。この時、 stream_from ではないことに注意してください。

次に、 create_message で正規表現でユーザ名を取得し、そのユーザに対してのみメッセージを送ります。
このとき、stream_for で監視しているstreamへメッセージを送るには、ActionCable.server.broadcastではなく、ChatChannel.broadcast_to を利用します。

最終的なコードは以下になります。

class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'all'
    stream_for current_user
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def create_message(data)
    if data['message'].match(/\@[a-zA-Z0-9_]+/)
      name = data['message'].match(/\@[a-zA-Z0-9_]+/).to_s.gsub('@', '')
      user = User.find_by_name(name)
      ChatChannel.broadcast_to(user, {name: current_user.name, content: data['message']})
    else
      ActionCable.server.broadcast 'all', {name: current_user.name, content: data['message']}
    end
  end
end

これで、簡単に掲示板とDMが使えるチャットアプリが完成しました!

f:id:viatmin:20160427154141g:plain

考察

websocket-railsでは、websocketのURLの指定などもjsで行っていましたが、
ActionCableではChannelに接続するだけで、値の受け渡しもchannelに対して行っていて、ブラウザ依存が減ったのかなと思います。
websocekt-railsでのchannelがstreamになっていると記事内で言っていましたがあっているのでしょうか。
あっていたらとてもややこしく感じますね。実際、いろんな記事でも HogeChannelsubscribedfoo チャンネルを監視すると言った記述が多く、ぱっと見良くわからなくなっていました。この辺、公式のドキュメントやcommitログとか見れば分かるのかなと思います。
使ってみた感じだと、stream、channel、subscription(view)でフロントサイド、サーバサイドで役割が分かれている印象でした。

おわりに

websocket-railsを使っていた頃に比べると、大きく詰まることもなく、すんなり行くことが出来てとても満足です!
websocket-railsは、いろんな方がforkしてアレンジしてくださったものを使わないとまともに動かなかったので、ActionCableはとても簡単に感じます。
もっとrails5を触ってみて、新しい機能をじゃんじゃん使っていこうと思います。