LoginSignup
3
7

More than 3 years have passed since last update.

【Rails】Action Cableで全ユーザーのログイン履歴をWebアプリにリアルタイム表示

Last updated at Posted at 2019-01-25

個人開発のWebアプリまちかどルートをv5.0正式版にむけての開発中、テスト版であるv5.0rc3へ実装したときのメモです。

v5_0rc3_pc.png

プログラミングに入門してちょうど7ヶ月。サーバーとの双方向通信を実現するRails 5.2の新機能Action Cableを、チャットルームのようなタイムライン表示以外にも活用できないかと考え、今回の実装に至りました。

データベースの準備

$ rails g migration AddOnlineToUsers online:boolean
$ rails g migration AddOnline_atToUsers online_at:datetime

のあと、たとえば上記1行目のマイグレーションファイルを

class AddOnlineToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :online, :boolean, default: :false
  end
end

というふうに編集してからマイグレーションを実行。

$ rails db:migrate

以上でデータベースの準備が完了しました。

このonlineカラムには、ログイン/ログアウトつまりAction Cableでいうところのsubscribed(購読)/unsubscribed(購読解除)の状態変化がtrue/falseとして書き込まれます。

ちなみに、データベースのカラムに元々updated_atがあるのに、なぜわざわざonline_atも追加したかというと、それなりの理由があります。

というのもonline以外のカラムが更新されたときupdate_atも当然更新されてしまうわけで、onlineupdated_atをペア条件にしてユーザー情報を抽出したり並べ替えすると不都合が出ました。ゆえに、ログイン/ログアウトの状態変化の日時を保存するカラムとしてonline_atを追加しました。

なおmodelにもひと工夫が必要でした。それについては後述します。

Action Cableの主要ファイル

Action Cableの基本的な初期設定はここでは省き、要点だけメモとして残します。

$ rails g channel appearance

このようにして、まずはappearanceというAction Cableのchannelを作成します。

/app/channels/appearance_channel.rb
class AppearanceChannel < ApplicationCable::Channel
  def subscribed
    member = User.where(id: current_user.id).first
    return unless member
    member.update_attributes(online: true, online_at: DateTime.now)
    stream_from "appearance_user"
  end

  def unsubscribed
    member = User.where(id: current_user.id).first
    return unless member
    member.update_attributes(online: false, online_at: DateTime.now)
  end
end

channelには、前述したように購読と購読解除の際onlineカラムをtrue/falseで記録するように書きました。と同時にonline_atカラムには日時を記録します。

/app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    private
      def find_verified_user
        if verified_user = User.find_by(id: cookies.encrypted[:user_id])
          verified_user
        else
          reject_unauthorized_connection
        end
      end
  end
end

ログイン認証済みのユーザーだけに限定するようにしてあります。ここがけっこう苦戦しました。

というのもひとつ前のappearance_channel.rbcurrent_user.idを参照しているのですが、Action Cableでは動作しませんでした。current_userはヘルパーメソッド化してあったのでどこからでも参照できるかと思っていましたが、Action Cableではそうもいかないみたいでした。

そこでいろいろ調べた結果、cookieを橋渡しに使う方法があるということでした。

具体的には

cookies.encrypted[:user_id] = @current_user.id

というcookie保存用の1行を、セッションが生まれるsessions_controller.rbposts_controller.rbといったcontrollerのファイルに追記しておきます。

このcookieを橋渡しにしてAction Cableからcurrent_user.idが参照可能になる、というわけです。

/app/jobs/appearance_broadcast_job.rb
class AppearanceBroadcastJob < ApplicationJob
  queue_as :default

  def perform(user)
    ActionCable.server.broadcast "appearance_user", render_json(user)
  end

  private
  def render_json(user)
    ApplicationController.renderer.render(json: user)
  end
end

このファイルの記述についてはお決まりのようなものみたいです。通信するユーザー情報をJSON形式でレンダーするようになっていますね。

view関連ファイル

/app/assets/javascripts/channels/appearance.js
App.appearance = App.cable.subscriptions.create({
  channel:'AppearanceChannel'
 }, {
  received: function(data) {
    var user = JSON.parse(data)

    if (user.online === true){
      var element = document.getElementById("users-list");
        element.insertAdjacentHTML("afterbegin", "<li class='user-login'>" + user.display_name + "</li>");
    };
    if (user.online === false){
      var element = document.getElementById("users-list");
        element.insertAdjacentHTML("afterbegin", "<li class='user-logout'>" + user.display_name + "</li>");
    };
  }
});

viewに対してリアルタイムにログイン履歴を反映するとき使われるjsファイルです。前述のappearance_broadcast_job.rbによってログイン/ログアウトしたユーザーの情報がJSON形式としてuserに格納されていますので、そのなかのonline情報がtrueならばログイン状態を表すCSS(ここではclass='user-login')、falseならログアウト状態を表すCSSを付けてリアルタイムに下記viewのusers-list部分にわたされます。

/app/controllers/posts_controller.rb
cookies.encrypted[:user_id] = @current_user.id
@online_users = User.where.not(online_at: nil).order(online_at: :desc)

controllerでは前述のcookieの1行と、viewに使う@online_usersを設定しておきます。@online_usersに格納されるデータには、online_atカラムが記録されているユーザーのみが抽出され、それを日時の新しい順に並べ替えています。

/app/views/posts/index.html.erb
<span id="users-list" class="logbox">
    <%= render partial: 'online_users', collection: @online_users, as: :member %>
</span>
/app/views/posts/_online_users.html.erb
<% if member.online == true %>
    <li class="user-login">
       <%= member.display_name %>
    </li>
<% elsif member.online == false %>
    <li class="user-logout">
       <%= member.display_name %>
    </li>
<% end %>

viewでは部分テンプレートを使っています。appearance.jsと連携してリアルタイムに更新されるようid="users-list"を指定してあるのがポイントです。

modelにひと工夫が必要でした

modelについて、ネットでしばしば見かける定番の記述が

after_create_commit { AppearanceBroadcastJob.perform_later self }

という1行だけだと思います。

でもそれではonlineカラムが更新されたときだけAction Cableが通信する(Jobに対してキューをわたす)ように設定できません。

そこで下記のようにしました。

/app/models/user.rb
class User < ApplicationRecord
(中略)
  after_update_commit :watchonline_self

  def watchonline_self
    if saved_change_to_online?
      AppearanceBroadcastJob.perform_later(self)
    end
  end
end

watchonline_selfというメソッドを作り、saved_change_to_online?によってonlineカラムが更新されたときだけAction CableがJobに対してキューをわたすようにしました。

あとがき

とても駆け足?でまとめました。いろいろ試行錯誤・紆余曲折を経て今回のコードに至ったので、もしかしたら抜けやミスがあるかもしれません。

とりあえずログイン履歴が実現できてよかったです。じぶんの開発したWebアプリで、みんながネトゲさながらにログイン/ログアウトしていく様子を見られるだけで楽しいものですね。

次期Rails 6ではAction Textというのが使えるようになったりするとか。
これからも新しい学びを楽しんでいきたいです。

3
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
7