使用 Firebase 模擬器套件進行本機開發

1. 開始之前

Cloud Firestore 和 Cloud Functions 等無伺服器後端工具非常容易使用,但可能很難測試。 Firebase 本機模擬器套件可讓您在開發電腦上執行這些服務的本機版本,以便您可以快速安全地開發應用程式。

先決條件

  • 簡單的編輯器,例如 Visual Studio Code、Atom 或 Sublime Text
  • Node.js 10.0.0 或更高版本(要安裝 Node.js,請使用 nvm ,要檢查您的版本,請執行node --version
  • Java 7 或更高版本(要安裝 Java,請使用這些說明,要檢查您的版本,請執行java -version

你會做什麼

在此 Codelab 中,您將運行並偵錯一個由多個 Firebase 服務提供支援的簡單線上購物應用程式:

  • Cloud Firestore:具有即時功能的全球可擴充、無伺服器、NoSQL 資料庫。
  • Cloud Functions :執行以回應事件或 HTTP 請求的無伺服器後端程式碼。
  • Firebase 驗證:與其他 Firebase 產品整合的託管驗證服務。
  • Firebase Hosting :快速、安全的網頁應用程式託管。

您將應用程式連接到模擬器套件以啟用本機開發。

2589e2f95b74fa88.png

您還將學習如何:

  • 如何將您的應用程式連接到模擬器套件以及如何連接各種模擬器。
  • Firebase 安全性規則的工作原理以及如何針對本機模擬器測試 Firestore 安全性規則。
  • 如何編寫由 Firestore 事件觸發的 Firebase 函數,以及如何撰寫針對模擬器套件執行的整合測試。

2. 設定

取得原始碼

在此 Codelab 中,您將從接近完整的 Fire Store 範例版本開始,因此您需要做的第一件事是克隆原始程式碼:

$ git clone https://github.com/firebase/emulators-codelab.git

然後進入 Codelab 目錄,您將在其中完成本 Codelab 的其餘部分:

$ cd emulators-codelab/codelab-initial-state

現在,安裝依賴項以便您可以運行程式碼。如果您的網路連線速度較慢,這可能需要一兩分鐘:

# Move into the functions directory
$ cd functions

# Install dependencies
$ npm install

# Move back into the previous directory
$ cd ../

取得 Firebase CLI

模擬器套件是 Firebase CLI(命令列介面)的一部分,可以使用以下命令將其安裝在您的電腦上:

$ npm install -g firebase-tools

接下來,確認您擁有最新版本的 CLI。此 Codelab 應適用於 9.0.0 或更高版本,但更高版本包含更多錯誤修復。

$ firebase --version
9.6.0

連接到您的 Firebase 項目

如果您沒有 Firebase 項目,請在Firebase 控制台中建立一個新的 Firebase 項目。記下您選擇的項目 ID,稍後您將需要它。

現在我們需要將此程式碼連接到您的 Firebase 專案。首先執行以下命令登入Firebase CLI:

$ firebase login

接下來執行以下命令來建立專案別名。將$YOUR_PROJECT_ID替換為您的 Firebase 專案的 ID。

$ firebase use $YOUR_PROJECT_ID

現在您已準備好運行該應用程式了!

3.運行模擬器

在本部分中,您將在本地運行該應用程式。這意味著是時候啟動模擬器套件了。

啟動模擬器

在 Codelab 來源目錄中,執行以下命令來啟動模擬器:

$ firebase emulators:start --import=./seed

您應該看到如下輸出:

$ firebase emulators:start --import=./seed
i  emulators: Starting emulators: auth, functions, firestore, hosting
⚠  functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: database, pubsub
i  firestore: Importing data from /Users/samstern/Projects/emulators-codelab/codelab-initial-state/seed/firestore_export/firestore_export.overall_export_metadata
i  firestore: Firestore Emulator logging to firestore-debug.log
i  hosting: Serving hosting files from: public
✔  hosting: Local server: http://127.0.0.1:5000
i  ui: Emulator UI logging to ui-debug.log
i  functions: Watching "/Users/samstern/Projects/emulators-codelab/codelab-initial-state/functions" for Cloud Functions...
✔  functions[calculateCart]: firestore function initialized.

┌─────────────────────────────────────────────────────────────┐
│ ✔  All emulators ready! It is now safe to connect your app. │
│ i  View Emulator UI at http://127.0.0.1:4000                │
└─────────────────────────────────────────────────────────────┘

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ 127.0.0.1:9099 │ http://127.0.0.1:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting        │ 127.0.0.1:5000 │ n/a                             │
└────────────────┴────────────────┴─────────────────────────────────┘
  Emulator Hub running at 127.0.0.1:4400
  Other reserved ports: 4500

Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.

一旦您看到“所有模擬器已啟動”訊息,該應用程式就可以使用了。

將 Web 應用程式連接到模擬器

根據日誌中的表,我們可以看到 Cloud Firestore 模擬器正在偵聽連接埠8080 ,而身份驗證模擬器正在偵聽連接埠9099

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ 127.0.0.1:9099 │ http://127.0.0.1:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting        │ 127.0.0.1:5000 │ n/a                             │
└────────────────┴────────────────┴─────────────────────────────────┘

讓我們將前端程式碼連接到模擬器,而不是生產環境。開啟public/js/homepage.js檔案並找到onDocumentReady函數。我們可以看到程式碼存取了標準的 Firestore 和 Auth 實例:

公共/js/homepage.js

  const auth = firebaseApp.auth();
  const db = firebaseApp.firestore();

讓我們更新dbauth物件以指向本機模擬器:

公共/js/homepage.js

  const auth = firebaseApp.auth();
  const db = firebaseApp.firestore();

  // ADD THESE LINES
  if (location.hostname === "127.0.0.1") {
    console.log("127.0.0.1 detected!");
    auth.useEmulator("http://127.0.0.1:9099");
    db.useEmulator("127.0.0.1", 8080);
  }

現在,當應用程式在本機電腦(由託管模擬器提供服務)上執行時,Firestore 用戶端也會指向本機模擬器而不是生產資料庫。

開啟模擬器UI

在網頁瀏覽器中,導覽至http://127.0.0.1:4000/ 。您應該會看到模擬器套件 UI。

模擬器 UI 主螢幕

按一下可查看 Firestore 模擬器的 UI。由於使用--import標誌導入的數據, items集合已包含數據。

4ef88d0148405d36.png

4. 運行應用程式

打開應用程式

在您的 Web 瀏覽器中,導航到http://127.0.0.1:5000 ,您應該會看到 Fire Store 在您的電腦上本地運行!

939f87946bac2ee4.png

使用應用程式

在主頁上選擇一個商品,然後按一下「加入購物車」 。不幸的是,您將遇到以下錯誤:

a11bd59933a8e885.png

讓我們修復這個錯誤吧!因為一切都在模擬器中運行,所以我們可以進行實驗而不用擔心影響真實數據。

5. 調試應用程式

找到錯誤

好吧,讓我們看看 Chrome 開發者控制台。按下Control+Shift+J (Windows、Linux、Chrome 作業系統)或Command+Option+J (Mac) 可在控制台上查看錯誤:

74c45df55291dab1.png

addToCart方法似乎有一些錯誤,讓我們來看看。我們在該方法中在哪裡嘗試存取稱為uid的東西,為什麼它會是null ?現在該方法在public/js/homepage.js中如下所示:

公共/js/homepage.js

  addToCart(id, itemData) {
    console.log("addToCart", id, JSON.stringify(itemData));
    return this.db
      .collection("carts")
      .doc(this.auth.currentUser.uid)
      .collection("items")
      .doc(id)
      .set(itemData);
  }

啊哈!我們尚未登入該應用程式。根據Firebase 驗證文檔,當我們未登入時, auth.currentUsernull 。讓我們為此添加一個檢查:

公共/js/homepage.js

  addToCart(id, itemData) {
    // ADD THESE LINES
    if (this.auth.currentUser === null) {
      this.showError("You must be signed in!");
      return;
    }

    // ...
  }

測試應用程式

現在,重新整理頁面,然後按一下「加入購物車」 。這次你應該得到一個更好的錯誤:

c65f6c05588133f7.png

但是,如果您點擊上方工具列中的「登入」 ,然後再次點擊「加入購物車」 ,您將看到購物車已更新。

然而,這些數字看起來根本不正確:

239f26f02f959eef.png

別擔心,我們很快就會修復這個錯誤。首先,讓我們深入了解將商品添加到購物車時實際發生的情況。

6. 本地函數觸發器

點擊「加入購物車」將啟動涉及多個模擬器的一系列事件。在 Firebase CLI 日誌中,將商品加入購物車後,您應該會看到類似以下訊息的內容:

i  functions: Beginning execution of "calculateCart"
i  functions: Finished "calculateCart" in ~1s

發生了四個關鍵事件來產生這些日誌和您觀察到的 UI 更新:

68c9323f2ad10f7a.png

1) Firestore 寫入 - 用戶端

新文件已新增至 Firestore 集合/carts/{cartId}/items/{itemId}/中。您可以在public/js/homepage.js內的addToCart函數中看到以下程式碼:

公共/js/homepage.js

  addToCart(id, itemData) {
    // ...
    console.log("addToCart", id, JSON.stringify(itemData));
    return this.db
      .collection("carts")
      .doc(this.auth.currentUser.uid)
      .collection("items")
      .doc(id)
      .set(itemData);
  }

2)雲函數觸發

雲端函數calculateCart透過使用onWrite觸發器監聽購物車專案發生的任何寫入事件(建立、更新或刪除),您可以在functions/index.js中看到:

函數/index.js

exports.calculateCart = functions.firestore
    .document("carts/{cartId}/items/{itemId}")
    .onWrite(async (change, context) => {
      try {
        let totalPrice = 125.98;
        let itemCount = 8;

        const cartRef = db.collection("carts").doc(context.params.cartId);

        await cartRef.update({
          totalPrice,
          itemCount
        });
      } catch(err) {
      }
    }
);

3) Firestore 寫入 - 管理者

calculateCart函數讀取購物車中的所有商品,並將總數量和價格相加,然後用新的總計更新「購物車」文件(請參閱上面的cartRef.update(...) )。

4) Firestore讀取-客戶端

訂閱 Web 前端以接收購物車變更的更新。在 Cloud Function 寫入新總計並更新 UI 後,它會獲得即時更新,如public/js/homepage.js中所示:

公共/js/homepage.js

this.cartUnsub = cartRef.onSnapshot(cart => {
   // The cart document was changed, update the UI
   // ...
});

回顧

幹得好!您只需設定一個完全本地的應用程序,該應用程式使用三個不同的 Firebase 模擬器進行完全本地測試。

db82eef1706c9058.gif

但等等,還有更多!在下一節中,您將學習:

  • 如何撰寫使用 Firebase 模擬器的單元測試。
  • 如何使用 Firebase 模擬器來偵錯您的安全規則。

7. 建立適合您的應用程式的安全規則

我們的網路應用程式讀取和寫入數據,但到目前為止我們根本不擔心安全性。 Cloud Firestore 使用稱為「安全規則」的系統來聲明誰有權讀取和寫入資料。模擬器套件是對這些規則進行原型設計的好方法。

在編輯器中,開啟檔案emulators-codelab/codelab-initial-state/firestore.rules 。您會看到我們的規則包含三個主要部分:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // User's cart metadata
    match /carts/{cartID} {
      // TODO: Change these! Anyone can read or write.
      allow read, write: if true;
    }

    // Items inside the user's cart
    match /carts/{cartID}/items/{itemID} {
      // TODO: Change these! Anyone can read or write.
      allow read, write: if true;
    }

    // All items available in the store. Users can read
    // items but never write them.
    match /items/{itemID} {
      allow read: if true;
    }
  }
}

現在任何人都可以在我們的資料庫中讀取和寫入資料!我們希望確保只有有效的操作才能通過,並且不會洩露任何敏感資訊。

在此 Codelab 中,遵循最小權限原則,我們將鎖定所有文件並逐漸添加存取權限,直到所有使用者都擁有他們需要的所有存取權限,但不會更多。讓我們透過將條件設為false來更新前兩個規則以拒絕存取:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // User's cart metadata
    match /carts/{cartID} {
      // UPDATE THIS LINE
      allow read, write: if false;
    }

    // Items inside the user's cart
    match /carts/{cartID}/items/{itemID} {
      // UPDATE THIS LINE
      allow read, write: if false;
    }

    // All items available in the store. Users can read
    // items but never write them.
    match /items/{itemID} {
      allow read: if true;
    }
  }
}

8. 運行模擬器和測試

啟動模擬器

在命令列上,確保您位於emulators-codelab/codelab-initial-state/中。您可能仍在執行前面步驟中的模擬器。如果沒有,請再次啟動模擬器:

$ firebase emulators:start --import=./seed

模擬器運行後,您可以在本地針對它們執行測試。

運行測試

emulators-codelab/codelab-initial-state/目錄中新終端選項卡的命令列上

首先進入函數目錄(我們將在 Codelab 的剩餘部分中留在這裡):

$ cd functions

現在在函數目錄中執行摩卡測試,然後捲動到輸出的頂部:

# Run the tests
$ npm test

> functions@ test .../emulators-codelab/codelab-initial-state/functions
> mocha

  shopping carts
    1) can be created and updated by the cart owner
    2) can be read only by the cart owner

  shopping cart items
    3) can be read only by the cart owner
    4) can be added only by the cart owner

  adding an item to the cart recalculates the cart total. 
    - should sum the cost of their items


  0 passing (364ms)
  1 pending
  4 failing

現在我們有四個失敗。當您建立規則檔案時,您可以透過觀察更多測試的通過情況來衡量進度。

9. 安全的購物車訪問

前兩個失敗是“購物車”測試,它測試:

  • 用戶只能創建和更新自己的購物車
  • 用戶只能讀取自己的購物車

函數/test.js

  it('can be created and updated by the cart owner', async () => {
    // Alice can create her own cart
    await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").set({
      ownerUID: "alice",
      total: 0
    }));

    // Bob can't create Alice's cart
    await firebase.assertFails(bobDb.doc("carts/alicesCart").set({
      ownerUID: "alice",
      total: 0
    }));

    // Alice can update her own cart with a new total
    await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").update({
      total: 1
    }));

    // Bob can't update Alice's cart with a new total
    await firebase.assertFails(bobDb.doc("carts/alicesCart").update({
      total: 1
    }));
  });

  it("can be read only by the cart owner", async () => {
    // Setup: Create Alice's cart as admin
    await admin.doc("carts/alicesCart").set({
      ownerUID: "alice",
      total: 0
    });

    // Alice can read her own cart
    await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").get());

    // Bob can't read Alice's cart
    await firebase.assertFails(bobDb.doc("carts/alicesCart").get());
  });

讓我們讓這些測試通過。在編輯器中,開啟安全規則檔案firestore.rules ,並更新match /carts/{cartID}中的語句:

firestore.規則

rules_version = '2';
service cloud.firestore {
    // UPDATE THESE LINES
    match /carts/{cartID} {
      allow create: if request.auth.uid == request.resource.data.ownerUID;
      allow read, update, delete: if request.auth.uid == resource.data.ownerUID;
    }

    // ...
  }
}

這些規則現在只允許購物車所有者進行讀寫存取。

為了驗證傳入資料和使用者身份驗證,我們使用每個規則上下文中可用的兩個物件:

10. 測試購物車訪問

每當儲存firestore.rules時,模擬器套件都會自動更新規則。您可以透過在執行模擬器的標籤中查看訊息Rules updated來確認模擬器已更新規則:

5680da418b420226.png

重新運行測試,並檢查前兩個測試現在是否通過:

$ npm test

> functions@ test .../emulators-codelab/codelab-initial-state/functions
> mocha

  shopping carts
    ✓ can be created and updated by the cart owner (195ms)
    ✓ can be read only by the cart owner (136ms)

  shopping cart items
    1) can be read only by the cart owner
    2) can be added only by the cart owner

  adding an item to the cart recalculates the cart total. 
    - should sum the cost of their items

  2 passing (482ms)
  1 pending
  2 failing

好工作!您現在已獲得對購物車的存取權限。讓我們繼續進行下一個失敗的測試。

11.檢查UI中的「加入購物車」流程

目前,儘管購物車所有者可以讀取和寫入購物車,但他們無法讀取或寫入購物車中的單一商品。這是因為雖然所有者有權訪問購物車文檔,但他們無權訪問購物車的items 子集合

對使用者來說,這是一種破碎的狀態。

回到在http://127.0.0.1:5000,並嘗試將一些東西添加到您的購物車。您會收到Permission Denied錯誤,可以從偵錯控制台看到該錯誤,因為我們尚未授予使用者對items子集合中建立的文件的存取權。

12.允許訪問購物車物品

這兩個測試確認用戶只能將商品添加到自己的購物車或從自己的購物車中讀取商品:

  it("can be read only by the cart owner", async () => {
    // Alice can read items in her own cart
    await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart/items/milk").get());

    // Bob can't read items in alice's cart
    await firebase.assertFails(bobDb.doc("carts/alicesCart/items/milk").get())
  });

  it("can be added only by the cart owner",  async () => {
    // Alice can add an item to her own cart
    await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart/items/lemon").set({
      name: "lemon",
      price: 0.99
    }));

    // Bob can't add an item to alice's cart
    await firebase.assertFails(bobDb.doc("carts/alicesCart/items/lemon").set({
      name: "lemon",
      price: 0.99
    }));
  });

因此,我們可以編寫一條規則,如果目前使用者與購物車文件上的ownerUID 具有相同的UID,則允許存取。由於不需要為create, update, delete指定不同的規則,因此您可以使用write規則,該規則適用於所有修改資料的請求。

更新 items 子集合中文檔的規則。條件中的get是從 Firestore 讀取一個值 - 在本例中是購物車文檔上的ownerUID

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ...

    // UPDATE THESE LINES
    match /carts/{cartID}/items/{itemID} {
      allow read, write: if get(/databases/$(database)/documents/carts/$(cartID)).data.ownerUID == request.auth.uid;
    }

    // ...
  }
}

13.測試購物車物品訪問

現在我們可以重新運行測試。捲動到輸出頂部並檢查是否通過了更多測試:

$ npm test

> functions@ test .../emulators-codelab/codelab-initial-state/functions
> mocha

  shopping carts
    ✓ can be created and updated by the cart owner (195ms)
    ✓ can be read only by the cart owner (136ms)

  shopping cart items
    ✓ can be read only by the cart owner (111ms)
    ✓ can be added only by the cart owner


  adding an item to the cart recalculates the cart total. 
    - should sum the cost of their items


  4 passing (401ms)
  1 pending

好的!現在我們所有的測試都通過了。我們有一項待定測試,但我們將通過幾個步驟完成該測試。

14.再次檢查「加入購物車」流程

返回 Web 前端 ( http://127.0.0.1:5000 ) 並將商品加入購物車。這是確認我們的測試和規則與客戶所需的功能相符的重要步驟。 (請記住,上次我們嘗試 UI 用戶無法將商品添加到購物車!)

69ad26cee520bf24.png

儲存firestore.rules時,用戶端會自動重新載入規則。因此,嘗試將一些東西添加到購物車中。

回顧

幹得好!您剛剛提高了應用程式的安全性,這是準備生產的重要一步!如果這是一個生產應用程序,我們可以將這些測試添加到我們的持續整合管道中。這將使我們充滿信心,即使其他人正在修改規則,我們的購物車資料也將具有這些存取控制。

ba5440b193e75967.gif

但等等,還有更多!

如果您繼續,您將學到:

  • 如何寫出由 Firestore 事件觸發的函數
  • 如何建立跨多個模擬器工作的測試

15. 設定 Cloud Functions 測試

到目前為止,我們專注於 Web 應用程式的前端和 Firestore 安全性規則。但此應用程式也使用 Cloud Functions 來使用戶的購物車保持最新狀態,因此我們也想測試該程式碼。

模擬器套件讓測試 Cloud Functions 變得非常容易,甚至是使用 Cloud Firestore 和其他服務的功能。

在編輯器中,開啟emulators-codelab/codelab-initial-state/functions/test.js檔案並捲動到檔案中的最後一個測試。現在,它被標記為待處理:

//  REMOVE .skip FROM THIS LINE
describe.skip("adding an item to the cart recalculates the cart total. ", () => {
  // ...

  it("should sum the cost of their items", async () => {
    ...
  });
});

要啟用測試,請刪除.skip ,因此它看起來像這樣:

describe("adding an item to the cart recalculates the cart total. ", () => {
  // ...

  it("should sum the cost of their items", async () => {
    ...
  });
});

接下來,找到檔案頂部的REAL_FIREBASE_PROJECT_ID變數並將其變更為您真實的 Firebase 項目 ID:

// CHANGE THIS LINE
const REAL_FIREBASE_PROJECT_ID = "changeme";

如果您忘記了專案 ID,可以在 Firebase 控制台的專案設定中找到您的 Firebase 專案 ID:

d6d0429b700d2b21.png

16. 演練功能測試

由於此測試驗證 Cloud Firestore 和 Cloud Functions 之間的交互,因此它比先前的 Codelab 中的測試涉及更多設定。讓我們完成這個測試並了解它的期望。

創建購物車

Cloud Functions 在受信任的伺服器環境中執行,並且可以使用 Admin SDK 使用的服務帳戶驗證。首先,使用initializeAdminApp而不是initializeApp初始化應用程式。然後,為我們將添加商品的購物車建立一個DocumentReference並初始化購物車:

it("should sum the cost of their items", async () => {
    const db = firebase
        .initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
        .firestore();

    // Setup: Initialize cart
    const aliceCartRef = db.doc("carts/alice")
    await aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });

    ...
  });

觸發功能

然後,將文件新增至購物車文件的items子集合以觸發該功能。新增兩項以確保您正在測試函數中發生的新增功能。

it("should sum the cost of their items", async () => {
    const db = firebase
        .initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
        .firestore();

    // Setup: Initialize cart
    const aliceCartRef = db.doc("carts/alice")
    await aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });

    //  Trigger calculateCart by adding items to the cart
    const aliceItemsRef = aliceCartRef.collection("items");
    await aliceItemsRef.doc("doc1").set({name: "nectarine", price: 2.99});
    await aliceItemsRef.doc("doc2").set({ name: "grapefruit", price: 6.99 });

    ...
    });
  });

設定測驗期望

使用onSnapshot()為購物車文件上的任何變更註冊偵聽器。 onSnapshot()傳回一個函數,您可以呼叫該函數來取消註冊偵聽器。

對於此測試,新增兩項總計成本為 9.98 美元的項目。然後,檢查購物車是否有預期的itemCounttotalPrice 。如果是這樣,那麼函數就完成了它的工作。

it("should sum the cost of their items", (done) => {
    const db = firebase
        .initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
        .firestore();

    // Setup: Initialize cart
    const aliceCartRef = db.doc("carts/alice")
    aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });

    //  Trigger calculateCart by adding items to the cart
    const aliceItemsRef = aliceCartRef.collection("items");
    aliceItemsRef.doc("doc1").set({name: "nectarine", price: 2.99});
    aliceItemsRef.doc("doc2").set({ name: "grapefruit", price: 6.99 });
    
    // Listen for every update to the cart. Every time an item is added to
    // the cart's subcollection of items, the function updates `totalPrice`
    // and `itemCount` attributes on the cart.
    // Returns a function that can be called to unsubscribe the listener.
    await new Promise((resolve) => {
      const unsubscribe = aliceCartRef.onSnapshot(snap => {
        // If the function worked, these will be cart's final attributes.
        const expectedCount = 2;
        const expectedTotal = 9.98;
  
        // When the `itemCount`and `totalPrice` match the expectations for the
        // two items added, the promise resolves, and the test passes.
        if (snap.data().itemCount === expectedCount && snap.data().totalPrice == expectedTotal) {
          // Call the function returned by `onSnapshot` to unsubscribe from updates
          unsubscribe();
          resolve();
        };
      });
    });
   });
 });

17. 運行測試

您可能仍在運行先前測試的模擬器。如果沒有,請啟動模擬器。從命令列運行

$ firebase emulators:start --import=./seed

開啟一個新的終端選項卡(保持模擬器運作)並移至函數目錄。您可能仍然在安全規則測試中保持開啟。

$ cd functions

現在執行單元測試,您應該會看到總共 5 個測試:

$ npm test

> functions@ test .../emulators-codelab/codelab-initial-state/functions
> mocha

  shopping cart creation
    ✓ can be created by the cart owner (82ms)

  shopping cart reads, updates, and deletes
    ✓ cart can be read by the cart owner (42ms)

  shopping cart items
    ✓ items can be read by the cart owner (40ms)
    ✓ items can be added by the cart owner

  adding an item to the cart recalculates the cart total. 
    1) should sum the cost of their items

  4 passing (2s)
  1 failing

如果查看具體的故障,似乎是超時錯誤。這是因為測試正在等待函數正確更新,但它永遠不會。現在,我們準備好編寫函數來滿足測試。

18. 寫一個函數

要修復此測試,您需要更新functions/index.js中的函數。雖然這個函數的一部分已經寫出來了,但它並不完整。這是該函數目前的樣子:

// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
    .firestore.document("carts/{cartId}/items/{itemId}")
    .onWrite(async (change, context) => {
      console.log(`onWrite: ${change.after.ref.path}`);
      if (!change.after.exists) {
        // Ignore deletes
        return;
      }

      let totalPrice = 125.98;
      let itemCount = 8;
      try {
        
        const cartRef = db.collection("carts").doc(context.params.cartId);

        await cartRef.update({
          totalPrice,
          itemCount
        });
      } catch(err) {
      }
    });

該函數正確設定了購物車引用,但它不是計算totalPriceitemCount的值,而是將它們更新為硬編碼的值。

獲取並迭代

items子集合

初始化一個新常數itemsSnap作為items子集合。然後,迭代集合中的所有文件。

// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
    .firestore.document("carts/{cartId}/items/{itemId}")
    .onWrite(async (change, context) => {
      console.log(`onWrite: ${change.after.ref.path}`);
      if (!change.after.exists) {
        // Ignore deletes
        return;
      }


      try {
        let totalPrice = 125.98;
        let itemCount = 8;

        const cartRef = db.collection("carts").doc(context.params.cartId);
        // ADD LINES FROM HERE
        const itemsSnap = await cartRef.collection("items").get();

        itemsSnap.docs.forEach(item => {
          const itemData = item.data();
        })
        // TO HERE
       
        return cartRef.update({
          totalPrice,
          itemCount
        });
      } catch(err) {
      }
    });

計算totalPrice和itemCount

首先,我們將totalPriceitemCount的值初始化為零。

然後,將邏輯新增到我們的迭代區塊中。首先,檢查該商品是否有價格。如果該項目沒有指定數量,則預設為1 。然後,將數量加到itemCount的運行總計中。最後,將商品的價格乘以數量加到totalPrice的運行總計中:

// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
    .firestore.document("carts/{cartId}/items/{itemId}")
    .onWrite(async (change, context) => {
      console.log(`onWrite: ${change.after.ref.path}`);
      if (!change.after.exists) {
        // Ignore deletes
        return;
      }

      try {
        // CHANGE THESE LINES
        let totalPrice = 0;
        let itemCount = 0;

        const cartRef = db.collection("carts").doc(context.params.cartId);
        const itemsSnap = await cartRef.collection("items").get();

        itemsSnap.docs.forEach(item => {
          const itemData = item.data();
          // ADD LINES FROM HERE
          if (itemData.price) {
            // If not specified, the quantity is 1
            const quantity = itemData.quantity ? itemData.quantity : 1;
            itemCount += quantity;
            totalPrice += (itemData.price * quantity);
          }
          // TO HERE
        })

        await cartRef.update({
          totalPrice,
          itemCount
        });
      } catch(err) {
      }
    });

您也可以新增日誌記錄以協助偵錯成功和錯誤狀態:

// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
    .firestore.document("carts/{cartId}/items/{itemId}")
    .onWrite(async (change, context) => {
      console.log(`onWrite: ${change.after.ref.path}`);
      if (!change.after.exists) {
        // Ignore deletes
        return;
      }

      let totalPrice = 0;
      let itemCount = 0;
      try {
        const cartRef = db.collection("carts").doc(context.params.cartId);
        const itemsSnap = await cartRef.collection("items").get();

        itemsSnap.docs.forEach(item => {
          const itemData = item.data();
          if (itemData.price) {
            // If not specified, the quantity is 1
            const quantity = (itemData.quantity) ? itemData.quantity : 1;
            itemCount += quantity;
            totalPrice += (itemData.price * quantity);
          }
        });

        await cartRef.update({
          totalPrice,
          itemCount
        });

        // OPTIONAL LOGGING HERE
        console.log("Cart total successfully recalculated: ", totalPrice);
      } catch(err) {
        // OPTIONAL LOGGING HERE
        console.warn("update error", err);
      }
    });

19. 重新運行測試

在命令列上,確保模擬器仍在運行並重新運行測試。您無需重新啟動模擬器,因為它們會自動取得功能的變更。您應該看到所有測試都通過了:

$ npm test
> functions@ test .../emulators-codelab/codelab-initial-state/functions
> mocha

  shopping cart creation
    ✓ can be created by the cart owner (306ms)

  shopping cart reads, updates, and deletes
    ✓ cart can be read by the cart owner (59ms)

  shopping cart items
    ✓ items can be read by the cart owner
    ✓ items can be added by the cart owner

  adding an item to the cart recalculates the cart total. 
    ✓ should sum the cost of their items (800ms)


  5 passing (1s)

好工作!

20. 使用 Storefront UI 進行嘗試

對於最終測試,返回 Web 應用程式 ( http://127.0.0.1:5000/ ) 並將商品新增至購物車。

69ad26cee520bf24.png

確認購物車已更新為正確的總數。極好的!

回顧

您已經完成了 Cloud Functions for Firebase 和 Cloud Firestore 之間的複雜測試案例。您編寫了一個雲端函數來使測試通過。您也確認了新功能正在 UI 中運作!您在本地完成了所有這些工作,在自己的電腦上運行模擬器。

您還建立了一個針對本機模擬器運行的 Web 用戶端、客製化的安全規則來保護數據,並使用本機模擬器測試了安全規則。

c6a7aeb91fe97a64.gif