การพัฒนาในพื้นที่ด้วย 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 ที่ปรับขนาดได้ทั่วโลก ไร้เซิร์ฟเวอร์ พร้อมความสามารถแบบเรียลไทม์
  • Cloud Functions : โค้ดแบ็กเอนด์แบบไร้เซิร์ฟเวอร์ที่ทำงานเพื่อตอบสนองต่อเหตุการณ์หรือคำขอ 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://localhost:5000
i  ui: Emulator UI logging to ui-debug.log
i  functions: Watching "/Users/samstern/Projects/emulators-codelab/codelab-initial-state/functions" for Cloud Functions...
✔  functions[calculateCart]: firestore function initialized.

┌─────────────────────────────────────────────────────────────┐
│ ✔  All emulators ready! It is now safe to connect your app. │
│ i  View Emulator UI at http://localhost:4000                │
└─────────────────────────────────────────────────────────────┘

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

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

เมื่อคุณเห็นข้อความ เริ่มต้นของอีมูเลเตอร์ทั้งหมด แอปก็พร้อมใช้งาน

เชื่อมต่อเว็บแอปกับอีมูเลเตอร์

จากตารางในบันทึกเราจะเห็นว่าโปรแกรมจำลอง Cloud Firestore กำลังฟังพอร์ต 8080 และโปรแกรมจำลองการตรวจสอบสิทธิ์กำลังฟังพอร์ต 9099

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

มาเชื่อมต่อโค้ดส่วนหน้าของคุณกับอีมูเลเตอร์ แทนที่จะเชื่อมต่อกับเวอร์ชันที่ใช้งานจริง เปิดไฟล์ 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 === "localhost") {
    console.log("localhost detected!");
    auth.useEmulator("http://localhost:9099");
    db.useEmulator("localhost", 8080);
  }

ตอนนี้เมื่อแอปทำงานบน localhost (ให้บริการโดย Hosting emulator) ไคลเอนต์ Firestore ยังชี้ไปที่ตัวจำลองในเครื่องมากกว่าที่ฐานข้อมูลที่ใช้งานจริง

เปิด EmulatorUI

ในเว็บเบราว์เซอร์ของคุณ ให้ไปที่ http://localhost:4000/ คุณควรเห็น UI ของ Emulator Suite

หน้าจอหลักของอีมูเลเตอร์ UI

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

4ef88d0148405d36.png

4. เปิดแอพ

เปิดแอป

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

939f87946bac2ee4.png

ใช้แอพ

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

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 เมื่อเราไม่ได้ลงชื่อเข้าใช้ 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;
    }

    // ...
  }

ทดสอบแอพ

ตอนนี้ รีเฟรช หน้าแล้วคลิก Add to Cart คุณควรได้รับข้อผิดพลาดที่ดีกว่าในครั้งนี้:

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) เรียกใช้ฟังก์ชันคลาวด์

ฟังก์ชัน Cloud calculateCart Cart รับฟังเหตุการณ์การเขียนใดๆ (สร้าง อัปเดต หรือลบ) ที่เกิดขึ้นกับรายการรถเข็นโดยใช้ทริกเกอร์ 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 จะอ่านรายการทั้งหมดในรถเข็นและเพิ่มปริมาณและราคาทั้งหมด จากนั้นจะอัปเดตเอกสาร "cart" ด้วยยอดรวมใหม่ (ดู 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 ที่แตกต่างกันสามตัวสำหรับการทดสอบในเครื่องโดยสมบูรณ์

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 นี้ ตามหลักการของ Least Privilege เราจะล็อกเอกสารทั้งหมดและค่อยๆ เพิ่มการเข้าถึงจนกว่าผู้ใช้ทั้งหมดจะมีสิทธิ์เข้าถึงที่จำเป็นทั้งหมด แต่ไม่มากไปกว่านั้น มาอัปเดตกฎสองข้อแรกเพื่อปฏิเสธการเข้าถึงโดยตั้งค่าเงื่อนไข 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/

ขั้นแรกให้ย้ายไปที่ไดเร็กทอรี functions (เราจะอยู่ที่นี่ตลอดช่วงที่เหลือของ codelab):

$ cd functions

ตอนนี้ให้รันการทดสอบ mocha ในไดเร็กทอรี 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://localhost: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://localhost:5000 ) และเพิ่มรายการลงในรถเข็น นี่เป็นขั้นตอนสำคัญในการยืนยันว่าการทดสอบและกฎของเราตรงกับฟังก์ชันที่ลูกค้าต้องการ (จำไว้ว่าครั้งสุดท้ายที่เราลองใช้ผู้ใช้ UI ไม่สามารถเพิ่มสินค้าลงในรถเข็นได้!)

69ad26cee520bf24.png

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

สรุป

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

ba5440b193e75967.gif

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

ถ้าคุณทำต่อไป คุณจะได้เรียนรู้:

  • วิธีเขียนฟังก์ชันที่ทริกเกอร์โดยเหตุการณ์ 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 Project ID จริงของคุณ:

// CHANGE THIS LINE
const REAL_FIREBASE_PROJECT_ID = "changeme";

หากคุณลืมรหัสโปรเจ็กต์ คุณสามารถค้นหารหัสโปรเจ็กต์ Firebase ได้ในการตั้งค่าโปรเจ็กต์ในคอนโซล Firebase:

d6d0429b700d2b21.png

16. เดินผ่านการทดสอบฟังก์ชั่น

เนื่องจากการทดสอบนี้ตรวจสอบการโต้ตอบระหว่าง Cloud Firestore และ Cloud Functions จึงมีการตั้งค่ามากกว่าการทดสอบใน Codelab ก่อนหน้านี้ มาดูการทดสอบนี้กันและทำความเข้าใจกับสิ่งที่คาดหวัง

สร้างรถเข็น

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() เพื่อลงทะเบียน Listener สำหรับการเปลี่ยนแปลงใดๆ ในเอกสารรถเข็น 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. ลองใช้ UI หน้าร้าน

สำหรับการทดสอบครั้งสุดท้าย ให้กลับไปที่เว็บแอป ( http://localhost:5000/ ) และเพิ่มรายการลงในรถเข็น

69ad26cee520bf24.png

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

สรุป

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

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

c6a7aeb91fe97a64.gif