在 JavaScript 中启用离线功能

即使网络连接暂时中断,Firebase 应用仍可正常工作。我们提供了多种工具,用于监测在线状态并将本地状态与服务器状态进行同步,本文对此进行了介绍。

管理在线状态

在实时应用中,通常需要检测客户端何时建立和断开连接。例如,当用户的客户端断开连接时,您可能需要将该用户标记为“离线”。

Firebase Database 客户端提供了一些简单的原语,在某个客户端与 Firebase Database 服务器断开连接时,可以使用这些原语向数据库写入数据。这些更新的完成情况与客户端是否正常断开连接无关,因此,即使连接突然中断或客户端崩溃,您仍可以依靠此类更新来清理数据。所有写入操作(包括设置、更新和移除)均可以在断开连接时执行。

在下面这个简单的示例中,我们使用 onDisconnect 原语在断开连接时写入数据:

Web 版本 9

import { getDatabase, ref, onDisconnect } from "firebase/database";

const db = getDatabase();
const presenceRef = ref(db, "disconnectmessage");
// Write a string when this client loses connection
onDisconnect(presenceRef).set("I disconnected!");

Web 版本 8

var presenceRef = firebase.database().ref("disconnectmessage");
// Write a string when this client loses connection
presenceRef.onDisconnect().set("I disconnected!");

onDisconnect 的工作方式

当您建立 onDisconnect() 操作后,该操作会在 Firebase Realtime Database 服务器上驻留。该服务器会检查安全性,确保用户可以执行所请求的写入事件,并在该操作无效时通知您的应用。然后,服务器会监控连接状况。如果连接超时,或 Realtime Database 客户端主动关闭连接,服务器会再一次检查安全性(以确保操作仍有效),然后触发事件。

您可以在应用中使用写入操作回调,以确保正确附加了 onDisconnect

Web 版本 9

onDisconnect(presenceRef).remove().catch((err) => {
  if (err) {
    console.error("could not establish onDisconnect event", err);
  }
});

Web 版本 8

presenceRef.onDisconnect().remove((err) => {
  if (err) {
    console.error("could not establish onDisconnect event", err);
  }
});

还可以调用 .cancel() 来取消 onDisconnect 事件:

Web 版本 9

const onDisconnectRef = onDisconnect(presenceRef);
onDisconnectRef.set("I disconnected");
// some time later when we change our minds
onDisconnectRef.cancel();

Web 版本 8

var onDisconnectRef = presenceRef.onDisconnect();
onDisconnectRef.set("I disconnected");
// some time later when we change our minds
onDisconnectRef.cancel();

检测连接状态

对于许多与在线状态相关的功能,让您的应用了解自己处于在线还是离线状态非常有用。Firebase Realtime Database 提供了 /.info/connected 这个特殊位置,每当 Firebase Realtime Database 客户端的连接状态发生变化时,该位置都会更新。示例如下:

Web 版本 9

import { getDatabase, ref, onValue } from "firebase/database";

const db = getDatabase();
const connectedRef = ref(db, ".info/connected");
onValue(connectedRef, (snap) => {
  if (snap.val() === true) {
    console.log("connected");
  } else {
    console.log("not connected");
  }
});

Web 版本 8

var connectedRef = firebase.database().ref(".info/connected");
connectedRef.on("value", (snap) => {
  if (snap.val() === true) {
    console.log("connected");
  } else {
    console.log("not connected");
  }
});

/.info/connected 布尔值不会在 Realtime Database 客户端之间同步,因为该值取决于客户端的状态。换句话说,一个客户端的 /.info/connected 值为 false,并不代表另一个客户端的这个值也是 false。

应对延迟

服务器时间戳

Firebase Realtime Database 服务器提供了一种机制,用于以数据形式插入服务器上生成的时间戳。这项功能与 onDisconnect 相结合,可让您轻松准确地记录 Realtime Database 客户端断开连接的时间:

Web 版本 9

import { getDatabase, ref, onDisconnect, serverTimestamp } from "firebase/database";

const db = getDatabase();
const userLastOnlineRef = ref(db, "users/joe/lastOnline");
onDisconnect(userLastOnlineRef).set(serverTimestamp());

Web 版本 8

var userLastOnlineRef = firebase.database().ref("users/joe/lastOnline");
userLastOnlineRef.onDisconnect().set(firebase.database.ServerValue.TIMESTAMP);

时钟偏差

虽然 firebase.database.ServerValue.TIMESTAMP 要准确得多,对大多数读写操作来说也更合适,但有时估算客户端相对于 Firebase Realtime Database 服务器的时钟偏差也很有用。您可以在 /.info/serverTimeOffset 这个位置附加回调函数来获取相应值(以毫秒为单位)。Firebase Realtime Database 客户端会将这个值与本地报告的时间(以毫秒为单位的纪元时间)相加,来估算服务器时间。请注意,这个时间差的准确性可能会受到网络延迟的影响,因此,它主要用于发现较大(> 1 秒)的时钟偏差。

Web 版本 9

import { getDatabase, ref, onValue } from "firebase/database";

const db = getDatabase();
const offsetRef = ref(db, ".info/serverTimeOffset");
onValue(offsetRef, (snap) => {
  const offset = snap.val();
  const estimatedServerTimeMs = new Date().getTime() + offset;
});

Web 版本 8

var offsetRef = firebase.database().ref(".info/serverTimeOffset");
offsetRef.on("value", (snap) => {
  var offset = snap.val();
  var estimatedServerTimeMs = new Date().getTime() + offset;
});

在线状态应用示例

通过将断开连接操作、连接状态监控与服务器时间戳相结合,您可以构建一个用户在线状态系统。在这个系统中,每位用户都可以在数据库中的某个位置存储数据,来表明其 Realtime Database 客户端是否在线。客户端会在处于在线状态时将这个位置设为 true,并在断开连接时将其设置为时间戳。这个时间戳即代表该用户最后的在线时间。

请注意,在将用户标记为在线之前,您的应用应将断开连接操作加入队列,以免在因客户端网络连接中断而无法将两个命令都发送到服务器的情况下,出现竞态条件。

下方是一个简单的用户在线状态系统:

Web 版本 9

import { getDatabase, ref, onValue, push, onDisconnect, set, serverTimestamp } from "firebase/database";

// Since I can connect from multiple devices or browser tabs, we store each connection instance separately
// any time that connectionsRef's value is null (i.e. has no children) I am offline
const db = getDatabase();
const myConnectionsRef = ref(db, 'users/joe/connections');

// stores the timestamp of my last disconnect (the last time I was seen online)
const lastOnlineRef = ref(db, 'users/joe/lastOnline');

const connectedRef = ref(db, '.info/connected');
onValue(connectedRef, (snap) => {
  if (snap.val() === true) {
    // We're connected (or reconnected)! Do anything here that should happen only if online (or on reconnect)
    const con = push(myConnectionsRef);

    // When I disconnect, remove this device
    onDisconnect(con).remove();

    // Add this device to my connections list
    // this value could contain info about the device or a timestamp too
    set(con, true);

    // When I disconnect, update the last time I was seen online
    onDisconnect(lastOnlineRef).set(serverTimestamp());
  }
});

Web 版本 8

// Since I can connect from multiple devices or browser tabs, we store each connection instance separately
// any time that connectionsRef's value is null (i.e. has no children) I am offline
var myConnectionsRef = firebase.database().ref('users/joe/connections');

// stores the timestamp of my last disconnect (the last time I was seen online)
var lastOnlineRef = firebase.database().ref('users/joe/lastOnline');

var connectedRef = firebase.database().ref('.info/connected');
connectedRef.on('value', (snap) => {
  if (snap.val() === true) {
    // We're connected (or reconnected)! Do anything here that should happen only if online (or on reconnect)
    var con = myConnectionsRef.push();

    // When I disconnect, remove this device
    con.onDisconnect().remove();

    // Add this device to my connections list
    // this value could contain info about the device or a timestamp too
    con.set(true);

    // When I disconnect, update the last time I was seen online
    lastOnlineRef.onDisconnect().set(firebase.database.ServerValue.TIMESTAMP);
  }
});