RailsとHerokuでノーティフィケーションをプッシュする / PusherとTurbolinksの兼ね合い

Shunsuke Sawada

リアルタイムでユーザーにお知らせを通知したい。
Facebookの「○○さんがあなたの投稿をLikeしました」みたいなやつ。
やってみたら意外とすぐできた。
(herokuのaddonのおかげ)

こんなの。
20150112-064343_capture

Pusher

http://pusher.com/
めんどくさそうだと思ってたけど、Pushserを使えばそんなに難しくない。
Websocketの仕組みは分からないけど、意識しなくて良い。
RailsとHeroku使ってます。HerokuじゃなくてもPusherは使えますが。
Screen Shot 2015-01-11 at 10.50.05 PM

herokuドキュメント
Pusher | Heroku Dev Center
Adding in-app notifications with Pusher | Heroku Dev Center

herokuドキュメントに沿ってやったんですが、
Turbolinksとの絡みがあるので、いろいろ変えました。

  
Gemfile

1
gem 'pusher'

HerokuのダッシュボードからPusherを追加。
リミットはあるけど無料で使える。

Pusher  Development — App keys

設定

APIキーとかを追加。Herokuのプロダクションは何にもしなくても面倒をみてくれるみたい。
Pusher.url とか Pusher.app_id とかは自分のローカル環境とHeroku上のアプリとでは違うものなので注意。

config/environments/development.rb

ruby
1
2
3
4
5
6
7
8
9
10
11
  # Real time notification
  require 'pusher'
  Pusher.url = "http://7f115110ccdsxxxxxxxxx:[email protected]/apps/100000"
  Pusher.logger = Rails.logger

  # Lines below are needed only for development environment.
  # In production, Pusher will automatically take care of this.
  require 'pusher'
  Pusher.app_id = '000000'
  Pusher.key    = '7f1151xxxxxxxxxxxxxxxxxx'
  Pusher.secret = 'f7b9a0f68dc6xxxxxxxxxxxxx'

Javascriptを読み込み

ダウンロードしてassets pipelineに入れてもいいかもしれないけど、リンクがおすすめって書いてあったのでとりあえずそうしあた。

We highly recommended that you link to our hosted version which is minified and served by a CDN. By linking to a minor version (e.g. 2.2) you will automatically receive patch updates.

views/layouts/application.html.erb

html
1
2
  <!-- Pusher (Heroku addon) -->
  <script src="//js.pusher.com/2.2/pusher.min.js" type="text/javascript"></script>

環境変数を設定

Javascriptを必要に応じて呼び出したいので、Api_keyを環境変数にしました。
公式ドキュメントにはないけど。api_keyをどうやってproduction/developmentで切り替えるかだけど、HTMLに埋め込んじゃいました。ついでにログインしているユーザーのIDも。
なんかいい方法ないかな。

ローカル環境

1
2
$ vi ~/.bash_profile
export PUSHER_API_KEY="7b01788fd3cxxxxd05470"

Heroku

1
$ heroku config:set YADOKALY_PUSHER_API_KEY=7b01788fdxxxxx05470

HTML(views/layouts/application.html.erbとかどこでも)

html
1
2
<span id="current_user_id" data-value="<%= current_user.id %>" style="display:none;"></span>
<span id="pusher_api_key" data-value="<%= ENV['PUSHER_API_KEY'] %>" style="display:none;"></span>

サーバーサイド

current_userにログインしているユーザーが入っているとうい前提です。
テストとしてpushというメソッドをつくりました。

ruby
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 class PusherController < ApplicationController
  protect_from_forgery :except => :auth # stop rails CSRF protection for this action

  def auth
    if current_user
      response = Pusher[params[:channel_name]].authenticate(params[:socket_id])
      render :json => response
    else
      render :text => "Not authorized", :status => '403'
    end
  end

  def push
    Pusher['private-'+current_user.id.to_s].trigger('new_message', {:sender => current_user.name, :link => "/chatroom/123"})
    render nothing: true
  end

end

対応するRouteを追加します。
config/routes.rb

ruby
1
2
  post 'pusher/auth'
  get 'pusher/push'

クライアントサイド

/pusher/pushを叩くリンクをViewのどこかに入れておいてください。

erb
1
<%= link_to "push", "/pusher/push", remote: true %>

bodyの中に書くとTurbolinksと色々あって、1回ノーティフィケーション送ったつもりが2個も3個も表示されるって状況になったので、ページ遷移するたびにPushrer instancesをカラにしてみました。

assets/javascripts/pusher.coffee

coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
pusher_reset = ->
    # Disconnect and remove all pusher instance
    # to avoide multiple execution.
    if Pusher
        $.each(Pusher.instances, (index, instance) ->
            instance.disconnect()
        )
        Pusher.instances = []
        console.log 'pusher_reset fired.'


ready = ->

    # Pusher notification
    # Pusher['private-1'].trigger() in controller
    current_user_id = $('#current_user_id').attr('data-value')
    api_key = $('#pusher_api_key').attr('data-value')

    if Pusher && current_user_id
        pusher  = new Pusher(api_key, { cluster: 'eu' })
        channel = pusher.subscribe('private-' + current_user_id)

        channel.bind('new_message', (data) ->
            console.log 'fire'
            # Will be executed when the method in controller called.
            if !$('.notify-wrap').length
                $('body').prepend('<div class="notify-wrap"></div>')
            msg = data.sender + ' さんからメッセージを受信しました。'
            msgDiv = '<div class="notify">' + 
                                    '<span class="got-it"><i class="fa fa-times-circle"></i></span>' + 
                                    '<a href="' + data.link + '">' + msg + '</a>' + 
                                '</div>'
            $(msgDiv).prependTo('.notify-wrap').animate({ right: 0 }, 350)
            ### if you want to hide it after some seconds
            setTimeout( ->
                $('#notify').fadeOut()
            , 10000)
            ###
        )
    $('body').on 'click', '.got-it', (e) ->
        e.preventDefault()
        $(this).closest('.notify').remove()

# For turbolinks
$(document).ready(ready)
$(document).on 'page:load', ready
$(document).on 'page:receive', pusher_reset

見た目

ちょっとかっこよくします。
横からスッと入ってくるようにしたいので、right: -300px;をデフォルトにしておいて、
前述の.animate({ right: 0 }, 350) でアニメーションさせてます。

assets/stylesheets/pusher.scss

scss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
$c_light_gray: #F7F7F7;
$c_blue: #114aa9;
$c_gray: #666666;

.notify-wrap {
    z-index: 10;
    position: fixed;
    top: 10px;
    right: 10px;
    .notify {
        position: relative;
        right: -300px;
        width: 200px;
        border-radius: 5px;
        font-size: 0.85em;
        border: 3px solid $c_light_gray;
        background-color: #fff; 
        box-shadow: 0 4px 10px #ddd;
        margin-bottom: 10px;
        &:hover {
            border-color: lighten($c_blue, 42);
        }
        a {
            height: 100%;
            width: 100%;
            display: block;
            padding: 10px 15px;
            &:hover {
                text-decoration: none;
                //color: #fff;
            }
        }
        .got-it {
            position: absolute;
            top: -7px;
            right: -7px;
            font-size: 20px;
            width: 24px;
            height: 24px;
            line-height: 20px;
            border: 2px solid #fff;
            background-color: #fff;
            border-radius: 50%;
            text-align: center;
            cursor: pointer;
            &:hover {
                color: #fff;
                border: 2px solid $c_gray;
                background-color: $c_gray;
            }
        }
    }
}

以上かな。
これで pushボタンを押したらスッとノーティフィケーションが現れるはず。
楽しい!

参考

Pusher | Heroku Dev Center
Adding in-app notifications with Pusher | Heroku Dev Center
Documentation | Pusher
Pusher connections stack up when using turbo links · Issue #48 · chrismccord/sync · GitHub
Turbolinks support for Pusher.js
rails/turbolinks · GitHub

323
Shunsuke Sawada

おすすめの記事

Rails / HTMLメールの作成を簡単にしたい
16
Rails / RSpec テスト書いたことない メンドクサイ(n´Д`)という時のチートシート
223