在 JavaScript 中啟用離線功能

即使應用程式暫時失去網路連線,Firebase 應用程式仍可運作。我們提供多種工具,可監控在場狀態,並將本機狀態與伺服器狀態同步處理,本文將介紹這些工具。

管理在家狀態

在即時應用程式中,偵測用戶端連線和中斷連線的時間通常很有用。舉例來說,您可能想在用戶端中斷連線時,將使用者標示為「離線」。

Firebase 資料庫用戶端提供簡單的基元,供您在用戶端與 Firebase 資料庫伺服器中斷連線時,用來寫入資料庫。無論用戶端是否正常中斷連線,都會發生這些更新,因此即使連線中斷或用戶端當機,您也可以依賴這些更新來清除資料。中斷連線後,您仍可執行所有寫入作業,包括設定、更新及移除。

以下是使用 onDisconnect 基本體在連線中斷時寫入資料的簡單範例:

Web

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

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

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

Web

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

您也可以呼叫 .cancel() 取消 onDisconnect 事件:

Web

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

Web

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

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

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

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

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

Web

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

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

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

範例 Presence 應用程式

結合中斷連線作業、連線狀態監控和伺服器時間戳記,即可建構使用者狀態系統。在這個系統中,每位使用者都會在資料庫位置儲存資料,指出Realtime Database用戶端是否連線。用戶端上線時會將這個位置設為 true,並在連線中斷時加上時間戳記。這個時間戳記表示指定使用者上次上線的時間。

請注意,應用程式應在使用者標示為上線前,將中斷連線作業排入佇列,以免在兩個指令都能傳送至伺服器前,用戶端的網路連線中斷,導致任何競爭情況。

以下是簡單的使用者狀態系統:

Web

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

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