個人開発のWebアプリ**「まちかどルート」**をv5.0正式版にむけての開発中、テスト版であるv5.0rc3へ実装したときのメモです。
プログラミングに入門してちょうど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
も当然更新されてしまうわけで、online
とupdated_at
をペア条件にしてユーザー情報を抽出したり並べ替えすると不都合が出ました。ゆえに、ログイン/ログアウトの状態変化の日時を保存するカラムとしてonline_at
を追加しました。
なおmodelにもひと工夫が必要でした。それについては後述します。
Action Cableの主要ファイル
Action Cableの基本的な初期設定はここでは省き、要点だけメモとして残します。
$ rails g channel appearance
このようにして、まずはappearance
というAction Cableのchannelを作成します。
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
カラムには日時を記録します。
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.rb
でcurrent_user.id
を参照しているのですが、Action Cableでは動作しませんでした。current_user
はヘルパーメソッド化してあったのでどこからでも参照できるかと思っていましたが、Action Cableではそうもいかないみたいでした。
そこでいろいろ調べた結果、cookieを橋渡しに使う方法があるということでした。
具体的には
cookies.encrypted[:user_id] = @current_user.id
というcookie保存用の1行を、セッションが生まれるsessions_controller.rb
やposts_controller.rb
といったcontrollerのファイルに追記しておきます。
このcookieを橋渡しにしてAction Cableからcurrent_user.id
が参照可能になる、というわけです。
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.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
部分にわたされます。
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
カラムが記録されているユーザーのみが抽出され、それを日時の新しい順に並べ替えています。
<span id="users-list" class="logbox">
<%= render partial: 'online_users', collection: @online_users, as: :member %>
</span>
<% 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に対してキューをわたす)ように設定できません。
そこで下記のようにしました。
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というのが使えるようになったりするとか。
これからも新しい学びを楽しんでいきたいです。