1. ก่อนเริ่ม
เครื่องมือแบ็กเอนด์แบบไร้เซิร์ฟเวอร์ เช่น Cloud Firestore และ Cloud Functions นั้นใช้งานง่ายมาก แต่ทดสอบได้ยาก Firebase Local Emulator Suite ช่วยให้คุณสามารถเรียกใช้บริการเหล่านี้ในเวอร์ชันท้องถิ่นบนเครื่องพัฒนาของคุณ คุณจึงสามารถพัฒนาแอปได้อย่างรวดเร็วและปลอดภัย
ข้อกำหนดเบื้องต้น
- โปรแกรมแก้ไขอย่างง่าย เช่น Visual Studio Code, Atom หรือ Sublime Text
- Node.js 10.0.0 หรือสูงกว่า (ในการติดตั้ง Node.js ให้ใช้ nvm เพื่อตรวจสอบเวอร์ชันของคุณ เรียกใช้
node --version
) - Java 7 หรือสูงกว่า (ในการติดตั้ง Java ใช้คำแนะนำเหล่านี้ เพื่อตรวจสอบเวอร์ชันของคุณ ให้รัน
java -version
)
สิ่งที่คุณจะทำ
ใน Codelab นี้ คุณจะเรียกใช้และดีบักแอปซื้อของออนไลน์แบบง่ายๆ ซึ่งขับเคลื่อนโดยบริการ Firebase หลายอย่าง:
- Cloud Firestore: ฐานข้อมูล NoSQL ที่ปรับขนาดได้ทั่วโลก ไร้เซิร์ฟเวอร์ พร้อมความสามารถตามเวลาจริง
- Cloud Functions : โค้ดแบ็กเอนด์แบบไร้เซิร์ฟเวอร์ที่ทำงานเพื่อตอบสนองเหตุการณ์หรือคำขอ HTTP
- Firebase Authentication : บริการตรวจสอบสิทธิ์ที่มีการจัดการซึ่งผสานรวมกับผลิตภัณฑ์ Firebase อื่นๆ
- Firebase Hosting : โฮสติ้งที่รวดเร็วและปลอดภัยสำหรับเว็บแอป
คุณจะเชื่อมต่อแอปกับ Emulator Suite เพื่อเปิดใช้งานการพัฒนาในท้องถิ่น
คุณจะได้เรียนรู้วิธีการ:
- วิธีเชื่อมต่อแอปของคุณกับ Emulator Suite และวิธีเชื่อมต่อโปรแกรมจำลองต่างๆ
- วิธีการทำงานของกฎความปลอดภัยของ Firebase และวิธีทดสอบกฎความปลอดภัยของ Firestore กับโปรแกรมจำลองในเครื่อง
- วิธีเขียนฟังก์ชัน Firebase ที่ถูกกระตุ้นโดยเหตุการณ์ Firestore และวิธีเขียนการทดสอบการรวมที่รันกับ Emulator Suite
2. ตั้งค่า
รับซอร์สโค้ด
ใน Codelab นี้ คุณจะเริ่มต้นด้วยตัวอย่างเวอร์ชันของ The Fire Store ที่ใกล้จะเสร็จสมบูรณ์ ดังนั้น สิ่งแรกที่คุณต้องทำคือโคลนซอร์สโค้ด:
$ git clone https://github.com/firebase/emulators-codelab.git
จากนั้นย้ายไปที่ไดเร็กทอรีของ codelab ซึ่งคุณจะทำงานส่วนที่เหลือของ codelab นี้:
$ cd emulators-codelab/codelab-initial-state
ตอนนี้ติดตั้งการพึ่งพาเพื่อให้คุณสามารถเรียกใช้รหัสได้ หากคุณใช้การเชื่อมต่ออินเทอร์เน็ตที่ช้าลง อาจใช้เวลาหนึ่งหรือสองนาที:
# Move into the functions directory
$ cd functions
# Install dependencies
$ npm install
# Move back into the previous directory
$ cd ../
รับ Firebase CLI
Emulator Suite เป็นส่วนหนึ่งของ Firebase CLI (อินเทอร์เฟซบรรทัดคำสั่ง) ซึ่งสามารถติดตั้งบนเครื่องของคุณด้วยคำสั่งต่อไปนี้:
$ npm install -g firebase-tools
จากนั้น ยืนยันว่าคุณมี CLI เวอร์ชันล่าสุด Codelab นี้ควรทำงานกับเวอร์ชัน 9.0.0 หรือสูงกว่า แต่เวอร์ชันที่ใหม่กว่าจะมีการแก้ไขข้อบกพร่องเพิ่มเติม
$ firebase --version 9.6.0
เชื่อมต่อกับโครงการ Firebase ของคุณ
หากคุณไม่มีโปรเจ็กต์ Firebase ใน คอนโซล Firebase ให้สร้างโปรเจ็กต์ Firebase ใหม่ จดรหัสโครงการที่คุณเลือก คุณจะต้องใช้ในภายหลัง
ตอนนี้เราต้องเชื่อมต่อรหัสนี้กับโครงการ Firebase ของคุณ เรียกใช้คำสั่งต่อไปนี้เพื่อเข้าสู่ระบบ Firebase CLI ก่อน:
$ firebase login
เรียกใช้คำสั่งต่อไปนี้เพื่อสร้างนามแฝงโครงการ แทนที่ $YOUR_PROJECT_ID
ด้วย ID ของโครงการ Firebase ของคุณ
$ firebase use $YOUR_PROJECT_ID
ตอนนี้คุณพร้อมที่จะเรียกใช้แอพแล้ว!
3. เรียกใช้โปรแกรมจำลอง
ในส่วนนี้ คุณจะเรียกใช้แอปในเครื่อง ซึ่งหมายความว่าได้เวลาบู๊ต Emulator Suite แล้ว
เริ่มโปรแกรมจำลอง
จากภายในไดเร็กทอรีต้นทางของ Codelab ให้รันคำสั่งต่อไปนี้เพื่อเริ่มโปรแกรมจำลอง:
$ firebase emulators:start --import=./seed
คุณควรเห็นผลลัพธ์บางอย่างดังนี้:
$ firebase emulators:start --import=./seed i emulators: Starting emulators: auth, functions, firestore, hosting ⚠ functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: database, pubsub i firestore: Importing data from /Users/samstern/Projects/emulators-codelab/codelab-initial-state/seed/firestore_export/firestore_export.overall_export_metadata i firestore: Firestore Emulator logging to firestore-debug.log i hosting: Serving hosting files from: public ✔ hosting: Local server: http://127.0.0.1:5000 i ui: Emulator UI logging to ui-debug.log i functions: Watching "/Users/samstern/Projects/emulators-codelab/codelab-initial-state/functions" for Cloud Functions... ✔ functions[calculateCart]: firestore function initialized. ┌─────────────────────────────────────────────────────────────┐ │ ✔ All emulators ready! It is now safe to connect your app. │ │ i View Emulator UI at http://127.0.0.1:4000 │ └─────────────────────────────────────────────────────────────┘ ┌────────────────┬────────────────┬─────────────────────────────────┐ │ Emulator │ Host:Port │ View in Emulator UI │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Authentication │ 127.0.0.1:9099 │ http://127.0.0.1:4000/auth │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Functions │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Firestore │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Hosting │ 127.0.0.1:5000 │ n/a │ └────────────────┴────────────────┴─────────────────────────────────┘ Emulator Hub running at 127.0.0.1:4400 Other reserved ports: 4500 Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.
เมื่อคุณเห็นข้อความ เริ่มต้นโปรแกรมจำลองทั้งหมด แสดงว่าแอปพร้อมใช้งานแล้ว
เชื่อมต่อเว็บแอปกับอีมูเลเตอร์
จากตารางในบันทึก เราจะเห็นว่าโปรแกรมจำลอง Cloud Firestore กำลังฟังพอร์ต 8080
และโปรแกรมจำลองการตรวจสอบสิทธิ์กำลังฟังบนพอร์ต 9099
┌────────────────┬────────────────┬─────────────────────────────────┐ │ Emulator │ Host:Port │ View in Emulator UI │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Authentication │ 127.0.0.1:9099 │ http://127.0.0.1:4000/auth │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Functions │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Firestore │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Hosting │ 127.0.0.1:5000 │ n/a │ └────────────────┴────────────────┴─────────────────────────────────┘
มาเชื่อมต่อโค้ดส่วนหน้าของคุณกับอีมูเลเตอร์ แทนที่จะเชื่อมต่อกับเวอร์ชันที่ใช้งานจริง เปิดไฟล์ public/js/homepage.js
และค้นหาฟังก์ชัน onDocumentReady
เราจะเห็นว่ารหัสเข้าถึงอินสแตนซ์ Firestore และ Auth มาตรฐาน:
สาธารณะ/js/homepage.js
const auth = firebaseApp.auth();
const db = firebaseApp.firestore();
มาอัปเดตวัตถุ db
และ auth
ให้ชี้ไปที่ตัวจำลองในเครื่อง:
สาธารณะ/js/homepage.js
const auth = firebaseApp.auth();
const db = firebaseApp.firestore();
// ADD THESE LINES
if (location.hostname === "127.0.0.1") {
console.log("127.0.0.1 detected!");
auth.useEmulator("http://127.0.0.1:9099");
db.useEmulator("127.0.0.1", 8080);
}
ตอนนี้เมื่อแอปทำงานบนเครื่องโลคัลของคุณ (ให้บริการโดยโปรแกรมจำลองโฮสติ้ง) ไคลเอนต์ Firestore จะชี้ไปที่โปรแกรมจำลองภายในเครื่องแทนที่จะเป็นฐานข้อมูลที่ใช้งานจริง
เปิด EmulatorUI
ในเว็บเบราว์เซอร์ของคุณ ให้ไปที่ http://127.0.0.1:4000/ คุณควรเห็น Emulator Suite UI
คลิกเพื่อดู UI สำหรับ Firestore Emulator คอลเลกชัน items
มีข้อมูลอยู่แล้วเนื่องจากข้อมูลที่นำเข้าด้วยแฟล็ก --import
4. เรียกใช้แอพ
เปิดแอป
ในเว็บเบราว์เซอร์ของคุณ ให้ไปที่ http://127.0.0.1:5000 แล้วคุณจะเห็น The Fire Store ทำงานอยู่ในเครื่องของคุณ!
ใช้แอพ
เลือกสินค้าในหน้าแรกแล้วคลิก เพิ่มในรถเข็น น่าเสียดายที่คุณจะพบข้อผิดพลาดต่อไปนี้:
มาแก้ไขข้อบกพร่องกันเถอะ! เนื่องจากทุกอย่างทำงานในอีมูเลเตอร์ เราจึงสามารถทดลองและไม่ต้องกังวลว่าจะส่งผลกระทบต่อข้อมูลจริง
5. ดีบักแอป
ค้นหาจุดบกพร่อง
ตกลง มาดูในคอนโซลนักพัฒนา Chrome กัน กด Control+Shift+J
(Windows, Linux, Chrome OS) หรือ Command+Option+J
(Mac) เพื่อดูข้อผิดพลาดบนคอนโซล:
ดูเหมือนว่ามีข้อผิดพลาดบางอย่างในเมธอด addToCart
มาดูกันดีกว่า เราพยายามเข้าถึงสิ่งที่เรียกว่า uid
ในวิธีการนั้นที่ไหนและทำไมมันถึงเป็น null
? ตอนนี้วิธีการมีลักษณะดังนี้ใน public/js/homepage.js
:
สาธารณะ/js/homepage.js
addToCart(id, itemData) {
console.log("addToCart", id, JSON.stringify(itemData));
return this.db
.collection("carts")
.doc(this.auth.currentUser.uid)
.collection("items")
.doc(id)
.set(itemData);
}
อะฮ่า! เราไม่ได้ลงชื่อเข้าใช้แอป ตาม เอกสารการรับรองความถูกต้องของ Firebase เมื่อเราไม่ได้ลงชื่อเข้าใช้ auth.currentUser
จะเป็น null
มาเพิ่มการตรวจสอบกัน:
สาธารณะ/js/homepage.js
addToCart(id, itemData) {
// ADD THESE LINES
if (this.auth.currentUser === null) {
this.showError("You must be signed in!");
return;
}
// ...
}
ทดสอบแอป
ตอนนี้ รีเฟรช หน้า แล้วคลิก เพิ่มในรถเข็น คุณควรได้รับข้อผิดพลาดที่ดีกว่านี้:
แต่ถ้าคุณคลิก ลงชื่อเข้าใช้ ในแถบเครื่องมือด้านบนแล้วคลิก เพิ่มในรถเข็น อีกครั้ง คุณจะเห็นว่ามีการอัปเดตรถเข็นแล้ว
อย่างไรก็ตาม ดูเหมือนว่าตัวเลขจะไม่ถูกต้องเลย:
ไม่ต้องกังวล เราจะแก้ไขข้อผิดพลาดนั้นในไม่ช้า ขั้นแรก เรามาเจาะลึกสิ่งที่เกิดขึ้นจริงเมื่อคุณเพิ่มสินค้าลงในรถเข็นของคุณ
6. ทริกเกอร์ฟังก์ชั่นท้องถิ่น
การคลิก เพิ่มในรถเข็น เป็นการเริ่มเหตุการณ์ต่อเนื่องที่เกี่ยวข้องกับโปรแกรมจำลองหลายรายการ ในบันทึก Firebase CLI คุณควรเห็นข้อความต่อไปนี้หลังจากที่คุณเพิ่มสินค้าลงในรถเข็น:
i functions: Beginning execution of "calculateCart" i functions: Finished "calculateCart" in ~1s
มีเหตุการณ์สำคัญสี่เหตุการณ์ที่เกิดขึ้นเพื่อสร้างบันทึกเหล่านั้นและการอัปเดต UI ที่คุณสังเกตเห็น:
1) การเขียน Firestore - ลูกค้า
เอกสารใหม่ถูกเพิ่มไปยังคอลเลกชัน Firestore /carts/{cartId}/items/{itemId}/
คุณสามารถดูรหัสนี้ได้ในฟังก์ชัน addToCart
ภายใน public/js/homepage.js
:
สาธารณะ/js/homepage.js
addToCart(id, itemData) {
// ...
console.log("addToCart", id, JSON.stringify(itemData));
return this.db
.collection("carts")
.doc(this.auth.currentUser.uid)
.collection("items")
.doc(id)
.set(itemData);
}
2) เรียกใช้ฟังก์ชันคลาวด์
ฟังก์ชันคลาวด์ calculateCart
จะรับฟังเหตุการณ์การเขียน (สร้าง อัปเดต หรือลบ) ที่เกิดขึ้นกับสินค้าในรถเข็นโดยใช้ทริกเกอร์ onWrite
ซึ่งคุณสามารถดูได้ใน functions/index.js
:
ฟังก์ชั่น/index.js
exports.calculateCart = functions.firestore
.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
try {
let totalPrice = 125.98;
let itemCount = 8;
const cartRef = db.collection("carts").doc(context.params.cartId);
await cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
}
);
3) การเขียน Firestore - ผู้ดูแลระบบ
ฟังก์ชัน calculateCart
อ่านรายการทั้งหมดในรถเข็นและเพิ่มปริมาณและราคารวม จากนั้นจะอัปเดตเอกสาร "รถเข็น" ด้วยผลรวมใหม่ (ดูที่ cartRef.update(...)
ด้านบน)
4) อ่าน Firestore - ลูกค้า
ส่วนหน้าของเว็บได้รับการสมัครรับข้อมูลอัปเดตเกี่ยวกับการเปลี่ยนแปลงในรถเข็น ได้รับการอัปเดตตามเวลาจริงหลังจาก Cloud Function เขียนผลรวมใหม่และอัปเดต UI ดังที่คุณเห็นใน public/js/homepage.js
:
สาธารณะ/js/homepage.js
this.cartUnsub = cartRef.onSnapshot(cart => {
// The cart document was changed, update the UI
// ...
});
สรุป
ทำได้ดีมาก! คุณเพิ่งตั้งค่าแอปในเครื่องอย่างสมบูรณ์ซึ่งใช้ตัวจำลอง Firebase สามตัวสำหรับการทดสอบในเครื่องอย่างสมบูรณ์
แต่เดี๋ยวก่อน ยังมีอีก! ในส่วนถัดไป คุณจะได้เรียนรู้:
- วิธีเขียนการทดสอบหน่วยที่ใช้ Firebase Emulators
- วิธีใช้ Firebase Emulators เพื่อดีบักกฎความปลอดภัยของคุณ
7. สร้างกฎความปลอดภัยที่ปรับให้เหมาะกับแอปของคุณ
แอปพลิเคชันเว็บของเราอ่านและเขียนข้อมูล แต่จนถึงตอนนี้เราไม่ได้กังวลเรื่องความปลอดภัยเลย Cloud Firestore ใช้ระบบที่เรียกว่า "กฎความปลอดภัย" เพื่อประกาศว่าใครมีสิทธิ์เข้าถึงเพื่ออ่านและเขียนข้อมูล Emulator Suite เป็นวิธีที่ยอดเยี่ยมในการสร้างต้นแบบกฎเหล่านี้
ในเอดิเตอร์ ให้เปิดไฟล์ emulators-codelab/codelab-initial-state/firestore.rules
คุณจะเห็นว่าเรามีสามส่วนหลักในกฎของเรา:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// User's cart metadata
match /carts/{cartID} {
// TODO: Change these! Anyone can read or write.
allow read, write: if true;
}
// Items inside the user's cart
match /carts/{cartID}/items/{itemID} {
// TODO: Change these! Anyone can read or write.
allow read, write: if true;
}
// All items available in the store. Users can read
// items but never write them.
match /items/{itemID} {
allow read: if true;
}
}
}
ตอนนี้ใครๆ ก็สามารถอ่านและเขียนข้อมูลลงในฐานข้อมูลของเราได้! เราต้องการให้แน่ใจว่าการดำเนินการที่ถูกต้องเท่านั้นที่จะผ่านไปได้ และเราไม่รั่วไหลข้อมูลที่ละเอียดอ่อนใดๆ
ในระหว่าง Codelab นี้ ตามหลักการของสิทธิพิเศษน้อยที่สุด เราจะล็อกเอกสารทั้งหมดและค่อยๆ เพิ่มการเข้าถึงจนกว่าผู้ใช้ทั้งหมดจะมีสิทธิ์เข้าถึงทั้งหมดที่ต้องการ แต่ไม่มากไปกว่านี้ มาอัปเดตกฎสองข้อแรกเพื่อปฏิเสธการเข้าถึงโดยตั้งค่าเงื่อนไขเป็น false
:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// User's cart metadata
match /carts/{cartID} {
// UPDATE THIS LINE
allow read, write: if false;
}
// Items inside the user's cart
match /carts/{cartID}/items/{itemID} {
// UPDATE THIS LINE
allow read, write: if false;
}
// All items available in the store. Users can read
// items but never write them.
match /items/{itemID} {
allow read: if true;
}
}
}
8. เรียกใช้โปรแกรมจำลองและการทดสอบ
เริ่มโปรแกรมจำลอง
ในบรรทัดคำสั่ง ตรวจสอบให้แน่ใจว่าคุณอยู่ใน emulators-codelab/codelab-initial-state/
คุณอาจยังมีโปรแกรมจำลองทำงานจากขั้นตอนก่อนหน้านี้ ถ้าไม่เริ่มโปรแกรมจำลองอีกครั้ง:
$ firebase emulators:start --import=./seed
เมื่ออีมูเลเตอร์กำลังทำงาน คุณสามารถเรียกใช้การทดสอบในเครื่องกับพวกมันได้
เรียกใช้การทดสอบ
บนบรรทัดคำสั่ง ในแท็บเทอร์มินัลใหม่ จากไดเร็กทอรี emulators-codelab/codelab-initial-state/
ขั้นแรกให้ย้ายไปที่ไดเร็กทอรีของฟังก์ชัน (เราจะอยู่ที่นี่สำหรับโค้ดแล็บที่เหลือ):
$ cd functions
ตอนนี้รันการทดสอบมอคค่าในไดเร็กทอรี functions และเลื่อนไปที่ด้านบนสุดของผลลัพธ์:
# Run the tests $ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping carts 1) can be created and updated by the cart owner 2) can be read only by the cart owner shopping cart items 3) can be read only by the cart owner 4) can be added only by the cart owner adding an item to the cart recalculates the cart total. - should sum the cost of their items 0 passing (364ms) 1 pending 4 failing
ตอนนี้เรามีสี่ความล้มเหลว เมื่อคุณสร้างไฟล์กฎ คุณสามารถวัดความคืบหน้าได้โดยการดูการทดสอบเพิ่มเติม
9. การเข้าถึงรถเข็นอย่างปลอดภัย
ความล้มเหลวสองประการแรกคือการทดสอบ "ตะกร้าสินค้า" ซึ่งทดสอบว่า:
- ผู้ใช้สามารถสร้างและอัปเดตรถเข็นของตนเองเท่านั้น
- ผู้ใช้สามารถอ่านรถเข็นของตนเองเท่านั้น
ฟังก์ชัน/test.js
it('can be created and updated by the cart owner', async () => {
// Alice can create her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").set({
ownerUID: "alice",
total: 0
}));
// Bob can't create Alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart").set({
ownerUID: "alice",
total: 0
}));
// Alice can update her own cart with a new total
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").update({
total: 1
}));
// Bob can't update Alice's cart with a new total
await firebase.assertFails(bobDb.doc("carts/alicesCart").update({
total: 1
}));
});
it("can be read only by the cart owner", async () => {
// Setup: Create Alice's cart as admin
await admin.doc("carts/alicesCart").set({
ownerUID: "alice",
total: 0
});
// Alice can read her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").get());
// Bob can't read Alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart").get());
});
ขอให้ผ่านการทดสอบเหล่านี้ ในเอดิเตอร์ ให้เปิดไฟล์กฎความปลอดภัย firestore.rules
และอัพเดตคำสั่งภายใน match /carts/{cartID}
:
firestore.rules
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;
}
// ...
}
}
ขณะนี้กฎเหล่านี้อนุญาตเฉพาะการเข้าถึงแบบอ่านและเขียนโดยเจ้าของรถเข็น
ในการตรวจสอบข้อมูลขาเข้าและการรับรองความถูกต้องของผู้ใช้ เราใช้สองออบเจ็กต์ที่มีอยู่ในบริบทของทุกกฎ:
- วัตถุ
request
ประกอบด้วยข้อมูลและข้อมูลเมตาเกี่ยวกับการดำเนินการที่กำลังพยายาม - หากโปรเจ็กต์ Firebase ใช้ Firebase Authentication อ็อบเจ็กต์
request.auth
จะอธิบายถึงผู้ใช้ที่กำลังส่งคำขอ
10. ทดสอบการเข้าถึงรถเข็น
Emulator Suite จะอัปเดตกฎโดยอัตโนมัติทุกครั้งที่บันทึก firestore.rules
คุณสามารถยืนยันได้ว่าอีมูเลเตอร์ได้อัปเดตกฎแล้วโดยดูในแท็บที่รันอีมูเลเตอร์เพื่อหาข้อความ Rules updated
:
รันการทดสอบอีกครั้ง และตรวจสอบว่าการทดสอบสองรายการแรกผ่านแล้ว:
$ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping carts ✓ can be created and updated by the cart owner (195ms) ✓ can be read only by the cart owner (136ms) shopping cart items 1) can be read only by the cart owner 2) can be added only by the cart owner adding an item to the cart recalculates the cart total. - should sum the cost of their items 2 passing (482ms) 1 pending 2 failing
ดีมาก! ตอนนี้คุณได้เข้าถึงตะกร้าสินค้าอย่างปลอดภัยแล้ว ไปที่การทดสอบล้มเหลวครั้งต่อไปกันเถอะ
11. ตรวจสอบขั้นตอน "เพิ่มในรถเข็น" ใน UI
ขณะนี้ แม้ว่าเจ้าของรถเข็นจะอ่านและเขียนลงในรถเข็นของตน แต่ก็ไม่สามารถอ่านหรือเขียนสินค้าแต่ละรายการในรถเข็นของตนได้ นั่นเป็นเพราะแม้ว่าเจ้าของจะสามารถเข้าถึงเอกสารรถเข็นได้ แต่พวกเขาไม่มีสิทธิ์เข้าถึง คอลเลกชันย่อยของสินค้า ในรถเข็น
นี่เป็นสถานะที่ใช้งานไม่ได้สำหรับผู้ใช้
กลับไปที่ UI ของเว็บซึ่งทำงานบน http://127.0.0.1:5000,
และลองเพิ่มบางอย่างในรถเข็นของคุณ คุณได้รับข้อผิดพลาด Permission Denied
ซึ่งมองเห็นได้จากคอนโซลการแก้ปัญหา เนื่องจากเรายังไม่ได้อนุญาตให้ผู้ใช้เข้าถึงเอกสารที่สร้างขึ้นในคอลเลกชันย่อย items
12. อนุญาตให้เข้าถึงรายการรถเข็น
การทดสอบทั้งสองนี้ยืนยันว่าผู้ใช้สามารถเพิ่มสินค้าหรืออ่านสินค้าจากรถเข็นของตนเองได้เท่านั้น:
it("can be read only by the cart owner", async () => {
// Alice can read items in her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart/items/milk").get());
// Bob can't read items in alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart/items/milk").get())
});
it("can be added only by the cart owner", async () => {
// Alice can add an item to her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart/items/lemon").set({
name: "lemon",
price: 0.99
}));
// Bob can't add an item to alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart/items/lemon").set({
name: "lemon",
price: 0.99
}));
});
ดังนั้นเราจึงสามารถเขียนกฎที่อนุญาตให้เข้าถึงได้หากผู้ใช้ปัจจุบันมี UID เดียวกันกับ OwnerUID ในเอกสารรถเข็น เนื่องจากไม่จำเป็นต้องระบุกฎที่แตกต่างกันสำหรับ create, update, delete
คุณจึงสามารถใช้กฎ write
ซึ่งจะใช้กับคำขอทั้งหมดที่แก้ไขข้อมูล
อัปเดตกฎสำหรับเอกสารในคอลเลกชันย่อยของรายการ get
สู่เงื่อนไขคือการอ่านค่าจาก Firestore ในกรณีนี้คือ ownerUID
ในเอกสารรถเข็น
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ...
// UPDATE THESE LINES
match /carts/{cartID}/items/{itemID} {
allow read, write: if get(/databases/$(database)/documents/carts/$(cartID)).data.ownerUID == request.auth.uid;
}
// ...
}
}
13. ทดสอบการเข้าถึงรายการรถเข็น
ตอนนี้เราสามารถเรียกใช้การทดสอบใหม่ได้ เลื่อนไปที่ด้านบนสุดของผลลัพธ์และตรวจสอบว่าผ่านการทดสอบเพิ่มเติม:
$ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping carts ✓ can be created and updated by the cart owner (195ms) ✓ can be read only by the cart owner (136ms) shopping cart items ✓ can be read only by the cart owner (111ms) ✓ can be added only by the cart owner adding an item to the cart recalculates the cart total. - should sum the cost of their items 4 passing (401ms) 1 pending
ดี! ตอนนี้การทดสอบทั้งหมดของเราผ่าน เรามีการทดสอบหนึ่งรายการที่รอดำเนินการ แต่เราจะดำเนินการในอีกไม่กี่ขั้นตอน
14. ตรวจสอบขั้นตอน "หยิบใส่ตะกร้า" อีกครั้ง
กลับไปที่ส่วนหน้าของเว็บ ( http://127.0.0.1:5000 ) และเพิ่มสินค้าลงในรถเข็น นี่เป็นขั้นตอนสำคัญในการยืนยันว่าการทดสอบและกฎของเราตรงกับฟังก์ชันที่ลูกค้าต้องการ (โปรดจำไว้ว่าครั้งล่าสุดที่เราทดลองใช้ ผู้ใช้ UI ไม่สามารถเพิ่มสินค้าลงในรถเข็นได้!)
ไคลเอ็นต์โหลดกฎใหม่โดยอัตโนมัติเมื่อบันทึก firestore.rules
ลองเพิ่มสินค้าลงในรถเข็น
สรุป
ทำได้ดีมาก! คุณเพิ่งปรับปรุงความปลอดภัยของแอป ซึ่งเป็นขั้นตอนสำคัญในการเตรียมแอปให้พร้อมสำหรับการผลิต หากนี่เป็นแอปที่ใช้งานจริง เราสามารถเพิ่มการทดสอบเหล่านี้ไปยังไปป์ไลน์การผสานรวมอย่างต่อเนื่องของเรา สิ่งนี้จะทำให้เรามีความมั่นใจในอนาคตว่าข้อมูลตะกร้าสินค้าของเราจะมีการควบคุมการเข้าถึงเหล่านี้ แม้ว่าผู้อื่นจะแก้ไขกฎก็ตาม
แต่เดี๋ยวก่อน ยังมีอีก!
ถ้าคุณดำเนินการต่อไป คุณจะได้เรียนรู้:
- วิธีเขียนฟังก์ชันที่เรียกโดยเหตุการณ์ Firestore
- วิธีสร้างการทดสอบที่ใช้ได้กับหลายโปรแกรมจำลอง
15. ตั้งค่าการทดสอบ Cloud Functions
จนถึงตอนนี้ เราได้มุ่งเน้นไปที่ส่วนหน้าของเว็บแอปและกฎความปลอดภัยของ Firestore แต่แอปนี้ยังใช้ Cloud Functions เพื่อให้รถเข็นของผู้ใช้อัปเดตอยู่เสมอ ดังนั้นเราจึงต้องการทดสอบโค้ดนั้นด้วย
Emulator Suite ช่วยให้การทดสอบ Cloud Functions เป็นเรื่องง่าย แม้แต่ฟังก์ชันที่ใช้ Cloud Firestore และบริการอื่นๆ
ในโปรแกรมแก้ไข ให้เปิดไฟล์ emulators-codelab/codelab-initial-state/functions/test.js
และเลื่อนไปที่การทดสอบล่าสุดในไฟล์ ขณะนี้ มันถูกทำเครื่องหมายว่ารอดำเนินการ:
// REMOVE .skip FROM THIS LINE
describe.skip("adding an item to the cart recalculates the cart total. ", () => {
// ...
it("should sum the cost of their items", async () => {
...
});
});
หากต้องการเปิดใช้งานการทดสอบ ให้ลบ .skip
เพื่อให้มีลักษณะดังนี้:
describe("adding an item to the cart recalculates the cart total. ", () => {
// ...
it("should sum the cost of their items", async () => {
...
});
});
จากนั้น ค้นหาตัวแปร REAL_FIREBASE_PROJECT_ID
ที่ด้านบนของไฟล์ และเปลี่ยนเป็นรหัสโครงการ Firebase จริงของคุณ:
// CHANGE THIS LINE
const REAL_FIREBASE_PROJECT_ID = "changeme";
หากคุณลืมรหัสโครงการ คุณสามารถค้นหารหัสโครงการ Firebase ได้ในการตั้งค่าโครงการในคอนโซล Firebase:
16. เดินผ่านการทดสอบฟังก์ชั่น
เนื่องจากการทดสอบนี้ตรวจสอบการโต้ตอบระหว่าง Cloud Firestore และ Cloud Functions จึงต้องมีการตั้งค่ามากกว่าการทดสอบใน Codelabs ก่อนหน้า มาดูการทดสอบนี้และทำความเข้าใจกับสิ่งที่คาดหวัง
สร้างรถเข็น
Cloud Functions ทำงานในสภาพแวดล้อมเซิร์ฟเวอร์ที่เชื่อถือได้ และสามารถใช้การตรวจสอบบัญชีบริการที่ใช้โดย Admin SDK ขั้นแรก ให้คุณเริ่มต้นแอปโดยใช้ initializeAdminApp
แทน initializeApp
จากนั้น คุณสร้าง DocumentReference สำหรับรถเข็นที่เราจะเพิ่มสินค้าและเริ่มต้นรถเข็น:
it("should sum the cost of their items", async () => {
const db = firebase
.initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
.firestore();
// Setup: Initialize cart
const aliceCartRef = db.doc("carts/alice")
await aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });
...
});
เรียกใช้ฟังก์ชัน
จากนั้นเพิ่มเอกสารไปยัง items
ย่อยของเอกสารรถเข็นของเราเพื่อเรียกใช้ฟังก์ชัน เพิ่มสองรายการเพื่อให้แน่ใจว่าคุณกำลังทดสอบการเพิ่มที่เกิดขึ้นในฟังก์ชัน
it("should sum the cost of their items", async () => {
const db = firebase
.initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
.firestore();
// Setup: Initialize cart
const aliceCartRef = db.doc("carts/alice")
await aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });
// Trigger calculateCart by adding items to the cart
const aliceItemsRef = aliceCartRef.collection("items");
await aliceItemsRef.doc("doc1").set({name: "nectarine", price: 2.99});
await aliceItemsRef.doc("doc2").set({ name: "grapefruit", price: 6.99 });
...
});
});
กำหนดความคาดหวังในการทดสอบ
ใช้ onSnapshot()
เพื่อลงทะเบียนผู้ฟังสำหรับการเปลี่ยนแปลงใดๆ ในเอกสารรถเข็น onSnapshot()
ส่งคืนฟังก์ชันที่คุณสามารถเรียกเพื่อยกเลิกการลงทะเบียนผู้ฟัง
สำหรับการทดสอบนี้ ให้เพิ่มสินค้าสองรายการที่มีราคารวมกัน $9.98 จากนั้นตรวจสอบว่ารถเข็นมี itemCount
และ totalPrice
ที่คาดไว้หรือไม่ ถ้าเป็นเช่นนั้น ฟังก์ชันก็ทำหน้าที่ของมัน
it("should sum the cost of their items", (done) => {
const db = firebase
.initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
.firestore();
// Setup: Initialize cart
const aliceCartRef = db.doc("carts/alice")
aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });
// Trigger calculateCart by adding items to the cart
const aliceItemsRef = aliceCartRef.collection("items");
aliceItemsRef.doc("doc1").set({name: "nectarine", price: 2.99});
aliceItemsRef.doc("doc2").set({ name: "grapefruit", price: 6.99 });
// Listen for every update to the cart. Every time an item is added to
// the cart's subcollection of items, the function updates `totalPrice`
// and `itemCount` attributes on the cart.
// Returns a function that can be called to unsubscribe the listener.
await new Promise((resolve) => {
const unsubscribe = aliceCartRef.onSnapshot(snap => {
// If the function worked, these will be cart's final attributes.
const expectedCount = 2;
const expectedTotal = 9.98;
// When the `itemCount`and `totalPrice` match the expectations for the
// two items added, the promise resolves, and the test passes.
if (snap.data().itemCount === expectedCount && snap.data().totalPrice == expectedTotal) {
// Call the function returned by `onSnapshot` to unsubscribe from updates
unsubscribe();
resolve();
};
});
});
});
});
17. เรียกใช้การทดสอบ
คุณอาจยังมีโปรแกรมจำลองที่ทำงานจากการทดสอบก่อนหน้านี้ ถ้าไม่เริ่มโปรแกรมจำลอง จากบรรทัดคำสั่ง ให้เรียกใช้
$ firebase emulators:start --import=./seed
เปิด แท็บเทอร์มินัลใหม่ (ปล่อยให้อีมูเลเตอร์ทำงานอยู่) และย้ายไปยังไดเร็กทอรีของฟังก์ชัน คุณอาจยังเปิดสิ่งนี้อยู่จากการทดสอบกฎความปลอดภัย
$ cd functions
ตอนนี้เรียกใช้การทดสอบหน่วย คุณควรเห็นการทดสอบทั้งหมด 5 รายการ:
$ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping cart creation ✓ can be created by the cart owner (82ms) shopping cart reads, updates, and deletes ✓ cart can be read by the cart owner (42ms) shopping cart items ✓ items can be read by the cart owner (40ms) ✓ items can be added by the cart owner adding an item to the cart recalculates the cart total. 1) should sum the cost of their items 4 passing (2s) 1 failing
หากคุณดูที่ความล้มเหลวที่เฉพาะเจาะจง ดูเหมือนว่าจะเป็นข้อผิดพลาดการหมดเวลา นี่เป็นเพราะการทดสอบกำลังรอให้ฟังก์ชันอัปเดตอย่างถูกต้อง แต่ก็ไม่เคยทำ ตอนนี้เราพร้อมที่จะเขียนฟังก์ชันเพื่อตอบสนองการทดสอบแล้ว
18. เขียนฟังก์ชัน
หากต้องการแก้ไขการทดสอบนี้ คุณต้องอัปเดตฟังก์ชันใน functions/index.js
แม้ว่าจะเขียนฟังก์ชันนี้ไว้บางส่วนแต่ยังไม่สมบูรณ์ นี่คือลักษณะของฟังก์ชันในปัจจุบัน:
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
let totalPrice = 125.98;
let itemCount = 8;
try {
const cartRef = db.collection("carts").doc(context.params.cartId);
await cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
});
ฟังก์ชันตั้งค่าการอ้างอิงรถเข็นอย่างถูกต้อง แต่แทนที่จะคำนวณค่าของ totalPrice
และ itemCount
ฟังก์ชันจะอัปเดตเป็นค่าฮาร์ดโค้ด
ดึงข้อมูลและวนซ้ำผ่าน
คอลเลกชันย่อย items
เริ่มต้นค่าคงที่ใหม่ itemsSnap
เพื่อเป็นคอลเลกชันย่อย items
จากนั้น วนซ้ำเอกสารทั้งหมดในคอลเลกชัน
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
try {
let totalPrice = 125.98;
let itemCount = 8;
const cartRef = db.collection("carts").doc(context.params.cartId);
// ADD LINES FROM HERE
const itemsSnap = await cartRef.collection("items").get();
itemsSnap.docs.forEach(item => {
const itemData = item.data();
})
// TO HERE
return cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
});
คำนวณราคารวมและจำนวนรายการ
ขั้นแรก ให้เริ่มต้นค่าของ totalPrice
และ itemCount
เป็นศูนย์
จากนั้นเพิ่มตรรกะลงในบล็อกการวนซ้ำของเรา ก่อนอื่นให้ตรวจสอบว่ารายการนั้นมีราคา หากสินค้าไม่ได้ระบุปริมาณ ให้ตั้งค่าเริ่มต้นเป็น 1
จากนั้นเพิ่มจำนวนให้กับผลรวมของ itemCount
สุดท้าย เพิ่มราคาของสินค้าคูณด้วยจำนวนไปยังผลรวมของ totalPrice
:
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
try {
// CHANGE THESE LINES
let totalPrice = 0;
let itemCount = 0;
const cartRef = db.collection("carts").doc(context.params.cartId);
const itemsSnap = await cartRef.collection("items").get();
itemsSnap.docs.forEach(item => {
const itemData = item.data();
// ADD LINES FROM HERE
if (itemData.price) {
// If not specified, the quantity is 1
const quantity = itemData.quantity ? itemData.quantity : 1;
itemCount += quantity;
totalPrice += (itemData.price * quantity);
}
// TO HERE
})
await cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
});
คุณยังสามารถเพิ่มการบันทึกเพื่อช่วยดีบักสถานะความสำเร็จและข้อผิดพลาด:
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
let totalPrice = 0;
let itemCount = 0;
try {
const cartRef = db.collection("carts").doc(context.params.cartId);
const itemsSnap = await cartRef.collection("items").get();
itemsSnap.docs.forEach(item => {
const itemData = item.data();
if (itemData.price) {
// If not specified, the quantity is 1
const quantity = (itemData.quantity) ? itemData.quantity : 1;
itemCount += quantity;
totalPrice += (itemData.price * quantity);
}
});
await cartRef.update({
totalPrice,
itemCount
});
// OPTIONAL LOGGING HERE
console.log("Cart total successfully recalculated: ", totalPrice);
} catch(err) {
// OPTIONAL LOGGING HERE
console.warn("update error", err);
}
});
19. ทดสอบซ้ำ
ในบรรทัดคำสั่ง ตรวจสอบให้แน่ใจว่าโปรแกรมจำลองยังคงทำงานอยู่และรันการทดสอบอีกครั้ง คุณไม่จำเป็นต้องรีสตาร์ทอีมูเลเตอร์ เนื่องจากอีมูเลเตอร์จะรับการเปลี่ยนแปลงในฟังก์ชันโดยอัตโนมัติ คุณควรเห็นการทดสอบทั้งหมดผ่าน:
$ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping cart creation ✓ can be created by the cart owner (306ms) shopping cart reads, updates, and deletes ✓ cart can be read by the cart owner (59ms) shopping cart items ✓ items can be read by the cart owner ✓ items can be added by the cart owner adding an item to the cart recalculates the cart total. ✓ should sum the cost of their items (800ms) 5 passing (1s)
ดีมาก!
20. ลองใช้โดยใช้ Storefront UI
สำหรับการทดสอบขั้นสุดท้าย ให้กลับไปที่เว็บแอป ( http://127.0.0.1:5000/ ) และเพิ่มสินค้าลงในรถเข็น
ยืนยันว่ารถเข็นอัปเดตด้วยยอดรวมที่ถูกต้อง มหัศจรรย์!
สรุป
คุณได้ผ่านกรณีทดสอบที่ซับซ้อนระหว่าง Cloud Functions สำหรับ Firebase และ Cloud Firestore แล้ว คุณเขียน Cloud Function เพื่อให้การทดสอบผ่าน คุณยังยืนยันด้วยว่าฟังก์ชันใหม่ทำงานใน UI! คุณทำทั้งหมดนี้ในเครื่อง เรียกใช้โปรแกรมจำลองบนเครื่องของคุณเอง
คุณยังสร้างเว็บไคลเอ็นต์ที่ทำงานกับโปรแกรมจำลองในเครื่อง ปรับแต่งกฎความปลอดภัยเพื่อปกป้องข้อมูล และทดสอบกฎความปลอดภัยโดยใช้โปรแกรมจำลองในเครื่อง