Phát triển cục bộ với Bộ mô phỏng Firebase

1. Trước khi bạn bắt đầu

Các công cụ phụ trợ không có máy chủ như Cloud Firestore và Cloud Functions rất dễ sử dụng nhưng có thể khó kiểm tra. Bộ mô phỏng cục bộ Firebase cho phép bạn chạy các phiên bản cục bộ của các dịch vụ này trên máy phát triển của mình để bạn có thể phát triển ứng dụng của mình một cách nhanh chóng và an toàn.

Điều kiện tiên quyết

  • Một trình soạn thảo đơn giản như Visual Studio Code, Atom hoặc Sublime Text
  • Node.js 10.0.0 trở lên (để cài đặt Node.js, hãy sử dụng nvm , để kiểm tra phiên bản của bạn, hãy chạy node --version )
  • Java 7 trở lên (để cài đặt Java, hãy sử dụng các hướng dẫn sau , để kiểm tra phiên bản của bạn, hãy chạy java -version )

Bạn sẽ làm gì

Trong lớp học lập trình này, bạn sẽ chạy và gỡ lỗi một ứng dụng mua sắm trực tuyến đơn giản được hỗ trợ bởi nhiều dịch vụ Firebase:

  • Cloud Firestore: cơ sở dữ liệu NoSQL, không có máy chủ, có thể mở rộng trên toàn cầu với khả năng thời gian thực.
  • Chức năng đám mây : mã phụ trợ không có máy chủ chạy để phản hồi các sự kiện hoặc yêu cầu HTTP.
  • Xác thực Firebase : dịch vụ xác thực được quản lý tích hợp với các sản phẩm Firebase khác.
  • Firebase Hosting : lưu trữ nhanh chóng và an toàn cho các ứng dụng web.

Bạn sẽ kết nối ứng dụng với Bộ mô phỏng để cho phép phát triển cục bộ.

2589e2f95b74fa88.png

Bạn cũng sẽ học cách:

  • Cách kết nối ứng dụng của bạn với Bộ mô phỏng và cách kết nối các trình mô phỏng khác nhau.
  • Cách hoạt động của Quy tắc bảo mật Firebase và cách kiểm tra Quy tắc bảo mật của Firestore dựa trên trình mô phỏng cục bộ.
  • Cách viết Hàm Firebase được kích hoạt bởi các sự kiện của Firestore và cách viết các bài kiểm tra tích hợp chạy trên Bộ mô phỏng.

2. Thiết lập

Lấy mã nguồn

Trong lớp học lập trình này, bạn bắt đầu với một phiên bản mẫu The Fire Store gần như hoàn chỉnh, vì vậy, điều đầu tiên bạn cần làm là sao chép mã nguồn:

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

Sau đó, hãy chuyển đến thư mục lớp học lập trình, nơi bạn sẽ làm việc trong phần còn lại của lớp học lập trình này:

$ cd emulators-codelab/codelab-initial-state

Bây giờ, hãy cài đặt các phần phụ thuộc để bạn có thể chạy mã. Nếu bạn sử dụng kết nối Internet chậm hơn, quá trình này có thể mất một hoặc hai phút:

# Move into the functions directory
$ cd functions

# Install dependencies
$ npm install

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

Nhận CLI Firebase

Bộ mô phỏng là một phần của Firebase CLI (giao diện dòng lệnh) có thể được cài đặt trên máy của bạn bằng lệnh sau:

$ npm install -g firebase-tools

Tiếp theo, xác nhận rằng bạn có phiên bản CLI mới nhất. Lớp học lập trình này sẽ hoạt động với phiên bản 9.0.0 trở lên nhưng các phiên bản mới hơn có nhiều bản sửa lỗi hơn.

$ firebase --version
9.6.0

Kết nối với dự án Firebase của bạn

Nếu bạn không có dự án Firebase, trong bảng điều khiển Firebase , hãy tạo dự án Firebase mới. Hãy ghi lại ID dự án bạn chọn, bạn sẽ cần nó sau này.

Bây giờ chúng ta cần kết nối mã này với dự án Firebase của bạn. Trước tiên hãy chạy lệnh sau để đăng nhập vào Firebase CLI:

$ firebase login

Tiếp theo chạy lệnh sau để tạo bí danh dự án. Thay thế $YOUR_PROJECT_ID bằng ID dự án Firebase của bạn.

$ firebase use $YOUR_PROJECT_ID

Bây giờ bạn đã sẵn sàng để chạy ứng dụng!

3. Chạy trình giả lập

Trong phần này, bạn sẽ chạy ứng dụng cục bộ. Điều này có nghĩa là đã đến lúc khởi động Emulator Suite.

Khởi động trình giả lập

Từ bên trong thư mục nguồn codelab, hãy chạy lệnh sau để khởi động trình mô phỏng:

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

Bạn sẽ thấy một số đầu ra như thế này:

$ 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.

Khi bạn thấy thông báo Tất cả trình mô phỏng đã bắt đầu thì ứng dụng đã sẵn sàng để sử dụng.

Kết nối ứng dụng web với trình giả lập

Dựa vào bảng trong nhật ký, chúng ta có thể thấy trình giả lập Cloud Firestore đang nghe trên cổng 8080 và trình giả lập Xác thực đang nghe trên cổng 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                             │
└────────────────┴────────────────┴─────────────────────────────────┘

Hãy kết nối mã giao diện người dùng của bạn với trình mô phỏng thay vì với sản phẩm. Mở tệp public/js/homepage.js và tìm hàm onDocumentReady . Chúng ta có thể thấy rằng mã truy cập vào các phiên bản Firestore và Auth tiêu chuẩn:

public/js/homepage.js

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

Hãy cập nhật các đối tượng dbauth để trỏ đến trình mô phỏng cục bộ:

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

Giờ đây, khi ứng dụng đang chạy trên máy cục bộ của bạn (được cung cấp bởi trình mô phỏng Hosting), ứng dụng khách Firestore cũng trỏ đến trình mô phỏng cục bộ thay vì vào cơ sở dữ liệu sản xuất.

Mở EmulatorUI

Trong trình duyệt web của bạn, điều hướng đến http://127.0.0.1:4000/ . Bạn sẽ thấy giao diện người dùng Emulator Suite.

Màn hình chính giao diện người dùng giả lập

Nhấp để xem giao diện người dùng cho Trình mô phỏng Firestore. Bộ sưu tập items đã chứa dữ liệu do dữ liệu được nhập bằng cờ --import .

4ef88d0148405d36.png

4. Chạy ứng dụng

Mở ứng dụng

Trong trình duyệt web của bạn, hãy điều hướng đến http://127.0.0.1:5000 và bạn sẽ thấy The Fire Store chạy cục bộ trên máy của mình!

939f87946bac2ee4.png

Sử dụng ứng dụng

Chọn một món hàng trên trang chủ và nhấn vào Thêm vào giỏ hàng . Thật không may, bạn sẽ gặp phải lỗi sau:

a11bd59933a8e885.png

Hãy sửa lỗi đó! Vì mọi thứ đều chạy trong trình giả lập nên chúng ta có thể thử nghiệm và không lo ảnh hưởng đến dữ liệu thực.

5. Gỡ lỗi ứng dụng

Tìm lỗi

Được rồi, hãy xem bảng điều khiển dành cho nhà phát triển Chrome. Nhấn Control+Shift+J (Windows, Linux, Chrome OS) hoặc Command+Option+J (Mac) để xem lỗi trên bảng điều khiển:

74c45df55291dab1.png

Có vẻ như đã xảy ra lỗi nào đó trong phương thức addToCart , chúng ta hãy xem xét điều đó. Chúng ta cố gắng truy cập thứ gì đó gọi là uid trong phương thức đó ở đâu và tại sao nó lại là null ? Hiện tại, phương thức này trông như thế này trong public/js/homepage.js :

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

Aha! Chúng tôi chưa đăng nhập vào ứng dụng. Theo tài liệu Xác thực Firebase , khi chúng tôi chưa đăng nhập, auth.currentUsernull . Hãy thêm một kiểm tra cho điều đó:

public/js/homepage.js

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

    // ...
  }

Kiểm tra ứng dụng

Bây giờ, hãy làm mới trang rồi nhấn vào Thêm vào giỏ hàng . Lần này bạn sẽ gặp một lỗi dễ chịu hơn:

c65f6c05588133f7.png

Nhưng nếu bạn nhấp vào Đăng nhập ở thanh công cụ phía trên rồi nhấp lại vào Thêm vào giỏ hàng , bạn sẽ thấy giỏ hàng đã được cập nhật.

Tuy nhiên, có vẻ như những con số này không hề chính xác chút nào:

239f26f02f959eef.png

Đừng lo lắng, chúng tôi sẽ sớm khắc phục lỗi đó. Trước tiên, hãy tìm hiểu sâu hơn điều gì đã thực sự xảy ra khi bạn thêm một mặt hàng vào giỏ hàng của mình.

6. Trình kích hoạt chức năng cục bộ

Việc nhấp vào Thêm vào giỏ hàng sẽ khởi động một chuỗi sự kiện liên quan đến nhiều trình mô phỏng. Trong nhật ký CLI của Firebase, bạn sẽ thấy thông báo tương tự như sau sau khi thêm một mặt hàng vào giỏ hàng của mình:

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

Có bốn sự kiện chính đã xảy ra để tạo ra những nhật ký đó và bản cập nhật giao diện người dùng mà bạn quan sát thấy:

68c9323f2ad10f7a.png

1) Firestore Write - Máy khách

Một tài liệu mới được thêm vào bộ sưu tập Firestore /carts/{cartId}/items/{itemId}/ . Bạn có thể thấy mã này trong hàm addToCart bên trong public/js/homepage.js :

public/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) Chức năng đám mây được kích hoạt

Chức năng đám mây calculateCart lắng nghe mọi sự kiện ghi (tạo, cập nhật hoặc xóa) xảy ra với các mặt hàng trong giỏ hàng bằng cách sử dụng trình kích hoạt onWrite mà bạn có thể thấy trong functions/index.js :

hàm/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) Viết Firestore - Quản trị viên

Hàm calculateCart đọc tất cả các mặt hàng trong giỏ hàng và cộng tổng số lượng và giá, sau đó cập nhật tài liệu "giỏ hàng" với tổng số mới (xem cartRef.update(...) ở trên).

4) Đọc Firestore - Máy khách

Giao diện người dùng web được đăng ký để nhận thông tin cập nhật về những thay đổi đối với giỏ hàng. Nó nhận được bản cập nhật theo thời gian thực sau khi Chức năng đám mây ghi tổng số mới và cập nhật giao diện người dùng, như bạn có thể thấy trong public/js/homepage.js :

public/js/homepage.js

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

Tóm tắt lại

Công việc tốt đẹp! Bạn vừa thiết lập một ứng dụng hoàn toàn cục bộ sử dụng ba trình mô phỏng Firebase khác nhau để thử nghiệm hoàn toàn cục bộ.

db82eef1706c9058.gif

Nhưng xin chờ chút nữa! Trong phần tiếp theo bạn sẽ học:

  • Cách viết bài kiểm thử đơn vị sử dụng Trình mô phỏng Firebase.
  • Cách sử dụng Trình mô phỏng Firebase để gỡ lỗi Quy tắc bảo mật của bạn.

7. Tạo quy tắc bảo mật phù hợp với ứng dụng của bạn

Ứng dụng web của chúng tôi đọc và ghi dữ liệu nhưng cho đến nay chúng tôi chưa thực sự lo lắng về vấn đề bảo mật. Cloud Firestore sử dụng một hệ thống có tên "Quy tắc bảo mật" để khai báo ai có quyền truy cập đọc và ghi dữ liệu. Bộ mô phỏng là một cách tuyệt vời để tạo nguyên mẫu cho những quy tắc này.

Trong trình chỉnh sửa, hãy mở tệp emulators-codelab/codelab-initial-state/firestore.rules . Bạn sẽ thấy rằng chúng tôi có ba phần chính trong quy tắc của mình:

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

Hiện tại, bất kỳ ai cũng có thể đọc và ghi dữ liệu vào cơ sở dữ liệu của chúng tôi! Chúng tôi muốn đảm bảo rằng chỉ những hoạt động hợp lệ mới được thông qua và chúng tôi không rò rỉ bất kỳ thông tin nhạy cảm nào.

Trong lớp học lập trình này, tuân theo Nguyên tắc đặc quyền tối thiểu, chúng tôi sẽ khóa tất cả tài liệu và dần dần thêm quyền truy cập cho đến khi tất cả người dùng có tất cả quyền truy cập họ cần, nhưng không nhiều hơn. Hãy cập nhật hai quy tắc đầu tiên để từ chối quyền truy cập bằng cách đặt điều kiện thành 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. Chạy trình giả lập và kiểm tra

Khởi động trình giả lập

Trên dòng lệnh, hãy đảm bảo bạn đang ở trong emulators-codelab/codelab-initial-state/ . Bạn vẫn có thể chạy trình mô phỏng từ các bước trước đó. Nếu không, hãy khởi động lại trình giả lập:

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

Sau khi trình mô phỏng đang chạy, bạn có thể chạy thử nghiệm cục bộ đối với chúng.

Chạy thử nghiệm

Trên dòng lệnh trong tab thiết bị đầu cuối mới từ thư emulators-codelab/codelab-initial-state/

Trước tiên, hãy chuyển đến thư mục hàm (chúng ta sẽ ở lại đây trong phần còn lại của lớp học lập trình):

$ cd functions

Bây giờ hãy chạy thử nghiệm mocha trong thư mục hàm và cuộn lên đầu đầu ra:

# 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

Hiện tại chúng tôi có bốn thất bại. Khi xây dựng tệp quy tắc, bạn có thể đo lường tiến độ bằng cách xem nhiều bài kiểm tra vượt qua hơn.

9. Truy cập giỏ hàng an toàn

Hai lần thất bại đầu tiên là thử nghiệm "giỏ hàng" nhằm kiểm tra rằng:

  • Người dùng chỉ có thể tạo và cập nhật giỏ hàng của riêng mình
  • Người dùng chỉ có thể đọc giỏ hàng của riêng họ

hàm/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());
  });

Hãy làm cho những bài kiểm tra này vượt qua. Trong trình chỉnh sửa, hãy mở tệp quy tắc bảo mật firestore.rules và cập nhật các câu lệnh trong match /carts/{cartID} :

firestore.rules

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

    // ...
  }
}

Các quy tắc này hiện chỉ cho phép chủ sở hữu giỏ hàng truy cập đọc và ghi.

Để xác minh dữ liệu đến và xác thực người dùng, chúng tôi sử dụng hai đối tượng có sẵn trong ngữ cảnh của mọi quy tắc:

  • Đối tượng request chứa dữ liệu và siêu dữ liệu về thao tác đang được thực hiện.
  • Nếu dự án Firebase đang sử dụng Xác thực Firebase thì đối tượng request.auth sẽ mô tả người dùng đang thực hiện yêu cầu.

10. Kiểm tra quyền truy cập giỏ hàng

Bộ mô phỏng tự động cập nhật các quy tắc bất cứ khi nào firestore.rules được lưu. Bạn có thể xác nhận rằng trình mô phỏng đã cập nhật các quy tắc bằng cách xem trong tab đang chạy trình mô phỏng để tìm thông báo Rules updated :

5680da418b420226.png

Chạy lại các bài kiểm tra và kiểm tra xem hai bài kiểm tra đầu tiên đã vượt qua chưa:

$ 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

Làm tốt lắm! Bây giờ bạn đã có quyền truy cập an toàn vào giỏ hàng. Hãy chuyển sang bài kiểm tra thất bại tiếp theo.

11. Kiểm tra luồng "Thêm vào giỏ hàng" trong UI

Hiện tại, mặc dù chủ sở hữu giỏ hàng đọc và ghi vào giỏ hàng của mình nhưng họ không thể đọc hoặc ghi từng mục riêng lẻ vào giỏ hàng của mình. Đó là bởi vì trong khi chủ sở hữu có quyền truy cập vào tài liệu giỏ hàng, họ không có quyền truy cập vào bộ sưu tập phụ các mặt hàng của giỏ hàng.

Đây là một trạng thái bị hỏng cho người dùng.

Quay lại giao diện người dùng web đang chạy trên http://127.0.0.1:5000, và thử thêm mặt hàng nào đó vào giỏ hàng của bạn. Bạn gặp phải lỗi Permission Denied , hiển thị từ bảng điều khiển gỡ lỗi vì chúng tôi chưa cấp cho người dùng quyền truy cập vào các tài liệu đã tạo trong bộ sưu tập items .

12. Cho phép truy cập các mục trong giỏ hàng

Hai thử nghiệm này xác nhận rằng người dùng chỉ có thể thêm mặt hàng vào hoặc đọc các mặt hàng từ giỏ hàng của chính họ:

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

Vì vậy, chúng ta có thể viết quy tắc cho phép truy cập nếu người dùng hiện tại có cùng UID với ownerUID trên tài liệu giỏ hàng. Vì không cần chỉ định các quy tắc khác nhau để create, update, delete nên bạn có thể sử dụng quy tắc write áp dụng cho tất cả các yêu cầu sửa đổi dữ liệu.

Cập nhật quy tắc cho các tài liệu trong bộ sưu tập con vật phẩm. get trong điều kiện đang đọc một giá trị từ Firestore–trong trường hợp này là ownerUID trên tài liệu giỏ hàng.

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. Kiểm tra quyền truy cập các mặt hàng trong giỏ hàng

Bây giờ chúng ta có thể chạy lại bài kiểm tra. Cuộn lên trên cùng của đầu ra và kiểm tra xem có nhiều bài kiểm tra vượt qua hơn không:

$ 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

Đẹp! Bây giờ tất cả các bài kiểm tra của chúng tôi đều vượt qua. Chúng tôi có một thử nghiệm đang chờ xử lý nhưng chúng tôi sẽ thực hiện thử nghiệm đó sau một vài bước.

14. Kiểm tra lại quy trình "thêm vào giỏ hàng"

Quay lại giao diện người dùng web ( http://127.0.0.1:5000 ) và thêm một mặt hàng vào giỏ hàng. Đây là một bước quan trọng để xác nhận rằng các thử nghiệm và quy tắc của chúng tôi phù hợp với chức năng mà khách hàng yêu cầu. (Hãy nhớ rằng lần cuối cùng chúng tôi dùng thử, người dùng UI không thể thêm mặt hàng vào giỏ hàng của họ!)

69ad26cee520bf24.png

Máy khách sẽ tự động tải lại các quy tắc khi firestore.rules được lưu. Vì vậy, hãy thử thêm thứ gì đó vào giỏ hàng.

Tóm tắt lại

Công việc tốt đẹp! Bạn vừa cải thiện tính bảo mật cho ứng dụng của mình, một bước thiết yếu để chuẩn bị đưa ứng dụng vào sản xuất! Nếu đây là một ứng dụng sản xuất, chúng tôi có thể thêm các thử nghiệm này vào quy trình tích hợp liên tục của mình. Điều này sẽ giúp chúng tôi tự tin trong tương lai rằng dữ liệu giỏ hàng của chúng tôi sẽ có các biện pháp kiểm soát truy cập này, ngay cả khi những người khác đang sửa đổi quy tắc.

ba5440b193e75967.gif

Nhưng xin chờ chút nữa!

nếu bạn tiếp tục, bạn sẽ học được:

  • Cách viết hàm được kích hoạt bởi sự kiện Firestore
  • Cách tạo thử nghiệm hoạt động trên nhiều trình mô phỏng

15. Thiết lập kiểm tra Chức năng đám mây

Cho đến nay, chúng tôi đã tập trung vào giao diện người dùng của ứng dụng web và Quy tắc bảo mật của Firestore. Nhưng ứng dụng này cũng sử dụng Chức năng đám mây để cập nhật giỏ hàng của người dùng, vì vậy chúng tôi cũng muốn kiểm tra mã đó.

Bộ mô phỏng giúp việc kiểm tra các Chức năng đám mây trở nên dễ dàng, ngay cả các chức năng sử dụng Cloud Firestore và các dịch vụ khác.

Trong trình chỉnh sửa, hãy mở tệp emulators-codelab/codelab-initial-state/functions/test.js và cuộn đến bài kiểm tra cuối cùng trong tệp. Ngay bây giờ, nó được đánh dấu là đang chờ xử lý:

//  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 () => {
    ...
  });
});

Để kích hoạt thử nghiệm, hãy xóa .skip , nó sẽ trông như thế này:

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

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

Tiếp theo, tìm biến REAL_FIREBASE_PROJECT_ID ở đầu tệp và thay đổi nó thành ID dự án Firebase thực của bạn.:

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

Nếu quên ID dự án của mình, bạn có thể tìm ID dự án Firebase của mình trong Cài đặt dự án trong Bảng điều khiển Firebase:

d6d0429b700d2b21.png

16. Xem qua các bài kiểm tra Chức năng

Vì thử nghiệm này xác thực sự tương tác giữa Cloud Firestore và Cloud Functions nên thử nghiệm này đòi hỏi nhiều thao tác thiết lập hơn so với các thử nghiệm trong lớp học lập trình trước đó. Hãy cùng xem qua bài kiểm tra này và biết nó mong đợi điều gì.

Tạo một giỏ hàng

Chức năng đám mây chạy trong môi trường máy chủ đáng tin cậy và có thể sử dụng xác thực tài khoản dịch vụ được SDK quản trị sử dụng. Trước tiên, bạn khởi tạo ứng dụng bằng cách sử dụng initializeAdminApp thay vì initializeApp . Sau đó, bạn tạo DocumentReference cho giỏ hàng mà chúng ta sẽ thêm các mặt hàng vào và khởi tạo giỏ hàng:

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

    ...
  });

Kích hoạt chức năng

Sau đó, thêm tài liệu vào bộ sưu tập con items của tài liệu giỏ hàng của chúng tôi để kích hoạt chức năng. Thêm hai mục để đảm bảo bạn đang kiểm tra phần bổ sung xảy ra trong hàm.

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

    ...
    });
  });

Đặt kỳ vọng kiểm tra

Sử dụng onSnapshot() để đăng ký trình nghe cho bất kỳ thay đổi nào trên tài liệu giỏ hàng. onSnapshot() trả về một hàm mà bạn có thể gọi để hủy đăng ký trình nghe.

Đối với thử nghiệm này, hãy thêm hai mặt hàng có giá 9,98 USD. Sau đó, kiểm tra xem giỏ hàng có itemCounttotalPrice dự kiến ​​hay không. Nếu vậy thì hàm đó đã thực hiện công việc của nó.

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. Chạy thử nghiệm

Bạn vẫn có thể chạy trình mô phỏng từ các thử nghiệm trước đó. Nếu không, hãy khởi động trình giả lập. Từ dòng lệnh, chạy

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

Mở tab terminal mới (để trình giả lập chạy) và di chuyển vào thư mục chức năng. Bạn vẫn có thể mở cái này từ các bài kiểm tra quy tắc bảo mật.

$ cd functions

Bây giờ hãy chạy thử nghiệm đơn vị, bạn sẽ thấy tổng cộng 5 bài kiểm tra:

$ 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

Nếu bạn nhìn vào lỗi cụ thể thì có vẻ như đó là lỗi hết thời gian chờ. Điều này là do quá trình kiểm tra đang chờ chức năng cập nhật chính xác nhưng không bao giờ thực hiện được. Bây giờ, chúng ta đã sẵn sàng viết hàm để đáp ứng bài kiểm tra.

18. Viết hàm

Để khắc phục thử nghiệm này, bạn cần cập nhật hàm trong functions/index.js . Mặc dù một số chức năng này đã được viết nhưng nó vẫn chưa hoàn chỉnh. Đây là giao diện của hàm hiện tại:

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

Hàm này đang đặt tham chiếu giỏ hàng một cách chính xác, nhưng sau đó thay vì tính toán các giá trị của totalPriceitemCount , hàm này sẽ cập nhật chúng thành giá trị được mã hóa cứng.

Tìm nạp và lặp qua

bộ sưu tập items

Khởi tạo một hằng số mới, itemsSnap , làm tập hợp items . Sau đó, lặp qua tất cả các tài liệu trong bộ sưu tập.

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

Tính tổng Giá và số lượng mặt hàng

Trước tiên, hãy khởi tạo các giá trị của totalPriceitemCount thành 0.

Sau đó, thêm logic vào khối lặp của chúng tôi. Đầu tiên, hãy kiểm tra xem mặt hàng đó có giá không. Nếu mặt hàng không có số lượng được chỉ định, hãy đặt mặc định là 1 . Sau đó, thêm số lượng vào tổng số itemCount đang chạy. Cuối cùng, cộng giá của mặt hàng nhân với số lượng để có tổng giá trị hiện tại của 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) {
      }
    });

Bạn cũng có thể thêm ghi nhật ký để giúp gỡ lỗi thành công và trạng thái lỗi:

// 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. Chạy lại bài kiểm tra

Trên dòng lệnh, hãy đảm bảo trình mô phỏng vẫn đang chạy và chạy lại thử nghiệm. Bạn không cần phải khởi động lại trình mô phỏng vì chúng tự động nhận các thay đổi đối với chức năng. Bạn sẽ thấy tất cả các bài kiểm tra đều vượt qua:

$ 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)

Làm tốt lắm!

20. Dùng thử bằng Giao diện người dùng Storefront

Đối với lần kiểm tra cuối cùng, hãy quay lại ứng dụng web ( http://127.0.0.1:5000/ ) và thêm một mặt hàng vào giỏ hàng.

69ad26cee520bf24.png

Xác nhận rằng giỏ hàng cập nhật tổng số tiền chính xác. Tuyệt vời!

Tóm tắt lại

Bạn đã xem qua một trường hợp thử nghiệm phức tạp giữa Cloud Functions cho Firebase và Cloud Firestore. Bạn đã viết Hàm đám mây để vượt qua bài kiểm tra. Bạn cũng đã xác nhận chức năng mới đang hoạt động trong giao diện người dùng! Bạn đã thực hiện tất cả những điều này cục bộ, chạy trình mô phỏng trên máy của riêng bạn.

Bạn cũng đã tạo một ứng dụng khách web chạy trên trình mô phỏng cục bộ, điều chỉnh các quy tắc bảo mật để bảo vệ dữ liệu và kiểm tra các quy tắc bảo mật bằng cách sử dụng trình mô phỏng cục bộ.

c6a7aeb91fe97a64.gif