[Firebase] RealtimeDatabaseでオンライン状態を判定する

今回はFirebaseのRealtimeDatabaseで、ログイン済みのユーザーがオンラインかオフライン化を表示するツールを作成します。以下のようにオンラインなら緑色、オフラインならグレーのアイコンが名前の横に表示されます。

開発

動作原理

公式ドキュメントのサンプルそのままですが、onDisconnect()を利用し、Firebaseから切断されたら適当なドキュメントに書き込む処理を定義しています。

ポイントはオンライン状態のときに、切断時にセットする値をあらかじめ定義することですね。冷静になって考えるとわかりますがオフラインになってからだと通信そのものができませんからね。

    firebase.database().ref('.info/connected').on('value', (snapshot) => {
      if ( snapshot.val() === false ) {
        return;
      };
      userStatusDatabaseRef.onDisconnect().set(isOfflineForDatabase).then( () => {
        // 一番最初にonlineであることを書き込む
        userStatusDatabaseRef.set(isOnlineForDatabase);
      });
    });

あとは先ほどのドキュメントをリッスンし、onDisconnect()が実行されオフラインになった旨の値が記述されているかをチェックしてやります。

    let statusRef = firebase.database().ref('/status');
    statusRef.on('value', (snapshot)=>{
        snapshot.forEach( (childSnapshot) => {
          let key   = childSnapshot.key;     // "/status/xxxxx"のID部分が取れる
          let data  = childSnapshot.val();   // ドキュメント内のデータを取得
          let state = (data.state==="online")? "fa-online":"fa-offline";
        }
     }

実際のRealtimeDatabase上には以下のように保存されます。

セキュリティルール

今回は以下のようなルールを適用しました。

{
  "rules": {
    ".read": true,
    "status":{
      ".write": "auth !== null",
      "$uid":{
        ".write": "auth.uid === $uid"
      }
    }
  }
}
  • 参照は誰でもOK
  • status配下への書き込みは認証されているユーザーならOK
  • status配下のドキュメントの書き込み(更新)は作成した本人のみOK

ソースコード

config.js

Firebaseのコンソールをそのまま貼り付けます。

// コンソールの内容をそのままコピペ
var config = {
  apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  authDomain: "test-f76bc.firebaseapp.com",
  databaseURL: "https://test-f76bc.firebaseio.com",
  projectId: "test-f76bc",
  storageBucket: "test-f76bc.appspot.com",
  messagingSenderId: "1111111111111111"
};
firebase.initializeApp(config);

index.html

本体です。 オンライン状態の表示にはFont AwesomeのCSSを利用しています。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>RealtimeDatabase Online Cheker</title>
  <style type="text/css">
    ul { width:300px; height:300px; border: 1px solid gray; padding-top:10px; padding-left:10px; overflow-y:scroll;}
    li { list-style-type: none; }
    .fa-online{color:green;}
    .fa-offline{color:lightgray;}
    .hide {display:none;}
  </style>
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
</head>
<body>

<h1>Online Cheker</h1>
<ul id="userlist">
</ul>
<form action="login.html" id="form-login" class="hide">
  <button>ログイン</button>
</form>

<script src="https://www.gstatic.com/firebasejs/5.8.1/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/5.8.1/firebase-auth.js"></script>
<script src="https://www.gstatic.com/firebasejs/5.8.1/firebase-database.js"></script>
<script src="/js/config.js"></script>
<script>
  firebase.auth().onAuthStateChanged( (user) => {
    //ログインボタンの表示ON/OFF
    document.getElementById("form-login").classList.toggle("hide", (user !== null) );
    if( user === null ){
      return(null);
    }

    let userStatusDatabaseRef = firebase.database().ref(`/status/${user.uid}`);
    let isOfflineForDatabase = {
        state: 'offline',
        name: user.displayName,
        last_changed: firebase.database.ServerValue.TIMESTAMP,
    };
    let isOnlineForDatabase = {
        state: 'online',
        name: user.displayName,
        last_changed: firebase.database.ServerValue.TIMESTAMP,
    };

    firebase.database().ref('.info/connected').on('value', (snapshot) => {
      if ( snapshot.val() === false ) {
        return;
      };
      userStatusDatabaseRef.onDisconnect().set(isOfflineForDatabase).then( () => {
        // 一番最初にonlineであることを書き込む
        userStatusDatabaseRef.set(isOnlineForDatabase);
      });
    });
  });

  window.onload = ()=>{
    let statusRef = firebase.database().ref('/status');
    statusRef.on('value', (snapshot)=>{
        snapshot.forEach( (childSnapshot) => {
          let key   = childSnapshot.key;     // "/status/xxxxx"のID部分が取れる
          let data  = childSnapshot.val();   // ドキュメント内のデータを取得
          let state = (data.state==="online")? "fa-online":"fa-offline";

          if( document.getElementById(key) === null ){
            document.getElementById("userlist").insertAdjacentHTML("beforeend",
              `<li id="${key}"><i class="fas fa-circle ${state}"></i> ${data.name}</li>`
            );
          }
          else{
            document.getElementById(key).innerHTML = `<i class="fas fa-circle ${state}"></i> ${data.name}`;
          }
        });
      })
  };
</script>
</body>
</html>

login.html

Firebaseでログインするためのフォームです。ログイン後はindex.htmlへ戻ります。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>RealtimeDatabase Online Cheker</title>
  <link type="text/css" rel="stylesheet" href="https://cdn.firebase.com/libs/firebaseui/3.5.2/firebaseui.css" />
  <style>#auth{text-align: center;}</style>
</head>
<body>
  <section id="auth">
    <h1>Online Cheker</h1>
    <p>ログインまたは新規登録をしてください</p>
    <div id="firebaseui-auth-container"></div>
  </section>

  <script src="https://www.gstatic.com/firebasejs/5.8.1/firebase-app.js"></script>
  <script src="https://www.gstatic.com/firebasejs/5.8.1/firebase-auth.js"></script>
  <script src="https://www.gstatic.com/firebasejs/ui/3.5.2/firebase-ui-auth__ja.js"></script>
  <script src="/js/config.js"></script>
  <script>
    //----------------------------------------------
    // Firebase UIの設定
    //----------------------------------------------
    var uiConfig = {
        // ログイン完了時のリダイレクト先
        signInSuccessUrl: '/db/realtimedb1/',

        // 利用する認証機能
        signInOptions: [
          firebase.auth.EmailAuthProvider.PROVIDER_ID  //メール認証
        ],

        // 利用規約のURL(任意で設定)
        tosUrl: 'http://example.com/kiyaku/',
        // プライバシーポリシーのURL(任意で設定)
        privacyPolicyUrl: 'http://example.com/privacy'
      };

      var ui = new firebaseui.auth.AuthUI(firebase.auth());
      ui.start('#firebaseui-auth-container', uiConfig);
  </script>
</body>
</html>

課題

onlineのまま死ぬ問題

何らかの事情でデータが"online"のままで終了してしまうことがありますので、実際に使用する際には以下のような対策を施したほうが良いでしょう。

  1. 定期的にlast_changedの値を更新し続ける
  2. クライアント側でlast_changedの値が一定時間変化しなければオフライン扱いにする

ユーザーがたまっていく問題

このまま稼働させ続けると、サービスによってはがんがんドキュメントがたまっていきます。そのためいずれかのタイミングで削除する処理も必要になってくる可能性があります。

その場合には例えば次のような処理を実装してやります。

  • 「ログアウト」ボタンを設置し、FirebaseのAuthからログアウトしたと同時にRealtimeDatabase上からもドキュメントを削除
  • CloudFunctionsを特定のタイミングで起動し、last_changedが一定時間経過していれば削除

参考ページ

firebase.google.com firebase.google.com firebase.google.com