在 Cloud Firestore 中构建在线状态系统

根据您正在构建的应用的类型,您可能会发现如果能够检测哪些用户或设备活跃在线上(也称为检测“在线状态”),会带来很多好处。

例如,如果您正在构建一个类似社交网络的应用或正在部署一组 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 中的运行方式。

Web

// 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 Functions 函数,以便访问 Cloud Firestore 的其他所有设备都知道此特定设备处于离线状态。

本教程中推荐的函数无法在客户端应用中运行。它们必须部署到 Cloud Functions for Firebase,并且需要 Firebase Admin SDK 中的服务器端逻辑。如需查看详细指导,请参阅 Cloud Functions 文档

更新 Cloud Firestore 的本地缓存

我们来看看解决第一个问题所需的更改 - 更新 Cloud Firestore 的本地缓存。

Web

// ...
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} 文档,并使用数据更改界面以反映连接状态。

Web

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

在全局更新 Cloud Firestore

虽然我们的应用正确地向“自己”报告了在线状态,但是在其他 Cloud Firestore 应用中,这个状态还并不准确,因为“离线”状态写入操作仅在本地执行,并不会在连接恢复时进行同步。为了解决这个问题,我们将使用一个 Cloud Functions 函数来监控 Realtime Database 中的 status/{uid} 路径。当 Realtime Database 中的值发生更改时,该值将同步到 Cloud Firestore,以保证所有用户的状态都正确无误。

Node.js

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 运行的完整的在线状态系统。下面是一个使用 where() 查询监控任何上线或下线用户的示例。

Web

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 事件进行去抖动处理。
  • 连接 - 此实现方案衡量的是与 Realtime Database(而不是 Cloud Firestore)的连接状态。如果与每个数据库的连接状态均不同,此解决方案可能会报告不正确的在线状态。
  • Android - 在 Android 上,不活跃状态持续 60 秒后,Realtime Database 会断开与后端的连接。不活跃意味着没有打开的监听器或待处理操作。要使连接保持开启状态,我们建议您除了 .info/connected 之外,还应该为另一个路径添加值事件监听器。例如,您可以在每次会话开始时执行 FirebaseDatabase.getInstance().getReference((new Date()).toString()).keepSynced()。如需了解详情,请参阅检测连接状态