การพัฒนาท้องถิ่นด้วย Firebase Emulator Suite

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 เพื่อเปิดใช้งานการพัฒนาในท้องถิ่น

2589e2f95b74fa88.png

คุณยังจะได้เรียนรู้วิธีการ:

  • วิธีเชื่อมต่อแอปของคุณกับ 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

หน้าจอหลักของ Emulators UI

คลิกเพื่อดู UI สำหรับ Firestore Emulator คอลเลกชัน items มีข้อมูลอยู่แล้วเนื่องจากข้อมูลที่นำเข้าด้วยแฟล็ก --import

4ef88d0148405d36.png

4. เรียกใช้แอป

เปิดแอป

ในเว็บเบราว์เซอร์ของคุณ ให้ไปที่ http://127.0.0.1:5000 และคุณจะเห็น The Fire Store ทำงานอยู่ในเครื่องของคุณ!

939f87946bac2ee4.png

ใช้แอพ

เลือกสินค้าในหน้าแรกแล้วคลิก เพิ่มลงตะกร้า ขออภัย คุณจะพบกับข้อผิดพลาดต่อไปนี้:

a11bd59933a8e885.png

มาแก้ไขข้อผิดพลาดกันเถอะ! เนื่องจากทุกอย่างทำงานในโปรแกรมจำลอง เราจึงสามารถทดลองได้โดยไม่ต้องกังวลว่าจะส่งผลกระทบต่อข้อมูลจริง

5. ดีบักแอป

ค้นหาจุดบกพร่อง

โอเค มาดูในคอนโซลนักพัฒนาซอฟต์แวร์ Chrome กัน กด Control+Shift+J (Windows, Linux, Chrome OS) หรือ Command+Option+J (Mac) เพื่อดูข้อผิดพลาดบนคอนโซล:

74c45df55291dab1.png

ดูเหมือนว่ามีข้อผิดพลาดบางอย่างในวิธีการ 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;
    }

    // ...
  }

ทดสอบแอป

ตอนนี้ รีเฟรช เพจแล้วคลิก เพิ่มลงรถเข็น คุณควรได้รับข้อผิดพลาดที่ดีกว่าในครั้งนี้:

c65f6c05588133f7.png

แต่ถ้าคุณคลิก ลงชื่อเข้าใช้ ในแถบเครื่องมือด้านบนแล้วคลิก เพิ่มลงตะกร้า อีกครั้ง คุณจะเห็นว่าตะกร้าสินค้าได้รับการอัปเดตแล้ว

อย่างไรก็ตาม ดูเหมือนว่าตัวเลขจะไม่ถูกต้องเลย:

239f26f02f959eef.png

ไม่ต้องกังวล เราจะแก้ไขข้อบกพร่องนั้นเร็วๆ นี้ ขั้นแรก มาเจาะลึกถึงสิ่งที่เกิดขึ้นจริงเมื่อคุณเพิ่มสินค้าลงในรถเข็นของคุณ

6. ทริกเกอร์ฟังก์ชันท้องถิ่น

การคลิก เพิ่มลงตะกร้า จะเป็นการเริ่มต้นเหตุการณ์ต่อเนื่องที่เกี่ยวข้องกับโปรแกรมจำลองหลายตัว ในบันทึก Firebase CLI คุณควรเห็นข้อความคล้าย ๆ ต่อไปนี้หลังจากที่คุณเพิ่มสินค้าลงในรถเข็น:

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

มีเหตุการณ์สำคัญสี่เหตุการณ์ที่เกิดขึ้นเพื่อสร้างบันทึกเหล่านั้นและการอัปเดต UI ที่คุณสังเกตเห็น:

68c9323f2ad10f7a.png

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 รายการสำหรับการทดสอบในเครื่องโดยสมบูรณ์

db82eef1706c9058.gif

แต่เดี๋ยวก่อนยังมีอีกมาก! ในส่วนถัดไป คุณจะได้เรียนรู้:

  • วิธีเขียนการทดสอบหน่วยที่ใช้ 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 :

5680da418b420226.png

รันการทดสอบอีกครั้ง และตรวจสอบว่าการทดสอบสองรายการแรกผ่านแล้ว:

$ 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 ผู้ใช้ไม่สามารถเพิ่มสินค้าลงในรถเข็นได้!)

69ad26cee520bf24.png

ไคลเอ็นต์จะโหลดกฎซ้ำโดยอัตโนมัติเมื่อมีการบันทึก firestore.rules ดังนั้น ลองเพิ่มสินค้าลงในรถเข็น

สรุป

ทำได้ดีมาก! คุณเพิ่งปรับปรุงความปลอดภัยของแอป ซึ่งเป็นขั้นตอนสำคัญในการเตรียมพร้อมสำหรับการผลิต! หากนี่เป็นแอปที่ใช้งานจริง เราก็สามารถเพิ่มการทดสอบเหล่านี้ไปยังไปป์ไลน์การรวมอย่างต่อเนื่องของเราได้ สิ่งนี้จะทำให้เรามั่นใจในอนาคตว่าข้อมูลรถเข็นช็อปปิ้งของเราจะมีการควบคุมการเข้าถึงเหล่านี้ แม้ว่าผู้อื่นจะแก้ไขกฎก็ตาม

ba5440b193e75967.gif

แต่เดี๋ยวก่อนยังมีอีกมาก!

หากคุณดำเนินการต่อ คุณจะได้เรียนรู้:

  • วิธีเขียนฟังก์ชันที่ถูกกระตุ้นโดยเหตุการณ์ 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:

d6d0429b700d2b21.png

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/ ) และเพิ่มรายการลงในรถเข็น

69ad26cee520bf24.png

ยืนยันว่ารถเข็นอัปเดตด้วยยอดรวมที่ถูกต้อง มหัศจรรย์!

สรุป

คุณได้ผ่านกรณีทดสอบที่ซับซ้อนระหว่าง Cloud Functions สำหรับ Firebase และ Cloud Firestore แล้ว คุณเขียน Cloud Function เพื่อให้การทดสอบผ่าน คุณยังยืนยันด้วยว่าฟังก์ชันใหม่ทำงานใน UI! คุณทำทั้งหมดนี้ในเครื่อง โดยรันโปรแกรมจำลองบนเครื่องของคุณเอง

คุณยังได้สร้างเว็บไคลเอ็นต์ที่ทำงานกับโปรแกรมจำลองในเครื่อง ปรับแต่งกฎความปลอดภัยเพื่อปกป้องข้อมูล และทดสอบกฎความปลอดภัยโดยใช้โปรแกรมจำลองในเครื่อง

c6a7aeb91fe97a64.gif