ピティナ開発者ブログ

全日本ピアノ指導者協会のIT担当者が気まぐれにつづる技術系中心のブログです


Firebaseで「この記事は現在●人が閲覧中」を実装

はじめに

しばらく前からプライベートでは登録したまま放置していたFirebaseを使ってみました。

firebase.google.com

きっかけは、以下の記事。

tech.plaid.co.jp

確かに、「●人が閲覧中」が手軽に実装できれば面白そうだなーと思って触りはじめてみたら、上記の記事には具体的な実装例までは書かれていなくて、なんとなく「やってやる、やってやるぞ!」(CV:島田敏)な気持ちになってきたので、意地で実装しました。
今、右下に表示されていると思います(Smoochのチャットボタンの真下あたりに)

Firebaseを使う準備

まずはFirebaseに登録!
登録はほんと簡単で、Googleアカウントがあれば何も入力する必要もなく、そのまま完了します。
続いて、アプリを作成する~という過程に進んでいきますが、このあたりまでは特に解説必要ないかな……と。
WebのSDKを選ぶとJavaScriptコードが発行されるので、指定通りにページのフッタの方へinitializeコードを設置しましょう。

とりあえず書き込む

じゃあ、具体的にデータベースへの書き込みをおこなってみます。

それについても、とても簡単で、例えば以下のように。

var pagePath = 'page/' + encodeURIComponent(location.pathname).replace(/\./g, '%2E');
updatePageData(pagePath);

function updatePageData(pagePath){
  var ref = firebase.database().ref(pagePath);
  ref.update({
    title: document.title,
    count: ''
  });
}

これで、page以下に、アクセスしたURLのドメインより下を各Keyにしてくれて、最後のところへtitleとcountをセットしてくれます。
初アクセスだとしてもupdateで問題ないです。

今回必要な、Firebaseのdocumentを貼っておきます。

Interface: DataSnapshot  |  Firebase Interface: Reference  |  Firebase Read and Write Data on the Web  |  Firebase

次に、userも必要ですが、アクセスするたびにuserが新しく作られていくのも何なので、時間設定しないcookieにuserIdを保管するようにしておきます。
cookie読み書きにjquery使うのもなーという感じがするのですが、以下記事が素敵な感じな書き方にまとめてくださっていたのでこれを参考に。

javascriptでクッキーの読み書きを行う方法 | xxxx7

cookieを読んでuserIdが見つからなかったらuserIdを生成した上でcookieへ保存、見つかったらそれをそのままuserIdとして使う、ということで。

  function getUserId(cookieKey, cookieUserId){
    if(cookieUserId == false){
      var userId = getUniqueStr();
      setCookie(cookieKey, userId);
    } else {
      var userId = getCookie('UserId');
    }
    return userId;
  }
  
  function setCookie(key, value){
    var cookie = key + '=' + value;
    document.cookie = cookie;
  }
  
  function getCookie(key){
    var cookies = document.cookie.split('; ');
    for (var i = 0; i < cookies.length; i++){
      var c = cookies[i].split('=');
      if(c[0] == key){
        return c[1];
      }
    }
    return false;
  }

  function getUniqueStr(myStrong){
    var strong = 1000;
    if (myStrong) strong = myStrong;
    return new Date().getTime().toString(16)  + Math.floor(strong*Math.random()).toString(16);
  }

なお、ランダムな文字列生成は以下の記事から拝借いたしました(まあ、今回は特にstrongに設定する必要もないかなと)

qiita.com

で、userをセットしましょう。

var userId = getUserId(cookieKey, cookieUserId);
var userPath = 'user/' + userId;

updateUserData(userId, userPath);

function updateUserData(userId, UserPath){
  var ref = firebase.database().ref(userPath + '/visit/' + location.pathname);
  ref.update({
    datetime: getDateTime(),
    timestamp: parseInt( new Date() /1000 )
  });
}

function getDateTime(){
  var d = new Date();
  var year  = d.getFullYear();
  var month = d.getMonth() + 1;
  var day   = d.getDate();
  var hour  = ( d.getHours()   < 10 ) ? '0' + d.getHours()   : d.getHours();
  var min   = ( d.getMinutes() < 10 ) ? '0' + d.getMinutes() : d.getMinutes();
  var sec   = ( d.getSeconds() < 10 ) ? '0' + d.getSeconds() : d.getSeconds();
  return ( year + '-' + month + '-' + day + ' ' + hour + ':' + min + ':' + sec );
}

別にtimestampだけでもいいのですが、datetimeも取りました。
ほとんどデバッグ用みたいなものですが一応。
変換の関数は以下から拝借。
Date.now(); の方がパフォーマンス良いのは知ってますが引用ということでそのままに。

yut.hatenablog.com

最後に、ユーザが訪れたページ毎に、ユーザが訪れた時刻をセットしてあげます。

updatePageAndUserData(pagePath, userId);

function updatePageAndUserData(pagePath, userId){
  var path = pagePath + '/visit/' + userId;
  var ref = firebase.database().ref(path);
  ref.update({
    datetime: getDateTime(),
    timestamp: parseInt( new Date() /1000 )
  });
}

これでひとまず、pageとuserを書き込むだけ書き込むのはできました。
timestampは、ミリ秒までは要らん、ということで。
続いて、カウント処理を作ります。

カウント処理

分かってしまえばアッサリ目です。

function updateUserCountParent(pagePath){
  var ref = firebase.database().ref(pagePath + '/visit');
  ref.on('value', snapshot => {
    updateUserCount(snapshot.numChildren());
  });
}
  
function updateUserCount(numChildren) {
  var ref = firebase.database().ref(pagePath);
  ref.update({
    count: numChildren
  });
}

ちなみに公式のdocumentだと、

function(snapshot){
}

の書き方が主要ですが、ただでさえ前後に()が続いていて見づらいので、

snapshot => {
}

の書き方の方が素敵だと思います。

さて、これでcountの値が更新されていくようになりましたので、この値を取得して、DOMを書き換えましょう。

最新のcount値でDOMを書き換え

これも答えを知ってしまえば簡単なのですが以下のような具合に。

function getUserCount(pagePath, countKey){
  var ref = firebase.database().ref(pagePath).child(countKey);
  ref.on('value', snapshot => {
    var current_user_count = snapshot.val();
    document.getElementById("current_user_count").innerHTML = current_user_count;
  });
}

「document.getElementByIdとかクソダセェっすよ」って、ちょっと前の記事に後輩が書いていたのはキニシナイ。
速いんだもの。

これで、

<div id="current_user">
この記事は現在<span id="current_user_count">0</span>人が閲覧中
</div>

のspanタグの中身が書き換えられるようになりました。

が、Firebaseは残念なことに、Firebase自体の機能として時限式のkey削除みたいなものはありません。
つまり、このままだと、どんどんと閲覧者が増え続けることに!
それは我々のぬか喜びに繋がって、嬉しくないことになるため、削除機能を自分で実装する必要があります。
ので、仕上げにそれを。

一定時間が経過したらpageにぶら下がっているuserを削除する

本気でリアルタイム監視をしたいならFirebaseであればできますが、今回、そこまでやらなくても感があるので、「10分経ったらpageにぶら下がっているuserを消す」という仕様でいきます。
ウチのブログで一つの記事を10分以上見ている方もいらっしゃるまい……

今回、一番詰まったのがこの部分でした。
参考にしたのは以下ですが、

stackoverflow.com stackoverflow.com

何かちょっと求めているものと違う。
最終的には、forEach使って以下のようにすることで実現できました。

function removeOldData(pagePath){
  var path = pagePath + '/visit/';
  var now = parseInt( new Date() /1000 );
  var cutoff = now - 10 * 60;
  var ref = firebase.database().ref(path);
  var old_ref = ref.orderByChild('timestamp').endAt(cutoff);
  old_ref.on('value', snapshot => {
    snapshot.forEach( childSnapshot =>{
      childSnapshot.ref.remove();
    });
  });
}

childSnapshot と childSnapshot.ref.remove(); っていうのが肝ですね。
ここになかなか辿り着けなかった……

以上、これで晴れて完了です。
適当にパクッていただければ、どなたでも「この記事は現在●人が閲覧中」を実装できます。
Baasはなかなかに素敵ですね。
サーバ要らず。

これ以外にも色々な使い方が考えられますが、まずは簡易な機能実装として何らか触ってみると、癖が分かって良いと思います。

余談

最初、記事のタイトルを、【Firebaseで「この記事は現在●人が閲覧中」を簡単に実装】にしようかなと思っていたのですが、以下の記事を見たのを思い出して心を改めました。

uxmilk.jp

簡単っちゃ簡単かもしれないけれど簡単じゃないよ。

(著: Hiroyuki Noguchi)
この記事は現在0人が閲覧中