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 máy chủ như Cloud Firestore và Cloud Functions rất dễ sử dụng, nhưng có thể khó kiểm tra. Firebase Local Emulator Suite 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, chạy node --version )
  • Java 7 trở lên (để cài đặt Java, hãy sử dụng các hướng dẫn này , để kiểm tra phiên bản của bạn, hãy chạy java -version )

Bạn sẽ làm gì

Trong bảng mã 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 cung cấp bởi nhiều dịch vụ Firebase:

  • Cloud Firestore: cơ sở dữ liệu NoSQL, không máy chủ, có thể mở rộng 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 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.
  • Lưu trữ Firebase : lưu trữ nhanh và an toàn cho các ứng dụng web.

Bạn sẽ kết nối ứng dụng với Bộ phần mềm giả lập để 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ộ phần mềm giả lập 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 của Firebase và cách kiểm tra Quy tắc bảo mật của Firestore dựa trên trình giả lập cục bộ.
  • Cách viết Hàm Firebase được kích hoạt bởi các sự kiện Firestore và cách viết các bài kiểm tra tích hợp chạy trên Bộ giả lập.

2. Thiết lập

Lấy mã nguồn

Trong codelab này, bạn bắt đầu với 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 đó, chuyển vào thư mục codelab, nơi bạn sẽ làm việc cho phần còn lại của codelab này:

$ cd emulators-codelab/codelab-initial-state

Bây giờ, hãy cài đặt các phụ thuộc để bạn có thể chạy mã. Nếu bạn đang 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 của Firebase

Bộ giả lập 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. Bộ mã này phải 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 bao gồm 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 một dự án Firebase mới. Ghi lại ID dự án bạn chọn, bạn sẽ cần nó sau này.

Bây giờ chúng tôi cần kết nối mã này với dự án Firebase của bạn. Đầu 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 cho dự án. Thay thế $YOUR_PROJECT_ID bằng ID của 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 Bộ giả lập.

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 giả lập:

$ 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://localhost: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://localhost:4000                │
└─────────────────────────────────────────────────────────────┘

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

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

Sau khi bạn nhìn thấy thông báo Tất cả trình giả lập đã bắt đầu , ứ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 rằng trình giả lập Cloud Firestore đang lắng nghe trên cổng 8080 và trình giả lập Xác thực đang lắng nghe trên cổng 9099 .

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ localhost:9099 │ http://localhost:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ localhost:5001 │ http://localhost:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ localhost:8080 │ http://localhost:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting        │ localhost: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 giả lập, thay vì với sản xuất. Mở tệp public/js/homepage.js Home.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 / Home.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 giả lập cục bộ:

public / js / Home.js

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

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

Bây giờ khi ứng dụng đang chạy trên localhost (được cung cấp bởi trình giả lập Hosting), ứng dụng khách Firestore cũng chỉ vào trình giả lập 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: // localhost: 4000 / . Bạn sẽ thấy giao diện người dùng Bộ giả lập.

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

Nhấp để xem giao diện người dùng cho Trình giả lập 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, điều hướng đến http: // localhost: 5000 và bạn sẽ thấy The Fire Store đang chạy cục bộ trên máy của bạn!

939f87946bac2ee4.png

Sử dụng ứng dụng

Chọn một mặt hàng trên trang chủ và nhấp 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ứ đang chạy trong trình giả lập nên chúng tôi 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 trong 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ư có một số lỗi trong phương thức addToCart , chúng ta hãy xem xét điều đó. Nơi nào chúng ta cố gắng truy cập một cái gì đó được gọi là uid trong phương thức đó và tại sao nó lại là null ? Ngay bây giờ, phương thức trông giống như thế này trong public/js/homepage.js :

public / js / Home.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.currentUser không có null . Hãy thêm một kiểm tra cho điều đó:

public / js / Home.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 và sau đó nhấp vào Thêm vào giỏ hàng . Bạn sẽ gặp một lỗi đẹp hơn lần này:

c65f6c05588133f7.png

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

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

239f26f02f959eef.png

Đừng lo lắng, chúng tôi sẽ sớm sửa lỗi đó. Đầu tiên, hãy đi sâu vào những 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. Kích hoạt chức năng cục bộ

Nhấp vào Thêm vào giỏ hàng khởi động chuỗi sự kiện liên quan đến nhiều trình giả lập. Trong nhật ký CLI của Firebase, bạn sẽ thấy thông báo giống như 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 các nhật ký đó và bản cập nhật giao diện người dùng mà bạn đã quan sát:

68c9323f2ad10f7a.png

1) Viết Firestore - 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 / Home.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 bất kỳ sự kiện ghi nào (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 , bạn có thể thấy trong functions/index.js :

functions / 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 Viết - 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á cả, 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) Firestore Read - Khách hàng

Giao diện người dùng web được đăng ký để nhận thông tin cập nhật về các 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 / Home.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 cục bộ hoàn toàn sử dụng ba trình giả lập Firebase khác nhau để thử nghiệm cục bộ hoàn toàn.

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 các bài kiểm tra đơ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 các 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 vẫn chưa thực sự lo lắng về bảo mật. Cloud Firestore sử dụng một hệ thống gọi là "Quy tắc bảo mật" để tuyên bố ai có quyền truy cập để đọc và ghi dữ liệu. Bộ giả lập là một cách tuyệt vời để tạo nguyên mẫu các quy tắc này.

Trong trình chỉnh sửa, 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;
    }
  }
}

Ngay bây giờ 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ỉ các hoạt động hợp lệ mới được thông qua và chúng tôi không làm rò rỉ bất kỳ thông tin nhạy cảm nào.

Trong chương trình codelab này, tuân theo Nguyên tắc ít đặc quyền nhất, chúng tôi sẽ khóa tất cả cá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 mà 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 rằng bạn đang ở trong emulators-codelab/codelab-initial-state/ . Bạn vẫn có thể có trình giả lập đang chạy 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

Khi trình giả lập đang chạy, bạn có thể chạy thử nghiệm cục bộ đối với chúng.

Chạy các bài kiểm tra

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

Đầu tiên hãy chuyển vào thư mục chức năng (chúng tôi sẽ ở đây trong phần còn lại của bảng mã):

$ cd functions

Bây giờ hãy chạy các bài kiểm tra mocha trong thư mục chức năng 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

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

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

Hai lần thất bại đầu tiên là bài kiểm tra "giỏ hàng" 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 họ
  • Người dùng chỉ có thể đọc giỏ hàng của họ

functions / 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 của 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 một 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 đưa ra yêu cầu.

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

Bộ giả lập 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 giả lập đã cập nhật các quy tắc bằng cách xem trong tab đang chạy trình mô phỏng để biết 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 hiệ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 giao diện người dùng

Hiện tại, mặc dù chủ xe hàng đọc và ghi vào xe hàng của họ, nhưng họ không thể đọc hoặc ghi các mặt hàng riêng lẻ trong xe 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 đối với người dùng.

Quay lại giao diện người dùng web đang chạy trên http://localhost:5000, và thử thêm thứ gì đó vào giỏ hàng của bạn. Bạn gặp 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 phụ items .

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

Hai bài kiểm tra này xác nhận rằng người dùng chỉ có thể thêm các mặt hàng vào hoặc đọc các mặt hàng từ giỏ hàng của 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 tôi 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 , 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 phụ các mục. Nhận get trong điều kiện là đọ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 đầu đầu ra và kiểm tra xem có nhiều bài kiểm tra khác vượt qua 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

Tốt đẹ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 bài kiểm tra đang chờ xử lý, nhưng chúng tôi sẽ hoàn thành bài kiểm tra đó 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 của web ( http: // localhost: 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 chúng tôi dùng thử giao diện người dùng đã không thể thêm các mặt hàng vào giỏ hàng của họ!)

69ad26cee520bf24.png

Máy khách 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 cần thiết để chuẩn bị cho ứng dụng sẵn sàng đưa 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 tin tưởng về sau rằng dữ liệu giỏ hàng của chúng tôi sẽ có các kiểm soát truy cập này, ngay cả khi những người khác đang sửa đổi các 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ách viết một hàm được kích hoạt bởi sự kiện Firestore
  • Cách tạo các bài kiểm tra hoạt động trên nhiều trình giả lập

15. Thiết lập các bài 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 của mình 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ộ phần mềm giả lập giúp bạn dễ dàng kiểm tra các Chức năng đám mây, thậm chí 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, mở tệp emulators-codelab/codelab-initial-state/functions/test.js và cuộn đến lần 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 kiểm tra, hãy xóa .skip , vì vậy nó trông giống như sau:

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 bạn 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. Đi qua các bài kiểm tra Chức năng

Bởi vì thử nghiệm này xác nhận sự tương tác giữa Cloud Firestore và Cloud Functions, nó liên quan đến nhiều thiết lập hơn so với các thử nghiệm trong các codelabs trước đó. Hãy xem qua bài kiểm tra này và tìm hiểu về những gì nó mong đợi.

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 sử dụng bởi SDK quản trị. Đầu tiên, bạn khởi tạo một ứng dụng bằng initializeAdminApp thay vì initializeApp . Sau đó, bạn tạo DocumentReference cho giỏ hàng, chúng tôi 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 phụ 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 thử nghiệm việc 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ý một 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 cộng hai mặt hàng có giá 9,98 đô la với nhau. Sau đó, kiểm tra xem giỏ hàng có số lượng hàng và tổng giá dự itemCount ​​hay totalPrice . Nếu đúng như 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 các bài kiểm tra

Bạn vẫn có thể có trình giả lập đang chạy 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 đầu cuối mới (để trình giả lập đang chạy) và chuyển vào thư mục chức năng. Bạn vẫn có thể mở điều này từ các bài kiểm tra quy tắc bảo mật.

$ cd functions

Bây giờ chạy các bài kiểm tra đơn vị, bạn sẽ thấy tổng số 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ể, nó có vẻ là lỗi hết thời gian chờ. Điều này là do thử nghiệm đang đợi chức năng cập nhật chính xác, nhưng nó không bao giờ xảy ra. Bây giờ, chúng ta đã sẵn sàng để viết hàm để đáp ứng thử nghiệm.

18. Viết một hàm

Để sửa lỗi kiểm tra 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ó không hoàn chỉnh. Đây là cách hàm hiện tại trông như thế nào:

// 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 đang thiết lập chính xác tham chiếu giỏ hàng, nhưng sau đó thay vì tính toán các giá trị của totalPriceitemCount , nó sẽ cập nhật chúng thành những giá trị được mã hóa cứng.

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

bộ items tập phụ

Khởi tạo một hằng số mới, itemsSnap , để trở thành bộ sưu tập con của items . Sau đó, lặp lại 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

Đầu tiên, hãy khởi tạo các giá trị của totalPriceitemCount bằng 0.

Sau đó, thêm logic vào khối lặp lại 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 để nó mặc định là 1 . Sau đó, thêm số lượng vào tổng số itemCount đang chạy. Cuối cùng, thêm giá của mặt hàng nhân với số lượng vào tổng số đang chạy của tổng 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à các 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 các bài kiểm tra

Trên dòng lệnh, hãy đảm bảo rằng các trình giả lập vẫn đang chạy và chạy lại các bài kiểm tra. Bạn không cần phải khởi động lại trình giả lập vì chúng tự động nhận các thay đổi đối với các 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. Hãy dùng thử bằng giao diện người dùng Storefront

Đối với bài kiểm tra cuối cùng, hãy quay lại ứng dụng web ( http: // localhost: 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 với tổng số chính xác. Tuyệt vời!

Tóm tắt lại

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

Bạn cũng đã tạo một ứng dụng khách web đang chạy dựa trên trình giả lập cục bộ, các quy tắc bảo mật được điều chỉnh để bảo vệ dữ liệu và thử nghiệm các quy tắc bảo mật bằng cách sử dụng trình giả lập cục bộ.

c6a7aeb91fe97a64.gif