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 แบบไร้เซิร์ฟเวอร์ที่ปรับขนาดได้ทั่วโลกพร้อมความสามารถแบบเรียลไทม์
- ฟังก์ชันคลาวด์ : โค้ดแบ็กเอนด์แบบไร้เซิร์ฟเวอร์ที่ทำงานเพื่อตอบสนองต่อเหตุการณ์หรือคำขอ 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 Authentication เมื่อเราไม่ได้ลงชื่อเข้าใช้ 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 ที่แตกต่างกัน 3 รายการสำหรับการทดสอบในเครื่องโดยสมบูรณ์
แต่เดี๋ยวก่อนยังมีอีกมาก! ในส่วนถัดไป คุณจะได้เรียนรู้:
- วิธีเขียนการทดสอบหน่วยที่ใช้ 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/
ขั้นแรกให้ย้ายไปยังไดเร็กทอรีฟังก์ชั่น (เราจะอยู่ที่นี่ตลอดส่วนที่เหลือของ codelab):
$ cd 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. ตั้งค่าการทดสอบฟังก์ชั่นคลาวด์
จนถึงตอนนี้เราได้มุ่งเน้นไปที่ส่วนหน้าของเว็บแอปและกฎความปลอดภัยของ Firestore แต่แอปนี้ยังใช้ฟังก์ชันคลาวด์เพื่อให้รถเข็นของผู้ใช้อัปเดตอยู่เสมอ ดังนั้นเราจึงต้องการทดสอบโค้ดนั้นด้วย
Emulator Suite ทำให้การทดสอบฟังก์ชันคลาวด์เป็นเรื่องง่าย แม้แต่ฟังก์ชันที่ใช้ 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 และฟังก์ชันคลาวด์ จึงมีการตั้งค่ามากกว่าการทดสอบใน Codelab ก่อนหน้า มาดูการทดสอบนี้และทำความเข้าใจว่าคาดหวังอะไรได้บ้าง
สร้างรถเข็น
ฟังก์ชันคลาวด์ทำงานในสภาพแวดล้อมเซิร์ฟเวอร์ที่เชื่อถือได้ และสามารถใช้การตรวจสอบบัญชีบริการที่ใช้โดย 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()
เพื่อลงทะเบียน Listener สำหรับการเปลี่ยนแปลงใดๆ ในเอกสารรถเข็น onSnapshot()
ส่งคืนฟังก์ชันที่คุณสามารถเรียกใช้เพื่อยกเลิกการลงทะเบียน Listener
สำหรับการทดสอบนี้ ให้เพิ่มสินค้า 2 ชิ้นที่มีราคารวมกัน 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. ลองใช้ UI หน้าร้าน
สำหรับการทดสอบขั้นสุดท้าย ให้กลับไปที่เว็บแอป ( http://127.0.0.1:5000/ ) และเพิ่มรายการลงในรถเข็น
ยืนยันว่ารถเข็นอัปเดตด้วยยอดรวมที่ถูกต้อง มหัศจรรย์!
สรุป
คุณได้ผ่านกรณีทดสอบที่ซับซ้อนระหว่าง Cloud Functions สำหรับ Firebase และ Cloud Firestore แล้ว คุณเขียน Cloud Function เพื่อให้การทดสอบผ่าน คุณยังยืนยันด้วยว่าฟังก์ชันใหม่ทำงานใน UI! คุณทำทั้งหมดนี้ในเครื่องโดยรันโปรแกรมจำลองบนเครื่องของคุณเอง
คุณยังได้สร้างเว็บไคลเอ็นต์ที่ทำงานกับโปรแกรมจำลองในเครื่อง ปรับแต่งกฎความปลอดภัยเพื่อปกป้องข้อมูล และทดสอบกฎความปลอดภัยโดยใช้โปรแกรมจำลองในเครื่อง