在網路上讀取及寫入資料

(選用) 使用 Firebase 本機模擬器套件設計原型並進行測試

在討論應用程式如何讀取即時資料庫及寫入資料之前,我們會先介紹一組工具,讓您用來設計原型及測試即時資料庫功能:Firebase 本機模擬器套件。如果您正在嘗試不同的資料模型、最佳化安全性規則,或想找出最符合成本效益的方式與後端互動,那麼即使不部署即時服務,也能在本機工作。

即時資料庫模擬器屬於本機模擬器套件的一部分,可讓應用程式與模擬資料庫內容和設定互動,並選擇性地與模擬專案資源 (函式、其他資料庫和安全性規則) 互動。

使用即時資料庫模擬器只需完成幾個步驟:

  1. 將一行程式碼新增至應用程式的測試設定,即可與模擬器連線。
  2. 從本機專案目錄的根目錄中執行 firebase emulators:start
  3. 照常透過即時資料庫平台 SDK 或即時資料庫 REST API,從應用程式的原型程式碼發出呼叫。

歡迎查看詳細的即時資料庫和 Cloud Functions 逐步操作說明。建議您也參閱「本機模擬器套件簡介」一文。

取得資料庫參照

如要從資料庫讀取或寫入資料,您需要 firebase.database.Reference 的執行個體:

網頁模組 API

import { getDatabase } from "firebase/database";

const database = getDatabase();

網路命名空間 API

var database = firebase.database();

寫入資料

本文說明擷取資料的基本概念,以及如何排序及篩選 Firebase 資料。

將非同步事件監聽器附加至 firebase.database.Reference,即可擷取 Firebase 資料。監聽器會針對資料的初始狀態觸發一次,並在資料變更時再次觸發。

基本寫入作業

針對基本寫入作業,您可以使用 set() 將資料儲存至指定的參照,取代該路徑中的所有現有資料。舉例來說,社交網誌應用程式可能會新增具有 set() 的使用者,如下所示:

網頁模組 API

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

function writeUserData(userId, name, email, imageUrl) {
  const db = getDatabase();
  set(ref(db, 'users/' + userId), {
    username: name,
    email: email,
    profile_picture : imageUrl
  });
}

網路命名空間 API

function writeUserData(userId, name, email, imageUrl) {
  firebase.database().ref('users/' + userId).set({
    username: name,
    email: email,
    profile_picture : imageUrl
  });
}

使用 set() 會覆寫指定位置的資料,包括任何子節點。

讀取資料

監聽價值事件

如要在路徑中讀取資料及監聽變更,請使用 onValue() 來觀察事件。您可以使用此事件來讀取特定路徑的內容靜態快照,因為這些快照在事件發生時就存在。此方法會在連接事件監聽器時觸發一次,並在每次資料 (包括子項) 變更時再次觸發。事件回呼會傳遞一個快照,當中包含該位置的所有資料,包括子項資料。如果沒有資料,則當您在呼叫 exists()val() 時,系統會傳回快照falsenull

以下範例將示範社交網誌應用程式,如何從資料庫擷取文章的星號數:

網頁模組 API

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

const db = getDatabase();
const starCountRef = ref(db, 'posts/' + postId + '/starCount');
onValue(starCountRef, (snapshot) => {
  const data = snapshot.val();
  updateStarCount(postElement, data);
});

網路命名空間 API

var starCountRef = firebase.database().ref('posts/' + postId + '/starCount');
starCountRef.on('value', (snapshot) => {
  const data = snapshot.val();
  updateStarCount(postElement, data);
});

事件監聽器會收到 snapshot,其中包含事件發生時資料庫中指定的位置資料。您可以使用 val() 方法在 snapshot 中擷取資料。

讀取資料一次

使用 get() 讀取資料一次

無論應用程式處於線上或離線狀態,SDK 都能用來管理與資料庫伺服器的互動。

一般來說,您應使用上述的值事件技術來讀取資料,以便從後端取得資料更新的通知。事件監聽器技術可減少用量和費用,並經過最佳化調整,可在使用者上線及離線時提供最佳體驗。

如果您只需要資料一次,可以使用 get() 取得資料庫中的資料快照。如果因任何原因 get() 無法傳回伺服器值,用戶端會探測本機儲存空間快取,如果還是找不到該值,就會傳回錯誤。

不必要的使用 get() 可能會增加頻寬用量,並導致效能遺失,可以如上所示使用即時事件監聽器來避免這種情況。

網頁模組 API

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

const dbRef = ref(getDatabase());
get(child(dbRef, `users/${userId}`)).then((snapshot) => {
  if (snapshot.exists()) {
    console.log(snapshot.val());
  } else {
    console.log("No data available");
  }
}).catch((error) => {
  console.error(error);
});

網路命名空間 API

const dbRef = firebase.database().ref();
dbRef.child("users").child(userId).get().then((snapshot) => {
  if (snapshot.exists()) {
    console.log(snapshot.val());
  } else {
    console.log("No data available");
  }
}).catch((error) => {
  console.error(error);
});

使用觀察器讀取資料一次

在某些情況下,您可能會希望立即傳回本機快取中的值,而不是檢查伺服器上的更新值。在這類情況下,您可以使用 once() 立即從本機磁碟快取取得資料。

如果資料只需載入一次、預期不會經常變更或需要主動監聽,這項功能就能派上用場。舉例來說,上述範例中的網誌應用程式會使用這個方法,在使用者開始撰寫新文章時載入個人資料:

網頁模組 API

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

const db = getDatabase();
const auth = getAuth();

const userId = auth.currentUser.uid;
return onValue(ref(db, '/users/' + userId), (snapshot) => {
  const username = (snapshot.val() && snapshot.val().username) || 'Anonymous';
  // ...
}, {
  onlyOnce: true
});

網路命名空間 API

var userId = firebase.auth().currentUser.uid;
return firebase.database().ref('/users/' + userId).once('value').then((snapshot) => {
  var username = (snapshot.val() && snapshot.val().username) || 'Anonymous';
  // ...
});

更新或刪除資料

更新特定欄位

如要同時寫入節點的特定子項,而不覆寫其他子節點,請使用 update() 方法。

呼叫 update() 時,您可以指定索引鍵路徑,更新較低層級的子項值。如果資料儲存在多個位置以利擴充,您可以使用資料擴散傳遞功能更新該資料的所有執行個體。

舉例來說,社交網誌應用程式可能會建立貼文,同時使用以下程式碼,將貼文更新為最近的活動動態消息和發布使用者的活動動態消息:

網頁模組 API

import { getDatabase, ref, child, push, update } from "firebase/database";

function writeNewPost(uid, username, picture, title, body) {
  const db = getDatabase();

  // A post entry.
  const postData = {
    author: username,
    uid: uid,
    body: body,
    title: title,
    starCount: 0,
    authorPic: picture
  };

  // Get a key for a new Post.
  const newPostKey = push(child(ref(db), 'posts')).key;

  // Write the new post's data simultaneously in the posts list and the user's post list.
  const updates = {};
  updates['/posts/' + newPostKey] = postData;
  updates['/user-posts/' + uid + '/' + newPostKey] = postData;

  return update(ref(db), updates);
}

網路命名空間 API

function writeNewPost(uid, username, picture, title, body) {
  // A post entry.
  var postData = {
    author: username,
    uid: uid,
    body: body,
    title: title,
    starCount: 0,
    authorPic: picture
  };

  // Get a key for a new Post.
  var newPostKey = firebase.database().ref().child('posts').push().key;

  // Write the new post's data simultaneously in the posts list and the user's post list.
  var updates = {};
  updates['/posts/' + newPostKey] = postData;
  updates['/user-posts/' + uid + '/' + newPostKey] = postData;

  return firebase.database().ref().update(updates);
}

此範例使用 push() 在節點中建立貼文,其中包含 /posts/$postid 中所有使用者的貼文,然後同時擷取金鑰。這樣就能在使用者的貼文中,用此金鑰在 /user-posts/$userid/$postid 建立第二個項目。

您可以使用這些路徑,只呼叫 update() 就能同時更新 JSON 樹狀結構中的多個位置,例如這個範例如何在兩個位置建立新貼文。以這種方式進行的同步更新是不可分割的:所有更新成功或所有更新都會失敗。

新增完成回呼

如要瞭解資料修訂時間,可以新增完成回呼。set()update() 會採用選用的完成回呼,當系統將寫入作業提交至資料庫時,就會呼叫該回呼。如果呼叫失敗,系統會傳送回呼錯誤物件,指出失敗的原因。

網頁模組 API

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

const db = getDatabase();
set(ref(db, 'users/' + userId), {
  username: name,
  email: email,
  profile_picture : imageUrl
})
.then(() => {
  // Data saved successfully!
})
.catch((error) => {
  // The write failed...
});

網路命名空間 API

firebase.database().ref('users/' + userId).set({
  username: name,
  email: email,
  profile_picture : imageUrl
}, (error) => {
  if (error) {
    // The write failed...
  } else {
    // Data saved successfully!
  }
});

刪除資料

刪除資料最簡單的方法是對該資料位置的參照呼叫 remove()

您也可以指定 null 為其他寫入作業 (例如 set()update()) 的值來刪除。您可以將這項技術與 update() 搭配使用,在單一 API 呼叫中刪除多個子項。

接收 Promise

如要瞭解資料何時提交至 Firebase 即時資料庫伺服器,可以使用 Promiseset()update() 都可以傳回 Promise,方便您知道何時將寫入作業提交至資料庫。

卸離事件監聽器

如要移除回呼,請對 Firebase 資料庫參考資料呼叫 off() 方法。

將單一事件監聽器做為參數傳遞至 off() 即可移除。如果對沒有引數的位置呼叫 off(),系統會移除該位置的所有事件監聽器。

對父項事件監聽器呼叫 off() 不會自動移除在其子節點上註冊的事件監聽器;此外,您也必須在任何子項事件監聽器上呼叫 off(),才能移除回呼。

將資料儲存為交易

處理可能會因為並行修改而損毀的資料 (例如漸進式計數器) 時,您可以使用交易作業。您可以將更新函式與選用的完成回呼提供給這項作業。update 函式會將資料目前狀態當做引數,並傳回您要寫入的新所需狀態。如果其他用戶端在成功寫入新值之前寫入位置,系統會以新的目前值再次呼叫更新函式,並重試寫入。

舉例來說,在社交網誌應用程式範例中,您可以讓使用者為文章加上星號或移除星號,也可以追蹤貼文獲得的星星數量,如下所示:

網頁模組 API

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

function toggleStar(uid) {
  const db = getDatabase();
  const postRef = ref(db, '/posts/foo-bar-123');

  runTransaction(postRef, (post) => {
    if (post) {
      if (post.stars && post.stars[uid]) {
        post.starCount--;
        post.stars[uid] = null;
      } else {
        post.starCount++;
        if (!post.stars) {
          post.stars = {};
        }
        post.stars[uid] = true;
      }
    }
    return post;
  });
}

網路命名空間 API

function toggleStar(postRef, uid) {
  postRef.transaction((post) => {
    if (post) {
      if (post.stars && post.stars[uid]) {
        post.starCount--;
        post.stars[uid] = null;
      } else {
        post.starCount++;
        if (!post.stars) {
          post.stars = {};
        }
        post.stars[uid] = true;
      }
    }
    return post;
  });
}

如果多位使用者同時為同一則貼文加上星號,或用戶端取得過時的資料,使用交易可避免星號計數有誤。如果交易遭拒,伺服器會將目前的值傳回用戶端,用戶端會使用更新後的值再次執行交易。系統會重複執行,直到接受交易或您取消交易為止。

整體伺服器端增量

在上述用途中,我們要將兩個值寫入資料庫:為貼文加上星號/移除星號的使用者 ID,以及增加的星號數量。如果我們知道使用者已為貼文加上星號,則可使用不可分割的遞增作業,而非交易。

網頁模組 API

function addStar(uid, key) {
  import { getDatabase, increment, ref, update } from "firebase/database";
  const dbRef = ref(getDatabase());

  const updates = {};
  updates[`posts/${key}/stars/${uid}`] = true;
  updates[`posts/${key}/starCount`] = increment(1);
  updates[`user-posts/${key}/stars/${uid}`] = true;
  updates[`user-posts/${key}/starCount`] = increment(1);
  update(dbRef, updates);
}

網路命名空間 API

function addStar(uid, key) {
  const updates = {};
  updates[`posts/${key}/stars/${uid}`] = true;
  updates[`posts/${key}/starCount`] = firebase.database.ServerValue.increment(1);
  updates[`user-posts/${key}/stars/${uid}`] = true;
  updates[`user-posts/${key}/starCount`] = firebase.database.ServerValue.increment(1);
  firebase.database().ref().update(updates);
}

此程式碼不會使用交易作業,所以在有衝突的更新時,不會自動重新執行。不過,由於增量作業會直接在資料庫伺服器上進行,因此不會發生衝突。

如要偵測並拒絕應用程式特定衝突 (例如使用者為先前已加上星號的貼文加上星號),則必須針對該用途編寫自訂安全性規則。

離線使用資料

如果用戶端的網路連線中斷,您的應用程式會繼續正常運作。

每個連接至 Firebase 資料庫的用戶端,都會維護自己的任何有效資料的內部版本。寫入資料時,系統會先將資料寫入這個本機版本。接著,Firebase 用戶端會「盡可能」將這些資料與遠端資料庫伺服器和其他用戶端同步處理。

因此,在將任何資料寫入伺服器之前,所有寫入資料庫的作業會立即觸發本機事件。換句話說,無論網路延遲或連線狀況,應用程式都會持續回應。

重新建立連線後,應用程式會收到一組適當的事件,讓用戶端與目前的伺服器狀態同步,而不必編寫任何自訂程式碼。

我們會在進一步瞭解線上和離線功能中,進一步說明離線行為。

後續步驟