تطوير محلي باستخدام "حزمة محاكي Firebase"

التطوير المحلي باستخدام "مجموعة أدوات المحاكاة" في Firebase

لمحة عن هذا الدرس التطبيقي حول الترميز

subjectتاريخ التعديل الأخير: سبتمبر 15, 2025
account_circleتأليف: Sam Stern, Rachel Myers

1. قبل البدء

أدوات الخلفية بدون خادم، مثل Cloud Firestore وCloud Functions، سهلة الاستخدام للغاية، ولكن قد يكون من الصعب اختبارها. تتيح لك "مجموعة أدوات المحاكاة المحلية لـ Firebase" تشغيل إصدارات محلية من هذه الخدمات على جهاز التطوير حتى تتمكّن من تطوير تطبيقك بسرعة وأمان.

المتطلبات الأساسية

  • محرّر بسيط، مثل Visual Studio Code أو Atom أو Sublime Text
  • ‫Node.js 10.0.0 أو إصدار أحدث (لتثبيت Node.js، استخدِم nvm، وللتحقّق من إصدارك، شغِّل node --version)
  • ‫Java 7 أو إصدار أحدث (لتثبيت Java، اتّبِع هذه التعليمات، وللتحقّق من إصدارك، شغِّل java -version)

المهام التي ستنفذها

في هذا الدرس التطبيقي حول الترميز، ستشغّل تطبيقًا بسيطًا للتسوّق على الإنترنت وتصحّح أخطاءه، وهو تطبيق يستند إلى خدمات Firebase المتعدّدة:

  • ‫Cloud Firestore: هي قاعدة بيانات NoSQL بدون خادم وقابلة للتوسّع على مستوى العالم وتتضمّن إمكانات في الوقت الفعلي.
  • Cloud Functions: هي رمز خلفي بدون خادم يتم تنفيذه استجابةً للأحداث أو طلبات HTTP.
  • مصادقة Firebase: هي خدمة مصادقة مُدارة تتكامل مع منتجات Firebase الأخرى.
  • استضافة Firebase: استضافة سريعة وآمنة لتطبيقات الويب

عليك ربط التطبيق بـ Emulator Suite لتفعيل التطوير على الجهاز.

2589e2f95b74fa88.png

ستتعرّف أيضًا على كيفية:

  • كيفية ربط تطبيقك بـ Emulator Suite وكيفية ربط المحاكيات المختلفة
  • كيفية عمل "قواعد الأمان" في Firebase وكيفية اختبار "قواعد الأمان" في Firestore باستخدام محاكي محلي
  • كيفية كتابة دالة Firebase يتم تشغيلها من خلال أحداث Firestore وكيفية كتابة اختبارات تكامل يتم تنفيذها على "مجموعة المحاكي"

2. إعداد

الحصول على رمز المصدر

في هذا الدرس العملي، ستبدأ بنسخة من نموذج The Fire Store شبه مكتملة، لذا أول ما عليك فعله هو استنساخ رمز المصدر:

$ git clone https://github.com/firebase/emulators-codelab.git

بعد ذلك، انتقِل إلى دليل 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

تشكّل "مجموعة أدوات المحاكاة" جزءًا من Firebase CLI (واجهة سطر الأوامر) التي يمكن تثبيتها على جهازك باستخدام الأمر التالي:

$ npm install -g firebase-tools

بعد ذلك، تأكَّد من تثبيت أحدث إصدار من واجهة سطر الأوامر. من المفترض أن يعمل هذا الدرس العملي مع الإصدار 9.0.0 أو الإصدارات الأحدث، ولكن تتضمّن الإصدارات الأحدث المزيد من إصلاحات الأخطاء.

$ firebase --version
9.6.0

الربط بمشروعك على Firebase

إنشاء مشروع Firebase

  1. سجِّل الدخول إلى وحدة تحكّم Firebase باستخدام حسابك على Google.
  2. انقر على الزر لإنشاء مشروع جديد، ثم أدخِل اسم المشروع (على سبيل المثال، Emulators Codelab).
  3. انقر على متابعة.
  4. إذا طُلب منك ذلك، راجِع بنود Firebase واقبلها، ثم انقر على متابعة.
  5. (اختياري) فعِّل ميزة "المساعدة المستندة إلى الذكاء الاصطناعي" في وحدة تحكّم Firebase (المعروفة باسم "Gemini في Firebase").
  6. في هذا الدرس العملي، لا تحتاج إلى "إحصاءات Google"، لذا أوقِف خيار "إحصاءات Google".
  7. انقر على إنشاء مشروع، وانتظِر إلى أن يتم توفير مشروعك، ثم انقر على متابعة.

ربط الرمز البرمجي بمشروعك على Firebase

الآن، علينا ربط هذا الرمز بمشروعك على Firebase. نفِّذ الأمر التالي أولاً لتسجيل الدخول إلى Firebase CLI:

$ firebase login

بعد ذلك، شغِّل الأمر التالي لإنشاء اسم مستعار للمشروع. استبدِل $YOUR_PROJECT_ID بمعرّف مشروعك على Firebase.

$ firebase use $YOUR_PROJECT_ID

أنت الآن جاهز لتشغيل التطبيق.

3. تشغيل المحاكيات

في هذا القسم، ستشغّل التطبيق محليًا. وهذا يعني أنّه حان الوقت لتشغيل Emulator Suite.

بدء المحاكيات

من داخل دليل مصدر الدرس العملي، نفِّذ الأمر التالي لبدء المحاكيات:

$ 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 وأنّ محاكي Authentication يستمع إلى المنفذ 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 العادية:

public/js/homepage.js

  const auth = firebaseApp.auth();
  const db = firebaseApp.firestore();

لنعدّل العنصرَين db وauth للإشارة إلى المحاكيات المحلية:

public/js/homepage.js

  const auth = firebaseApp.auth();
  const db = firebaseApp.firestore();

  // ADD THESE LINES
  if (location.hostname === "127.0.0.1") {
    console.log("127.0.0.1 detected!");
    auth.useEmulator("http://127.0.0.1:9099");
    db.useEmulator("127.0.0.1", 8080);
  }

عند تشغيل التطبيق على جهازك المحلي (الذي يوفّره محاكي Hosting)، يشير عميل Firestore أيضًا إلى المحاكي المحلي بدلاً من قاعدة بيانات الإنتاج.

فتح EmulatorUI

في متصفّح الويب، انتقِل إلى http://127.0.0.1:4000/‎. من المفترض أن تظهر لك واجهة مستخدم Emulator Suite.

الشاشة الرئيسية لواجهة المستخدم في المحاكي

انقر لعرض واجهة مستخدم "محاكي Firestore". تحتوي المجموعة 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 أو ChromeOS) أو Command+Option+J (في أجهزة Mac) للاطّلاع على الخطأ في وحدة التحكّم:

74c45df55291dab1.png

يبدو أنّه حدث خطأ في طريقة addToCart، لنلقِ نظرة على ذلك. أين نحاول الوصول إلى شيء يُسمى uid في هذه الطريقة ولماذا يكون null؟ في الوقت الحالي، تبدو الطريقة على النحو التالي في public/js/homepage.js:

public/js/homepage.js

  addToCart(id, itemData) {
    console.log("addToCart", id, JSON.stringify(itemData));
    return this.db
      .collection("carts")
      .doc(this.auth.currentUser.uid)
      .collection("items")
      .doc(id)
      .set(itemData);
  }

آها! لم نسجّل الدخول إلى التطبيق. وفقًا لمستندات Firebase Authentication، عندما لا نسجّل الدخول، تكون قيمة auth.currentUser هي null. لنضِف شرطًا للتحقّق من ذلك:

public/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

حدثت أربعة أحداث رئيسية لإنشاء هذه السجلات وتحديث واجهة المستخدم الذي لاحظته:

68c9323f2ad10f7a.png

1) عملية الكتابة في Firestore - العميل

تتم إضافة مستند جديد إلى مجموعة Firestore /carts/{cartId}/items/{itemId}/. يمكنك الاطّلاع على هذا الرمز في الدالة addToCart داخل public/js/homepage.js:

public/js/homepage.js

  addToCart(id, itemData) {
    // ...
    console.log("addToCart", id, JSON.stringify(itemData));
    return this.db
      .collection("carts")
      .doc(this.auth.currentUser.uid)
      .collection("items")
      .doc(id)
      .set(itemData);
  }

2) تشغيل Cloud Function

تستمع دالة Cloud calculateCart إلى أي أحداث كتابة (إنشاء أو تعديل أو حذف) تحدث لعناصر سلة التسوق باستخدام المشغّل onWrite، والذي يمكنك الاطّلاع عليه في functions/index.js:

functions/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" الإجماليات الجديدة وتعدّل واجهة المستخدم، كما هو موضّح في public/js/homepage.js:

public/js/homepage.js

this.cartUnsub = cartRef.onSnapshot(cart => {
   
// The cart document was changed, update the UI
   
// ...
});

ملخّص

أحسنت. لقد أعددت للتو تطبيقًا محليًا بالكامل يستخدم ثلاث محاكيات مختلفة من Firebase لإجراء اختبارات محلية بالكامل.

db82eef1706c9058.gif

انتظر، فهناك المزيد. في القسم التالي، سنتعرّف على ما يلي:

  • كيفية كتابة اختبارات الوحدات التي تستخدم محاكيات Firebase
  • كيفية استخدام محاكيات Firebase لتصحيح أخطاء "قواعد الأمان"

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;
    }
  }
}

في الوقت الحالي، يمكن لأي شخص قراءة البيانات وكتابتها في قاعدة البيانات. نريد التأكّد من أنّ العمليات الصالحة فقط هي التي يتم تنفيذها، وأنّنا لا نسرّب أي معلومات حسّاسة.

خلال هذا الدرس العملي، سنتبع مبدأ أقل امتياز، وسنحظر جميع المستندات ونضيف إذن الوصول تدريجيًا إلى أن يحصل جميع المستخدمين على جميع أذونات الوصول التي يحتاجون إليها، ولكن ليس أكثر من ذلك. لنعدّل القاعدتَين الأوليَين لرفض الوصول من خلال ضبط الشرط على false:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // User's cart metadata
    match /carts/{cartID} {
      // UPDATE THIS LINE
      allow read, write: if false;
    }

    // Items inside the user's cart
    match /carts/{cartID}/items/{itemID} {
      // UPDATE THIS LINE
      allow read, write: if false;
    }

    // All items available in the store. Users can read
    // items but never write them.
    match /items/{itemID} {
      allow read: if true;
    }
  }
}

8. تشغيل المحاكيات والاختبارات

بدء المحاكيات

في سطر الأوامر، تأكَّد من أنّك في emulators-codelab/codelab-initial-state/. قد تكون المحاكيات لا تزال تعمل من الخطوات السابقة. إذا لم يكن الأمر كذلك، أعِد تشغيل المحاكيات:

$ firebase emulators:start --import=./seed

بعد تشغيل المحاكيات، يمكنك إجراء الاختبارات محليًا عليها.

إجراء الاختبارات

في سطر الأوامر في علامة تبويب وحدة طرفية جديدة من الدليل emulators-codelab/codelab-initial-state/

انتقِل أولاً إلى دليل الدوال (سنبقى هنا لبقية الدرس العملي):

$ cd functions

الآن، شغِّل اختبارات Mocha في دليل الدوال، وانتقِل إلى أعلى الناتج:

# 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. الوصول الآمن إلى سلة التسوّق

أول حالتين من حالات الفشل هما اختبارا "عربة التسوّق" اللذان يختبران ما يلي:

  • يمكن للمستخدمين إنشاء سلّات التسوّق وتعديلها فقط
  • يمكن للمستخدمين قراءة سلال التسوّق الخاصة بهم فقط

functions/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. الوصول إلى سلة التسوّق التجريبية

تعدِّل "مجموعة المحاكي" القواعد تلقائيًا كلّما تم حفظ 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. التحقّق من عملية "الإضافة إلى سلة التسوّق" في واجهة المستخدم

في الوقت الحالي، على الرغم من أنّ مالكي سلّة التسوّق يمكنهم القراءة والكتابة في سلّتهم، لا يمكنهم قراءة أو كتابة عناصر فردية في سلّتهم. ويرجع ذلك إلى أنّه على الرغم من أنّ المالكين يمكنهم الوصول إلى مستند سلّة التسوّق، لا يمكنهم الوصول إلى مجموعة العناصر الفرعية في سلّة التسوّق.

هذه حالة غير صالحة للمستخدمين.

ارجع إلى واجهة مستخدم الويب التي تعمل على 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) وأضِف سلعة إلى سلة التسوّق. هذه خطوة مهمة للتأكّد من أنّ اختباراتنا وقواعدنا تتطابق مع الوظائف التي يطلبها العميل. (يُرجى العِلم أنّه في آخر مرة جرّبنا فيها واجهة المستخدم، لم يتمكّن المستخدمون من إضافة سلع إلى سلة التسوّق).

69ad26cee520bf24.png

يعيد العميل تحميل القواعد تلقائيًا عند حفظ firestore.rules. لذا، حاوِل إضافة سلعة إلى سلة التسوّق.

ملخّص

أحسنت. لقد حسّنت للتو أمان تطبيقك، وهي خطوة أساسية لتجهيزه للإصدار العلني. إذا كان هذا تطبيقًا متاحًا للجميع، يمكننا إضافة هذه الاختبارات إلى مسار الدمج المتواصل. سيمنحنا ذلك الثقة في أنّ بيانات عربة التسوّق ستتضمّن عناصر التحكّم في الوصول هذه، حتى إذا كان الآخرون يعدّلون القواعد.

ba5440b193e75967.gif

ولكن انتظر، هناك المزيد!

إذا واصلت القراءة، ستتعرّف على ما يلي:

  • كيفية كتابة دالة يتم تشغيلها بواسطة حدث Firestore
  • كيفية إنشاء اختبارات تعمل على محاكيات متعددة

15. إعداد اختبارات Cloud Functions

حتى الآن، ركّزنا على الواجهة الأمامية لتطبيق الويب وقواعد الأمان في Firestore. لكنّ هذا التطبيق يستخدم أيضًا Cloud Functions لإبقاء سلة التسوّق لدى المستخدم محدّثة، لذا نريد اختبار هذه التعليمات البرمجية أيضًا.

تسهّل "حزمة المحاكي" اختبار "وظائف السحابة"، حتى الوظائف التي تستخدم Cloud Firestore وخدمات أخرى.

في المحرِّر، افتح الملف emulators-codelab/codelab-initial-state/functions/test.js وانتقِل إلى آخر اختبار في الملف. في الوقت الحالي، تم وضع علامة "في انتظار المراجعة" عليها:

//  REMOVE .skip FROM THIS LINE
describe.skip("adding an item to the cart recalculates the cart total. ", () => {
  // ...

  it("should sum the cost of their items", async () => {
    ...
  });
});

لتفعيل الاختبار، أزِل .skip، ليصبح على النحو التالي:

describe("adding an item to the cart recalculates the cart total. ", () => {
 
// ...

  it
("should sum the cost of their items", async () => {
   
...
 
});
});

بعد ذلك، ابحث عن المتغيّر REAL_FIREBASE_PROJECT_ID في أعلى الملف وغيِّره إلى رقم تعريف مشروع Firebase الفعلي:

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

إذا نسيت رقم تعريف مشروعك، يمكنك العثور عليه في "إعدادات المشروع" ضِمن "وحدة تحكّم Firebase" باتّباع الخطوات التالية:

d6d0429b700d2b21.png

16. التنقّل بين اختبارات الدوال

بما أنّ هذا الاختبار يتحقّق من التفاعل بين Cloud Firestore وCloud Functions، يتطلّب إعدادًا أكثر من الاختبارات الواردة في دروس البرمجة السابقة. لنستعرض هذا الاختبار ونفهم ما هو متوقّع منه.

إنشاء سلة تسوّق

تعمل Cloud Functions في بيئة خادم موثوقة ويمكنها استخدام مصادقة حساب الخدمة التي تستخدمها حزمة SDK للمشرف . أولاً، عليك تهيئة تطبيق باستخدام initializeAdminApp بدلاً من initializeApp. بعد ذلك، يمكنك إنشاء DocumentReference لسلة التسوّق التي ستتم إضافة السلع إليها وتهيئة سلة التسوّق:

it("should sum the cost of their items", async () => {
    const db = firebase
        .initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
        .firestore();

    // Setup: Initialize cart
    const aliceCartRef = db.doc("carts/alice")
    await aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });

    ...
  });

تشغيل الدالة

بعد ذلك، أضِف مستندات إلى المجموعة الفرعية items لمستند سلة التسوّق من أجل تشغيل الدالة. أضِف عنصرَين للتأكّد من أنّك تختبر عملية الإضافة التي تحدث في الدالة.

it("should sum the cost of their items", async () => {
    const db = firebase
        .initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
        .firestore();

    // Setup: Initialize cart
    const aliceCartRef = db.doc("carts/alice")
    await aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });

    //  Trigger calculateCart by adding items to the cart
    const aliceItemsRef = aliceCartRef.collection("items");
    await aliceItemsRef.doc("doc1").set({name: "nectarine", price: 2.99});
    await aliceItemsRef.doc("doc2").set({ name: "grapefruit", price: 6.99 });

    ...
    });
  });

تحديد توقعات الاختبار

استخدِم onSnapshot() لتسجيل أداة معالجة لأي تغييرات في مستند سلة التسوّق. تعرض الدالة onSnapshot() دالة يمكنك استدعاؤها لإلغاء تسجيل أداة معالجة الحدث.

في هذا الاختبار، أضِف سلعتَين تبلغ تكلفتهما الإجمالية 9.98 دولار أمريكي. بعد ذلك، تحقَّق مما إذا كانت سلة التسوّق تتضمّن itemCount وtotalPrice المتوقّعتَين. إذا كان الأمر كذلك، تكون الدالة قد أدّت وظيفتها.

it("should sum the cost of their items", (done) => {
    const db = firebase
        .initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
        .firestore();

    // Setup: Initialize cart
    const aliceCartRef = db.doc("carts/alice")
    aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });

    //  Trigger calculateCart by adding items to the cart
    const aliceItemsRef = aliceCartRef.collection("items");
    aliceItemsRef.doc("doc1").set({name: "nectarine", price: 2.99});
    aliceItemsRef.doc("doc2").set({ name: "grapefruit", price: 6.99 });
   
    // Listen for every update to the cart. Every time an item is added to
    // the cart's subcollection of items, the function updates `totalPrice`
    // and `itemCount` attributes on the cart.
    // Returns a function that can be called to unsubscribe the listener.
    await new Promise((resolve) => {
      const unsubscribe = aliceCartRef.onSnapshot(snap => {
        // If the function worked, these will be cart's final attributes.
        const expectedCount = 2;
        const expectedTotal = 9.98;
 
        // When the `itemCount`and `totalPrice` match the expectations for the
        // two items added, the promise resolves, and the test passes.
        if (snap.data().itemCount === expectedCount && snap.data().totalPrice == expectedTotal) {
          // Call the function returned by `onSnapshot` to unsubscribe from updates
          unsubscribe();
          resolve();
        };
      });
    });
   });
 });

17. إجراء الاختبارات

قد تكون المحاكيات لا تزال تعمل من الاختبارات السابقة. إذا لم يكن الأمر كذلك، ابدأ المحاكيات. من سطر الأوامر، شغِّل

$ firebase emulators:start --import=./seed

افتح علامة تبويب طرفية جديدة (مع إبقاء المحاكيات قيد التشغيل) وانتقِل إلى دليل الدوال. قد تكون هذه الصفحة لا تزال مفتوحة من اختبارات قواعد الأمان.

$ cd functions

الآن، شغِّل اختبارات الوحدات، وستظهر لك 5 اختبارات إجمالية:

$ npm test

> functions@ test .../emulators-codelab/codelab-initial-state/functions
> mocha

  shopping cart creation
    ✓ can be created by the cart owner (82ms)

  shopping cart reads, updates, and deletes
    ✓ cart can be read by the cart owner (42ms)

  shopping cart items
    ✓ items can be read by the cart owner (40ms)
    ✓ items can be added by the cart owner

  adding an item to the cart recalculates the cart total. 
    1) should sum the cost of their items

  4 passing (2s)
  1 failing

إذا نظرت إلى الخطأ المحدّد، يبدو أنّه خطأ في المهلة. يرجع ذلك إلى أنّ الاختبار ينتظر تعديل الدالة بشكلٍ صحيح، ولكن لا يتم تعديلها أبدًا. الآن، نحن على استعداد لكتابة الدالة التي تستوفي الاختبار.

18. كتابة دالة

لإصلاح هذا الاختبار، عليك تعديل الدالة في functions/index.js. على الرغم من كتابة جزء من هذه الدالة، إلا أنّها غير مكتملة. إليك الشكل الحالي للدالة:

// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
    .firestore.document("carts/{cartId}/items/{itemId}")
    .onWrite(async (change, context) => {
      console.log(`onWrite: ${change.after.ref.path}`);
      if (!change.after.exists) {
        // Ignore deletes
        return;
      }

      let totalPrice = 125.98;
      let itemCount = 8;
      try {
       
        const cartRef = db.collection("carts").doc(context.params.cartId);

        await cartRef.update({
          totalPrice,
          itemCount
        });
      } catch(err) {
      }
    });

تضبط الدالة مرجع سلة التسوّق بشكلٍ صحيح، ولكن بدلاً من احتساب قيمتَي totalPrice وitemCount، يتم تعديلهما إلى قيم مبرمَجة.

استرجاع والتكرار خلاله

items مجموعة فرعية

ابدأ ثابتًا جديدًا، itemsSnap، ليكون المجموعة الفرعية items. بعد ذلك، كرِّر جميع المستندات في المجموعة.

// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
    .firestore.document("carts/{cartId}/items/{itemId}")
    .onWrite(async (change, context) => {
      console.log(`onWrite: ${change.after.ref.path}`);
      if (!change.after.exists) {
        // Ignore deletes
        return;
      }


      try {
        let totalPrice = 125.98;
        let itemCount = 8;

        const cartRef = db.collection("carts").doc(context.params.cartId);
        // ADD LINES FROM HERE
        const itemsSnap = await cartRef.collection("items").get();

        itemsSnap.docs.forEach(item => {
          const itemData = item.data();
        })
        // TO HERE
       
        return cartRef.update({
          totalPrice,
          itemCount
        });
      } catch(err) {
      }
    });

حساب totalPrice وitemCount

أولاً، لنبدأ بضبط قيم 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. جرِّبها باستخدام واجهة مستخدم المتجر.

لإجراء الاختبار النهائي، ارجع إلى تطبيق الويب ( http://127.0.0.1:5000/‎) وأضِف منتجًا إلى سلة التسوّق.

69ad26cee520bf24.png

تأكَّد من أنّ سلّة التسوّق يتم تعديلها لتضمين المبلغ الإجمالي الصحيح. رائع!

ملخّص

لقد استعرضت حالة اختبار معقّدة بين "وظائف السحابة الإلكترونية لبرنامج Firebase" وCloud Firestore. لقد كتبت Cloud Function لاجتياز الاختبار. لقد تأكّدت أيضًا من أنّ الوظيفة الجديدة تعمل في واجهة المستخدم. لقد أجريت كل ذلك محليًا، وشغّلت المحاكيات على جهازك.

لقد أنشأت أيضًا برنامجًا على الويب يعمل على المحاكيات المحلية، وصمّمت قواعد أمان مخصّصة لحماية البيانات، واختبرت قواعد الأمان باستخدام المحاكيات المحلية.

c6a7aeb91fe97a64.gif