Firebase Summit のすべての発表内容に目を通し、Firebase を活用してアプリ開発を加速し、自信を持ってアプリを実行できる方法をご確認ください。 詳細

CloudFirestoreでプレゼンスを構築する

コレクションでコンテンツを整理 必要に応じて、コンテンツの保存と分類を行います。

作成しているアプリの種類によっては、アクティブにオンラインになっているユーザーまたはデバイスを検出することが役立つ場合があります。これは、「プレゼンス」の検出とも呼ばれます。

たとえば、ソーシャル ネットワークのようなアプリを構築している場合、または IoT デバイスのフリートを展開している場合、この情報を使用して、オンラインで自由にチャットできる友人のリストを表示したり、IoT デバイスを「最後に見たもの」で並べ替えたりすることができます。 ."

Cloud Firestore はプレゼンスをネイティブにサポートしていませんが、他の Firebase プロダクトを活用してプレゼンス システムを構築できます。

ソリューション: Cloud Functions と Realtime Database

Cloud Firestore を Firebase Realtime Database のネイティブ プレゼンス機能に接続するには、Cloud Functions を使用します。

Realtime Database を使用して接続ステータスを報告し、Cloud Functions を使用してそのデータを Cloud Firestore にミラーリングします。

Realtime Database でのプレゼンスの使用

まず、従来のプレゼンス システムが Realtime Database でどのように機能するかを考えてみましょう。

ウェブ

// Fetch the current user's ID from Firebase Authentication.
var uid = firebase.auth().currentUser.uid;

// Create a reference to this user's specific status node.
// This is where we will store data about being online/offline.
var userStatusDatabaseRef = firebase.database().ref('/status/' + uid);

// We'll create two constants which we will write to 
// the Realtime database when this device is offline
// or online.
var isOfflineForDatabase = {
    state: 'offline',
    last_changed: firebase.database.ServerValue.TIMESTAMP,
};

var isOnlineForDatabase = {
    state: 'online',
    last_changed: firebase.database.ServerValue.TIMESTAMP,
};

// Create a reference to the special '.info/connected' path in 
// Realtime Database. This path returns `true` when connected
// and `false` when disconnected.
firebase.database().ref('.info/connected').on('value', function(snapshot) {
    // If we're not currently connected, don't do anything.
    if (snapshot.val() == false) {
        return;
    };

    // If we are currently connected, then use the 'onDisconnect()' 
    // method to add a set which will only trigger once this 
    // client has disconnected by closing the app, 
    // losing internet, or any other means.
    userStatusDatabaseRef.onDisconnect().set(isOfflineForDatabase).then(function() {
        // The promise returned from .onDisconnect().set() will
        // resolve as soon as the server acknowledges the onDisconnect() 
        // request, NOT once we've actually disconnected:
        // https://firebase.google.com/docs/reference/js/firebase.database.OnDisconnect

        // We can now safely set ourselves as 'online' knowing that the
        // server will mark us as offline once we lose connection.
        userStatusDatabaseRef.set(isOnlineForDatabase);
    });
});

この例は、完全な Realtime Database プレゼンス システムです。複数の切断、クラッシュなどを処理します。

Cloud Firestore への接続

Cloud Firestore で同様のソリューションを実装するには、同じ Realtime Database コードを使用してから、Cloud Functions を使用して Realtime Database と Cloud Firestore の同期を維持します。

まだ行っていない場合は、 Realtime Databaseをプロジェクトに追加し、上記のプレゼンス ソリューションを含めます。

次に、次の方法でプレゼンス状態を Cloud Firestore に同期します。

  1. アプリがオフラインであることを認識できるように、オフライン デバイスの Cloud Firestore キャッシュに対してローカルで。
  2. グローバルに Cloud Function を使用して、Cloud Firestore にアクセスする他のすべてのデバイスがこの特定のデバイスがオフラインであることを認識できるようにします。

Cloud Firestore のローカル キャッシュの更新

Cloud Firestore のローカル キャッシュの更新という最初の問題を解決するために必要な変更を見てみましょう。

ウェブ

// ...
var userStatusFirestoreRef = firebase.firestore().doc('/status/' + uid);

// Firestore uses a different server timestamp value, so we'll 
// create two more constants for Firestore state.
var isOfflineForFirestore = {
    state: 'offline',
    last_changed: firebase.firestore.FieldValue.serverTimestamp(),
};

var isOnlineForFirestore = {
    state: 'online',
    last_changed: firebase.firestore.FieldValue.serverTimestamp(),
};

firebase.database().ref('.info/connected').on('value', function(snapshot) {
    if (snapshot.val() == false) {
        // Instead of simply returning, we'll also set Firestore's state
        // to 'offline'. This ensures that our Firestore cache is aware
        // of the switch to 'offline.'
        userStatusFirestoreRef.set(isOfflineForFirestore);
        return;
    };

    userStatusDatabaseRef.onDisconnect().set(isOfflineForDatabase).then(function() {
        userStatusDatabaseRef.set(isOnlineForDatabase);

        // We'll also add Firestore set here for when we come online.
        userStatusFirestoreRef.set(isOnlineForFirestore);
    });
});

これらの変更により、ローカルのCloud Firestore の状態が常にデバイスのオンライン/オフライン ステータスを反映するようになりました。これは、 /status/{uid}ドキュメントをリッスンし、データを使用して UI を変更して接続ステータスを反映できることを意味します。

ウェブ

userStatusFirestoreRef.onSnapshot(function(doc) {
    var isOnline = doc.data().state == 'online';
    // ... use isOnline
});

Cloud Firestore をグローバルに更新する

このアプリケーションはオンライン プレゼンスを自身に正しく報告しますが、「オフライン」ステータスの書き込みはローカルのみであり、接続が復元されたときに同期されないため、このステータスは他の Cloud Firestore アプリではまだ正確ではありません。これに対抗するために、Realtime Database のstatus/{uid}パスを監視する Cloud Function を使用します。 Realtime Database の値が変更されると、すべてのユーザーのステータスが正しくなるように、値が Cloud Firestore に同期されます。

Node.js

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

// Since this code will be running in the Cloud Functions environment
// we call initialize Firestore without any arguments because it
// detects authentication from the environment.
const firestore = admin.firestore();

// Create a new function which is triggered on changes to /status/{uid}
// Note: This is a Realtime Database trigger, *not* Firestore.
exports.onUserStatusChanged = functions.database.ref('/status/{uid}').onUpdate(
    async (change, context) => {
      // Get the data written to Realtime Database
      const eventStatus = change.after.val();

      // Then use other event data to create a reference to the
      // corresponding Firestore document.
      const userStatusFirestoreRef = firestore.doc(`status/${context.params.uid}`);

      // It is likely that the Realtime Database change that triggered
      // this event has already been overwritten by a fast change in
      // online / offline status, so we'll re-read the current data
      // and compare the timestamps.
      const statusSnapshot = await change.after.ref.once('value');
      const status = statusSnapshot.val();
      functions.logger.log(status, eventStatus);
      // If the current timestamp for this data is newer than
      // the data that triggered this event, we exit this function.
      if (status.last_changed > eventStatus.last_changed) {
        return null;
      }

      // Otherwise, we convert the last_changed field to a Date
      eventStatus.last_changed = new Date(eventStatus.last_changed);

      // ... and write it to Firestore.
      return userStatusFirestoreRef.set(eventStatus);
    });

この関数をデプロイすると、完全なプレゼンス システムが Cloud Firestore で実行されます。以下は、 where()クエリを使用してオンラインまたはオフラインになったユーザーを監視する例です。

ウェブ

firebase.firestore().collection('status')
    .where('state', '==', 'online')
    .onSnapshot(function(snapshot) {
        snapshot.docChanges().forEach(function(change) {
            if (change.type === 'added') {
                var msg = 'User ' + change.doc.id + ' is online.';
                console.log(msg);
                // ...
            }
            if (change.type === 'removed') {
                var msg = 'User ' + change.doc.id + ' is offline.';
                console.log(msg);
                // ...
            }
        });
    });

制限事項

Realtime Database を使用して Cloud Firestore アプリにプレゼンスを追加することはスケーラブルで効果的ですが、いくつかの制限があります。

  • デバウンス- Cloud Firestore でリアルタイムの変更をリッスンする場合、このソリューションは複数の変更をトリガーする可能性があります。これらの変更が必要以上のイベントをトリガーする場合は、Cloud Firestore イベントを手動でデバウンスします。
  • 接続- この実装は、Cloud Firestore ではなく、Realtime Database への接続を測定します。各データベースへの接続ステータスが同じでない場合、このソリューションは誤ったプレゼンス状態を報告する可能性があります。
  • Android - Android では、非アクティブ状態が 60 秒続くと、Realtime Database がバックエンドから切断されます。非アクティブとは、開いているリスナーや保留中の操作がないことを意味します。接続を開いたままにするには、値イベント リスナーを.info/connected以外のパスに追加することをお勧めします。たとえば、各セッションの開始時にFirebaseDatabase.getInstance().getReference((new Date()).toString()).keepSynced()を実行できます。詳細については、「接続状態の検出」を参照してください。