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ộ.
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 db
và auth
để 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.
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
.
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!
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:
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:
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.currentUser
là null
. 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:
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:
Đừ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:
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ộ.
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
:
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ọ!)
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.
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:
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ó itemCount
và totalPrice
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 totalPrice
và itemCount
, 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 totalPrice
và itemCount
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.
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ộ.