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サーバからチャンネルへメッセージを送信しました。
ActionCableでは、Railsサーバ内にChannel(Websocket通信を行うControllerのようなもの)を定義します。
ブラウザからは、Channelにconnectします。
Channel内では、streamを監視し、自身が監視しているstreamにメッセージが流されたとき、ブラウザ側へメッセージを送信します。
websocket-railsのchannelが Channelとstreamに分割されたような感じでしょうか。
今回作るチャット
今回は、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
だけをつけておきます。
動作テスト用に、 taro
と hanako
でユーザを作っておきます。
ログインとチャット画面の作成
ログインは、パスワード認証無しで、ユーザ名だけでログインするようにします。(面倒なので)
ユーザ名がDBに存在しなければ作成します。
また、ログインユーザの id
を session
に格納しておきます。
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内では、all
streamに向けて、メッセージをブロードキャストしています。
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の送受信を行うクラスを定義しています。
connected
とdisconnected
は、接続の確立と切断の際の処理を記述します。○○さんが入室しました
のような通知を行うことが出来ます。
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.rb
の subscribe
に stream_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が使えるチャットアプリが完成しました!
考察
websocket-railsでは、websocketのURLの指定などもjsで行っていましたが、
ActionCableではChannelに接続するだけで、値の受け渡しもchannelに対して行っていて、ブラウザ依存が減ったのかなと思います。
websocekt-railsでのchannelがstreamになっていると記事内で言っていましたがあっているのでしょうか。
あっていたらとてもややこしく感じますね。実際、いろんな記事でも HogeChannel
の subscribed
で foo
チャンネルを監視すると言った記述が多く、ぱっと見良くわからなくなっていました。この辺、公式のドキュメントやcommitログとか見れば分かるのかなと思います。
使ってみた感じだと、stream、channel、subscription(view)でフロントサイド、サーバサイドで役割が分かれている印象でした。
おわりに
websocket-railsを使っていた頃に比べると、大きく詰まることもなく、すんなり行くことが出来てとても満足です!
websocket-railsは、いろんな方がforkしてアレンジしてくださったものを使わないとまともに動かなかったので、ActionCableはとても簡単に感じます。
もっとrails5を触ってみて、新しい機能をじゃんじゃん使っていこうと思います。