vue.jsとFirebaseのRealtime DatabaseとAuthorization機能を利用したSlackクローンの構築を複数回にわけて行っています。7回目となる今回はメイン画面のユーザ一覧に表示されているユーザがログイン状態(Slack Cloneのメイン画面をを開いている)にあるかどうかを確認できる機能の実装を行っていきます。

最終的には下記のようにログインしているsusan@test.comとjohn@example.comとログインしていないjack@example.co.jpとkevin@test.comがメールアドレスの左側に表示される色によって一目でわかるようになります。

ユーザのログイン状態がわかる
ユーザのログイン状態がわかる

ユーザの接続情報の確認

ユーザがFirebaseに接続されているかどうか確認できるようにFirebaseデータベース上には”.info/connected”という場所が事前に用意されています。その場所にアクセスを行い、値(true or false)をチェックすることでデータベースに接続しているかどうかを確認することができます。

下記が”.info/connected”の値をチェックするコードです。


firebase
  .database()
  .ref(".info/connected")
  .on("value", snapshot => {
    if (snapshot.val() === true) {
      console.log("connected");
    } else {
      console.log("not connected");
    }
  });
.info/connectedに対してはtureかfalseの値を読み込むだけでこちらから何か情報を書き込みを行うことはありません。

上記のコードを追加し、サインインが完了してデータベースへの接続を行うとChromeではあればデベロッパーツールのコンソールログにconnectedと表示されます。

接続解除された時に処理を行う機能

FirebaseのRealtime Databaseには、ユーザのログイン状態を確認する機能を構築する上で.info/connectedの他にもう一つ重要な機能があります。

その機能はonDisconnectというメソッドを利用するものでブラウザを閉じた時や別のURLに移動した場合にデータベースに対して事前に実行したい処理を設定しておくことができます。

.info/connectedとonDisconnectを利用してユーザがログインしている状態を確認できる仕組みを作ります。英語ではpresence systemなどと呼ばれます。

実際に動作を確認したほうがわかりやすいので下記のコードを確認した後に動作確認を行います。


mounted(){
//略
firebase
  .database()
  .ref(".info/connected")
  .on("value", snapshot => {
    if (snapshot.val() === true) {
      var connectionRef = firebase
        .database()
        .ref("connections")
        .push();
      connectionRef.onDisconnect().remove();
      connectionRef.set({
        message: "ログインしたよ"
      });
    }
  });
  1. サインイン後のHome.vueファイルのライフサイクルフックmountedでデータベースの.info/connectedにアクセスを行いデータベースへの接続が完了しているかチェックを行います。
  2. 接続が完了しているとデータベースのconnectionsという場所にアクセスを行いこの場所の下にsetメソッドでデータを書き込みます。
  3. setで書き込んだデータはonDisconnect().remove()を設定することでブラウザを閉じた時や別のURLに移動した場合に自動でsetで書き込んだ内容を削除してくれます。
connectionsは任意の名前を付けてください。またconnectionsに書き込むデータは決まっているわけではないので自由に設定を行うことができます。公式ドキュメントでは、disconnectmessageと指定しています。また、removeをonDisconnetで設定していますが、setを使って削除ではなく書き込みも行うことができます。

上記のコードをHome.vueファイルに追加し、Firebaseのデータベースのコンソールを見ながら動作確認します。

サインインが完了するとすぐにコンソール上のデータベースのconnectionsが作成され、その下にsetで設定したmessageの”ログインしたよ”が追加されます。

connectionsの下にデータを書き込む
connectionsの下にデータを書き込む

サインインしたブラウザを閉じてください。書き込まれたconnectionsがブラウザを閉じた直後に自動で削除されます。これはonDisconnectでremoveを設定しているためです。

ブラウザで閉じた直後にconnectionが削除される
ブラウザで閉じた直後にconnectionが削除される

info/connectedとonDisconnectを使うことでconnectionsに情報が書き込まれている間はユーザはログインしている状態と考えることができます。

ブラウザを閉じてもユーザのサインインは行われている状態なので再度アプリケーションのURLにアクセスするとユーザ名、パスワードを入力することなくメイン画面にアクセスすることができます。

ログイン状態の確認機能の構築

ユーザがデータベース上のconnectionsの下に情報を書き込むことでそのユーザがアプリケーションにログインしていると判断します。ただ情報を書き込むだけでなくどのユーザがログインしているのか別のユーザが判断するためにconnectionsに何を書き込むかが重要になります。

ユーザを識別するために本アプリケーションではuser_idを利用しているのでuser_idを書き込むことでどのユーザが現在ログインしているかを判断することができます。しかし、同じユーザが複数のタブ、ブラウザまたはPCやスマホ等で同時にアクセスしている場合を考えた場合user_idだけでは情報が十分ではありません。

本アプリケーションではログインしているユーザを一意に識別するためにuser_idとデータベースにデータを追加した時に自動設定されるkeyを保存します。


mounted() {
//略
firebase
  .database()
  .ref(".info/connected")
  .on("value", snapshot => {
    if (snapshot.val() === true) {
      let ref = this.connectionRef.push();
      this.connection_key = ref.key;
      ref.onDisconnect().remove();

      ref.set({
        user_id: this.user.uid,
        key: this.connection_key
      });
    }
  });

connectionsへの接続情報はvue.jsのデータプロパティに追加しています。


data() {
  return {
    user: "",
//略
    connectionRef: firebase.database().ref("connections"),

ブラウザを閉じた時に自動で削除されるだけではなく意図的に追加した情報を削除できるようにデータプロパティconnection_keyをvue.jsに追加して保存しておきます。


  data() {
    return {
      user: "",
//略
      connection_key: "",
サインアウトした時は追加したconnection_keyを利用してconnectionsに追加した情報を意図的に削除します。削除しない場合はサインアウトしたにも関わらずログインしている状態と判断されます。

リスナーを追加した場合は、ライフサイクルフックbeforeDestroyでリスナーを削除する処理も追加しておきます。


beforeDestroy() {
//略
  firebase
    .database()
    .ref(".info/connected")
    .off();
}

サインインを行い、Firebaseのコンソールにuser_idとkeyが表示されるか確認します。

user_idとkeyをデータベースに書き込む
user_idとkeyをデータベースに書き込む

ブラウザを閉じると追加した情報が削除されることを確認してください。またサインアウトした場合もログイン状態ではないので先程追加したconnection_keyを利用して削除します。


signOut() {
  this.connectionRef.child(this.connection_key).remove();
  firebase.auth().signOut();
  this.$router.push("/signin");
},

サインアウトした場合でもconnectionに追加された情報がFirebaseのデータベースから削除されるのかの確認も行ってください。

vue.js側でのユーザのログイン状態の管理

vue.js側ではデータプロパティusersにこのアプリケーションに登録したユーザ情報の一覧を保存しています(データベースからmounted時に取得)。各ユーザのユーザ情報にログインしているどうかを判断するstatusを追加します。

下記のコードではサインインしたユーザ自分自身だけmoutend時にstatusをonlineに設定しています。他のユーザ情報にもstatusを追加しますが値はofflineとしています。この値は後ほどconnectionsから情報を取得してonlineかofflineかを判断します。


mounted() {
  this.user = firebase.auth().currentUser;

  firebase
    .database()
    .ref("users")
    .on("child_added", snapshot => {
      let user = snapshot.val();

      if (this.user.uid == user.user_id) {
        user.status = "online";
      } else {
        user.status = "offline";
      }
      this.users.push(user);
    });

サイドバーに表示されているユーザ一覧はusersをv-forで展開していたのでユーザのログイン状態を表示できるように追加を行います。classにバインドを行い、ユーザがログイン状態の場合はbg-yellow-400を設定、そうでない場合はbg-gray-600を設定しユーザの左側に表示される○の色をログイン状態かどうかで変えています。


<div class="mt-2 flex items-center" v-for="user in users" :key="user.user_id">
  <span
    class="rounded-full w-3 h-3 mr-2"
    :class="[isOnline(user) ? 'bg-yellow-400' : 'bg-gray-600']"
  ></span>
  <span class="opacity-50" @click="directMessage(user)">{{ user.email }}</span>
</div>

isOnline関数は引数にuserをとり、userのstatusでtrueかfalseを戻す処理を行っています。vue.jsのメソッドにisOnline関数の以下を追加します。


isOnline(user) {
  if (user.status === "online") {
    return true;
  } else {
    return false;
  }
}

追加後john@example.comでサインインを行うとstatusがonlineになるため、左側の○の色がログイン状態を表す色になります。

ログイン状態を表す○が表示
ログイン状態を表す○が表示

接続情報をvue.js内でも管理

connectionsに保存されている接続情報についてはサインイン後はvue.js側でも保存させるためデータプロパティのconnectionsを追加します。


data() {
  return {
    user: "",
//略
    connection_key: "",
    connectionRef: firebase.database().ref("connections"),
    connections: []
  };

ユーザがログインした場合

connectionsの情報はchild_addedイベントを利用して追加を行います。ユーザがログインしてconnectionsに追加されるとそのユーザの接続情報をconnectionsに保存し、ユーザ一覧からそのユーザを見つけてstatusをonlineにしています。

this.usersにデータが保存されていない場合はfindを使ってuserを取得できずundefinedになるためundefinedの場合は何も処理を行いません。


firebase
  .database()
  .ref("connections")
  .on("child_added", snapshot => {
    let new_connection = snapshot.val();
    this.connections.push(new_connection);

    let user = this.users.find(
      user => user.user_id === new_connection.user_id
    );

    if (user != undefined) {
      user.status !== "online";
    }
  });

ユーザがログアウトした場合

ユーザが自らサインアウトした場合だけではなくブラウザを閉じた場合もconnectionsに保存された接続情報を削除されます。その情報を受け取り反映されるためにchild_removedイベントを設定してconnectinonsから情報が削除されることを監視します。


firebase
  .database()
  .ref("connections")
  .on("child_removed", snapshot => {
    let remove_connection = snapshot.val();

    this.connections = this.connections.filter(
      connection => connection.key !== remove_connection.key
    );

    let index = this.connections.findIndex(connection => {
      return connection.user_id === remove_connection.user_id;
    });

    if (index === -1) {
      let user = this.users.find(
        user => user.user_id == remove_connection.user_id
      );
      user.status = "offline";
    }
  });

ユーザがログアウトするとデータベースのconnectionsからログアウトしたユーザの情報が削除されます。削除されるとchild_removedイベントにより削除したユーザの情報(user_id, key)が取得できます。

keyを利用してvue.jsに保存されているconnections情報からそのkeyを持つ接続情報を削除します。

次にuser_idを使って、削除後のconnectionsの中に同じuser_idを持つユーザ情報があるかチェックを行います。indexが-1の場合は同じuser_idを持つユーザが存在しないため、statusをoffllineにします。indexが-1以外の場合は別のタブやブラウザで接続している同じユーザの情報が残っているためstatusをofflineにしません。

これらのコードを追加後サイドバーの各ユーザの右の○がログイン状態を表す色になっているのか確認を行ってください。

注意点

1つのブラウザで複数のタグを開いてサインアウトした場合は残りのブラウザでも自動でサインアウトが行われます。しかし、データベースのconnenctionsには自動でサインアウトしたユーザの情報が残ることになります。その後別のURLに移動したりタブを閉じると情報は削除されます。

このように本実装では対応できない箇所もまだまだあると思うのでこのようなシステムを作る際のヒントを何かしら得ていただければ大変ありがたいです。