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

1. 開始之前

Cloud Firestore 和 Cloud Functions 等無服務器後端工具非常易於使用,但可能難以測試。 Firebase Local Emulator Suite 允許您在開發機器上運行這些服務的本地版本,以便您可以快速安全地開發您的應用程序。

先決條件

  • 一個簡單的編輯器,例如 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 託管:快速安全地託管網絡應用程序。

您會將應用程序連接到 Emulator Suite 以啟用本地開發。

2589e2f95b74fa88.png

您還將學習如何:

  • 如何將您的應用程序連接到模擬器套件以及各種模擬器的連接方式。
  • Firebase 安全規則的工作原理以及如何針對本地模擬器測試 Firestore 安全規則。
  • 如何編寫由 Firestore 事件觸發的 Firebase 函數,以及如何編寫針對 Emulator Suite 運行的集成測試。

2.設置

獲取源代碼

在此 Codelab 中,您將從接近完成的 The 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

Emulator Suite 是 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.

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

將網絡應用程序連接到模擬器

根據日誌中的表格,我們可以看到 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 客戶端也指向本地模擬器而不是生產數據庫。

打開模擬器界面

在 Web 瀏覽器中,導航到http://127.0.0.1:4000/ 。您應該會看到 Emulator Suite 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)雲函數觸發

Cloud Function 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 使用名為“安全規則”的系統來聲明誰有權讀取和寫入數據。 Emulator Suite 是製作這些規則原型的好方法。

在編輯器中,打開文件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/新終端選項卡中的命令行上

首先進入 functions 目錄(我們將在此處完成 Codelab 的剩餘部分):

$ cd functions

現在在 functions 目錄中運行 mocha 測試,並滾動到輸出的頂部:

# 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時,Emulator Suite 會自動更新規則。您可以通過在運行模擬器的選項卡中查看消息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
    }));
  });

因此,我們可以編寫一個規則,如果當前用戶的 UID 與購物車文檔上的 ownerUID 相同,則允許訪問。由於不需要為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 來使用戶的購物車保持最新狀態,因此我們也想測試該代碼。

Emulator Suite 讓測試 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 之間的交互,因此它比之前的代碼實驗室中的測試涉及更多設置。讓我們瀏覽一下這個測試並了解它的期望。

創建購物車

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 之間的複雜測試用例。您編寫了一個 Cloud Function 來使測試通過。您還確認新功能在 UI 中正常運行!您在本地完成所有這些操作,在您自己的機器上運行模擬器。

您還創建了一個針對本地模擬器運行的 Web 客戶端,定制了安全規則來保護數據,並使用本地模擬器測試了安全規則。

c6a7aeb91fe97a64.gif