Code Serendipity

Keyword: Ruby, Python, javascript, RoR, heroku, AWS...

Code Serendipity

Keyword: Ruby, Python, wordpress, javascript, AWS...

【Rails5】ActionCableリアルタイムチャットを実装。

https://image.slidesharecdn.com/newfeaturesinrails5-151020085545-lva1-app6892/95/new-features-in-rails-5-4-638.jpg?cb=1445331597

Action Cableは、 WebSocketとRailsのその他の部分をシームレスに統合するためのものです。Action Cable が導入されたことで、Rails アプリケーションの効率の良さとスケーラビリティを損なわずに、通常のRailsアプリケーションと同じスタイル・方法でリアルタイム機能をRubyで記述できます。クライアント側のJavaScriptフレームワークとサーバー側のRubyフレームワークを同時に提供する、フルスタックのフレームワークです。Active RecordなどのORMで書かれたすべてのドメインモデルにアクセスできます。

Action Cable の概要 | Rails ガイド

概念, 用語

Publish/subscribe

出版-購読型モデルとも呼ばれる非同期メッセージングパラダイムの一種。メッセージの送信者(出版側)が特定の受信者(購読側)を想定せずにメッセージを送るようプログラムされたもの。出版されたメッセージにはクラス分けされ、購読者に関する知識を持たない。購読側は興味のあるクラスを指定しておき、そのクラスに属するメッセージだけを受け取り、出版者についての知識を持たない。出版側と購読側の結合度が低いため、スケーラビリティがよく、動的なネットワーク構成に対応可能である。

出版-購読型モデル - Wikipedia

サンプルアプリを作る

習うより慣れよ、ということで、チャットアプリを作る。 今回は Rails 5.1.1 ruby 2.4.0 で行った。 なお

qiita.com

にならった。

ActionCableはRails5じゃないとデフォルトで実装されていないので注意。

まずはRoomsコントローラと、content カラムを持つMessageモデルをデータベースに持つシンプルなアプリを作る。Rails既習ユーザを想定しているため, 前提をざっくり書くと

  • Rooms Controller#showにて@messages = Message.all
  • Rooms#showのViewにて _message.html.erb パーシャルを作り、そこでmessage.contentを表示。
  • routes.rbでroot ‘rooms/show'を設定

このアプリをリアルタイムチャットにしていく。

チャンネルを作成する rails g channel

Channelの果たす役割は、通常のMVC設定でコントローラが果たす役割と似ている。 ActionCableの論理的な作業単位をカプセル化する。チャンネルがあることで、コンスーマ(クライアントサイド)は、チャンネルを購読することができるようになる。

$ rails g channel room speak

を叩くと、以下の2つのファイルが作成される。

$ rails g channel room speak
    create  app/channels/room_channel.rb
    create  app/assets/javascripts/channels/room.coffee

作成されたチャンネルファイルを見ると最初から以下のsubscribed, unsubscribed, speakの3つのメソッドが定義されている。

# Be sure to restart your server when you modify this file. Action Cable runs in an EventMachine loop that does not support auto reloading.
class RoomChannel < ApplicationCable::Channel
  def subscribed
    # stream_from "some_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak
  end
end

確認する

localhost:3000 の(つまりクライアントサイドの) consoleにて App.room.speak() と打ち込むと、 Railsのログ(サーバーサイド)に、

f:id:serendipity4u:20170703120758p:plain

RoomChannel#speak と出るではないか! (嬉しくて3回やった)

クライアントサイドからサーバーサイドに通信できていることが確認できた。

さて、ここでrails console(サーバーサイド)で、

Message.create! content: 'Hello world!'

とやってもリアルタイムに更新されることはない。リロードしないと、表示されない。まだ足りないのである。

speakアクションを設定する

app/assets/javascripts/channels/room.coffee

App.room = App.cable.subscriptions.create "RoomChannel",
  # (省略)

  speak: (message) ->
    @perform 'speak', message: message //サーバーサイドのspeakアクションにmessageパラメータを渡す

このspeakメソッドによって, 全クライアントに, 受け取ったmessageデータをブロードキャストしている。

app/assets/javascripts/channels/room.coffee で、クライアント側がデータを受け取った時の挙動を定義する。

received: (data) -> 
  alert data['message']

Redisサーバを起動する。Redisとは一意のキーと、保存したい値のペアでデータを保存する(KVS:KeyValueStore)・データを全てメモリ上に持つ(なので動作が高速)という特徴を持つデータベースである(詳しくは RailsのセッションストアとしてRedisを使う(Mac/EC2:AmazonLinux) - Qiitaを参照 )。

$ redis-server

Railsサーバーを再起動する。

ブラウザのlocalhost:3000のコンソールで

> App.room.speak('hello')

とすると、アラームが表示される。

フォームでデータを送信する

app/view/rooms/show.html.erb

にて、

<form>
<input type='text' data-behavior='room_speaker'>
</form>

を用意する。これをリターンキーで送信できるようにするために, app/assets/javascripts/channels/room.coffeeにおいて

jQuery(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
  if event.keyCode is 13 # return キーのキーコードが13
    App.room.speak event.target.value # speak メソッド, event.target.valueを引数に.
    event.target.value = ''
    event.preventDefault()

を追記する。(蛇足だが, jqueryを使っているのでGemfile に gem ‘jquery-rails'を追加しておくこと+ application.jsに//=require jquery, //= require jquery-ujs を追加のこと) この状態でフォームに入力してエンターキーを押すと、

異なるブラウザのクライアントも, リアルタイムで受信するわけです(2ツノブラウザでlocalhost:3000を開いて, 並べています).

f:id:serendipity4u:20170703143937p:plain

入力したmessageを保存する

app/channels/room_channel.rb で、 speakアクションが呼ばれたらdata[‘message’]をMessageオブジェクトとして保存するようにする。

  def speak(data)
     Message.create! content: data['message']
  end

さらに、 Messageモデルのコールバックとして, Messageオブジェクトが保存されたら, 非同期でブロードキャスト処理をするように設定する。

$ rails g job MessageBroadcast

app/jobs/message_broadcast_job.rbが作成される。

class MessageBroadcastJob < ApplicationJob
  queue_as :default

  def perform(message)
    ActionCable.server.broadcast 'room_channel', message: render_message(message)
  end
  
  private
  def render_message(message)
     ApplicationController.renderer.render(partial: 'messages/message', locals: { message: message })
  end
end

app/model/message.rbにて、オブジェクトが保存されたら、ジョブを実行することを定義する。

after_create_commit { MessageBroadCastJob.perform_later self}

これで、保存とhtmlのアラーム表示が行われるようになった。

f:id:serendipity4u:20170703150407p:plain

今度は、このdata[‘message’]に入っているhtmlを使って、表示を書き換える。

サーバーからデータを受け取り、ブラウザに反映

そのためには、app/assets/javascripts/channels/room.coffee のreceived 部分を書き換える。

app/assets/javascripts/channels/room.coffee

data[‘message’]にhtmlが入っているから, append するだけ.

received: (data) ->
  $('#messages').append data['message']

これでクライアント間で完全にリアルタイムチャット化されます。

f:id:serendipity4u:20170703151316p:plain

参考

qiita.com

Action Cable の概要 | Rails ガイド

befool.co.jp

Real-Time Rails: Implementing WebSockets in Rails 5 with Action Cable | Heroku