在 Cloud Firestore 中建構產品

視您建構的應用程式類型而定,偵測哪些使用者或裝置正在線上 (也就是偵測「上線狀態」) 可能會很有幫助。

舉例來說,如果您正在建構社群網路等應用程式,或部署一組 IoT 裝置,則可使用這項資訊顯示線上且可聊天的朋友清單,或依「上次上線時間」排序 IoT 裝置。

Cloud Firestore 原生不支援線上狀態,但您可以利用其他 Firebase 產品建構線上狀態系統。

解決方案:Cloud Functions 搭配 即時資料庫

如要將 Cloud Firestore 連結至 Firebase 即時資料庫的原生線上狀態功能,請使用 Cloud Functions。

使用即時資料庫回報連線狀態,然後使用 Cloud Functions 將該資料鏡像到 Cloud Firestore。

在即時資料庫中使用上線狀態

首先,請思考傳統上線狀態系統在即時資料庫中的運作方式。

網頁

// 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);
    });
});

這個範例是完整的即時資料庫上線狀態系統。可處理多次中斷連線、當機等問題。

正在連線至「Cloud Firestore」

如要在 Cloud Firestore 中實作類似的解決方案,請使用相同的即時資料庫程式碼,然後使用 Cloud Functions 讓即時資料庫和 Cloud Firestore 保持同步。

如果您尚未將即時資料庫新增至專案,請先新增,並加入上述上線狀態解決方案。

接下來,您將透過下列方法將目前狀態同步至 Cloud Firestore:

  1. 在本機上,將資料寫入離線裝置的 Cloud Firestore 快取,讓應用程式知道裝置處於離線狀態。
  2. 全域:使用 Cloud 函式,讓所有存取 Cloud Firestore 的其他裝置知道這個特定裝置已離線。

本教學課程建議的函式無法在用戶端應用程式中執行,必須部署至 Cloud Functions for Firebase,且需要 Firebase Admin SDK 的伺服器端邏輯。如需詳細指南,請參閱 Cloud Functions 說明文件。

正在更新 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 應用程式中的狀態仍不準確,因為「離線」狀態寫入作業僅限本機,且在連線恢復時不會同步。為解決這個問題,我們將使用 Cloud 函式監控 Realtime Database 中的 status/{uid} 路徑。當即時資料庫值變更時,該值會同步至 Cloud Firestore,確保所有使用者的狀態正確無誤。

Node.js

const functions = require('firebase-functions/v1');
const { initializeApp } = require('firebase-admin/app');
const { getFirestore } = require('firebase-admin/firestore');
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 = getFirestore();

// 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);
                // ...
            }
        });
    });

限制

使用即時資料庫為 Cloud Firestore 應用程式新增上線狀態,不僅可擴充且有效,但有一些限制:

  • 去抖動:監聽 Cloud Firestore 的即時變更時,這項解決方案可能會觸發多項變更。如果這些變更觸發的事件數量超出預期,請手動去抖動 Cloud Firestore 事件。
  • 連線能力:這項實作會評估與 Realtime Database 的連線能力,而非與 Cloud Firestore 的連線能力。如果各資料庫的連線狀態不一致,這項解決方案可能會回報錯誤的存在狀態。
  • Android:在 Android 上,即時資料庫閒置 60 秒後會與後端中斷連線。閒置是指沒有開啟的事件監聽器或待處理作業。為保持連線開啟,建議您在 .info/connected 以外的路徑中新增值事件監聽器。例如,您可以在每個工作階段開始時執行 FirebaseDatabase.getInstance().getReference((new Date()).toString()).keepSynced()。詳情請參閱「偵測連線狀態」。