mazeltov7のweb断片

備忘録的なテキトーなことを書きます。(技術記事はQiitaに移行しました http://qiita.com/mazeltov7 )

【Rails4】ActionController::Liveで簡単なチャットアプリサンプルを作ってみる

Rails4から新たに取り入れられた非同期な実装を可能にする!?ActionController::Liveを使って、簡単なチャットアプリを作ってみようと思います。

主に、RailsCastの401のエピソードに詳細は書いてあります。(が、proなので、課金必要ですorz)
あと、ここも有難い感じです。
ActionController::Liveで始めるリアルタイムWeb at Ginza.rb

まず、ActionController::Live入れてないバージョンを見ます。
コードはこちら(GitHub)から見れます。

index.html.erb

<h1>Chat</h1>

<ul id="chat">
  <%= render @messages %>
</ul>

<%= form_for Message.new, remote: true do |f| %>
  <%= f.text_field :name, placeholder: 'Name' %>
  <br>
  <%= f.text_area :content, placeholder: 'Write something...' %>
  <%= f.submit "Send" %>
<% end %>

Ajaxでリダイレクトせずにぺらっと表示させる感じです。

create.js.erb

$('#chat').append("<%= j render(@message) %>");
$("#message_content").val('');

これで、とりあえず、チャットっぽくはなりますが、
オンタイムで他の人が入れたメッセージはリダイレクトしないと出てきません。これをTwitterみたく出てくるようにします!

ActionController::LiveではServer Sent Eventsを使います。こちらにざっくり説明が書いてあります。
https://developer.mozilla.org/ja/docs/Server-sent_events/Using_server-sent_events

Server Sent EventsでWEBrickunicornが使えないみたいで、pumaやthin,rainbowをアプリケーション・サーバーに使う必要があるみたいです。
ここではpumaを使います。
また、もう一点。
develepment.rbで

  config.cache_classes = true
  config.eager_load = true

デフォルトでfalseになってる両者をtrueにする必要があるみたいです。
config.cache_classesをtrueにすることで、クラスの読み込みが起動時の1回になるそうな。
config.eager_loadをtrueにすることで、Railsアプリケーションの全体とすべてのクラスが、autoloadではなく、eager-loadされることによりthread-safeになるそうな。
詳しくは以下。
http://railscasts.com/episodes/400-what-s-new-in-rails-4?language=ja&view=asciicast
http://railscasts.com/episodes/365-thread-safety

messages_controller.rb(一部)

class MessagesController < ApplicationController
  include ActionController::Live

  def create
    @message = Message.create(attributes)
  end

  def events
    response.headers["Content-Type"] = "text/event-stream"
    start = Time.zone.now
    10.times do
      Message.uncached do
        Message.where('created_at > ?', start).each do |message|
          response.stream.write "data: #{message.to_json}\n\n"
          start = message.created_at
        end
      end
      sleep 2
    end
  rescue IOError
    logger.info "Stream closed"
  ensure
    response.stream.close
  end

  private

  def attributes
    params.require(:message).permit(:name, :content)
  end

end

message.js.coffee

source = new EventSource('/messages/events')
source.addEventListener 'message', (e) ->
  message = $.parseJSON(e.data)
  $('#chat').append($('<li>').text("#{message.name}: #{message.content}").css("list-Style-Type","none"))

create.js.erb

$("#message_content").val('');

routes.rb(一部)

  resources :messages do
    collection { get :events }
  end

これで、$ rails s pumaで起動して、いくつかブラウザ立ち上げて試すと、eventが定期的に見に行ってくれてて、確かに、同時っぽくなる。
けど、結構不安定だったり、ややラグがある。

なので、ここで、RedisのPub/Subの機能を使ってやってみる。
http://redis.io/topics/pubsub
日本語版
edis の Pub/Sub を使って Node.js + WebSocket のスケールアウトを実現する方法

gem 'redis'を入れて、
initializers/redis.rb

$redis = Redis.new

messages_controller.rb(一部)

  def create
    response.headers["Content-Type"] = "text/javascript"
    @message = Message.create(attributes)
    $redis.publish('messages.create', @message.to_json)
  end

  def events
    response.headers["Content-Type"] = "text/event-stream"
    redis = Redis.new
    redis.subscribe('messages.create') do |on|
      on.message do |event, data|
        response.stream.write("data: #{data}\n\n")
      end
    end
  rescue IOError
    logger.info "Stream closed"
  ensure
    redis.quit
    response.stream.close
  end

これで、試すと、素早く、Twitterみたいに動きます!yay!

コードはこちら(GitHub)。


なんかおかしなところなどありましたら、ご指摘下さい。





コードもうちょいちっさく表示した方が見やすい気がするなぁ
Gistとか貼るのめんどいしなー、うーむ。