التطوير المحلي باستخدام "مجموعة أدوات المحاكاة" في Firebase
لمحة عن هذا الدرس التطبيقي حول الترميز
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 لتفعيل التطوير على الجهاز.
ستتعرّف أيضًا على كيفية:
- كيفية ربط تطبيقك بـ 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
- سجِّل الدخول إلى وحدة تحكّم Firebase باستخدام حسابك على Google.
- انقر على الزر لإنشاء مشروع جديد، ثم أدخِل اسم المشروع (على سبيل المثال،
Emulators Codelab
).
- انقر على متابعة.
- إذا طُلب منك ذلك، راجِع بنود Firebase واقبلها، ثم انقر على متابعة.
- (اختياري) فعِّل ميزة "المساعدة المستندة إلى الذكاء الاصطناعي" في وحدة تحكّم Firebase (المعروفة باسم "Gemini في Firebase").
- في هذا الدرس العملي، لا تحتاج إلى "إحصاءات Google"، لذا أوقِف خيار "إحصاءات Google".
- انقر على إنشاء مشروع، وانتظِر إلى أن يتم توفير مشروعك، ثم انقر على متابعة.
ربط الرمز البرمجي بمشروعك على 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
.
4. تشغيل التطبيق
فتح التطبيق
في متصفّح الويب، انتقِل إلى http://127.0.0.1:5000، وسيظهر لك تطبيق The Fire Store يعمل على جهازك.
استخدام التطبيق
اختَر سلعة من الصفحة الرئيسية وانقر على إضافة إلى سلة التسوّق. سيظهر لك الخطأ التالي:
لنصلح هذا الخطأ. بما أنّ كل شيء يتم تشغيله في المحاكيات، يمكننا إجراء التجارب بدون القلق بشأن التأثير في البيانات الحقيقية.
5. تصحيح أخطاء التطبيق
العثور على الخطأ
حسنًا، لنبحث في وحدة تحكّم مطوّري برامج Chrome. اضغط على Control+Shift+J
(في أجهزة Windows أو Linux أو ChromeOS) أو Command+Option+J
(في أجهزة Mac) للاطّلاع على الخطأ في وحدة التحكّم:
يبدو أنّه حدث خطأ في طريقة 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;
}
// ...
}
اختبار التطبيق
الآن، أعِد تحميل الصفحة، ثم انقر على إضافة إلى سلة التسوّق. من المفترض أن تظهر لك رسالة خطأ أفضل هذه المرة:
ولكن إذا نقرت على تسجيل الدخول في شريط الأدوات العلوي ثم على إضافة إلى سلة التسوّق مرة أخرى، ستلاحظ أنّه تم تعديل سلة التسوّق.
ومع ذلك، يبدو أنّ الأرقام غير صحيحة على الإطلاق:
لا تقلق، سنحلّ هذا الخطأ قريبًا. أولاً، لنستكشف بالتفصيل ما حدث عند إضافة سلعة إلى سلة التسوّق.
6. مشغّلات الدوال المحلية
يؤدي النقر على إضافة إلى سلة التسوّق إلى بدء سلسلة من الأحداث التي تتضمّن عدة محاكيات. في سجلات Firebase CLI، من المفترض أن تظهر لك رسائل مشابهة لما يلي بعد إضافة منتج إلى سلة التسوّق:
i functions: Beginning execution of "calculateCart" i functions: Finished "calculateCart" in ~1s
حدثت أربعة أحداث رئيسية لإنشاء هذه السجلات وتحديث واجهة المستخدم الذي لاحظته:
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 لإجراء اختبارات محلية بالكامل.
انتظر، فهناك المزيد. في القسم التالي، سنتعرّف على ما يلي:
- كيفية كتابة اختبارات الوحدات التي تستخدم محاكيات 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
:
أعِد تشغيل الاختبارات وتأكَّد من اجتياز الاختبارَين الأولَين الآن:
$ 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) وأضِف سلعة إلى سلة التسوّق. هذه خطوة مهمة للتأكّد من أنّ اختباراتنا وقواعدنا تتطابق مع الوظائف التي يطلبها العميل. (يُرجى العِلم أنّه في آخر مرة جرّبنا فيها واجهة المستخدم، لم يتمكّن المستخدمون من إضافة سلع إلى سلة التسوّق).
يعيد العميل تحميل القواعد تلقائيًا عند حفظ firestore.rules
. لذا، حاوِل إضافة سلعة إلى سلة التسوّق.
ملخّص
أحسنت. لقد حسّنت للتو أمان تطبيقك، وهي خطوة أساسية لتجهيزه للإصدار العلني. إذا كان هذا تطبيقًا متاحًا للجميع، يمكننا إضافة هذه الاختبارات إلى مسار الدمج المتواصل. سيمنحنا ذلك الثقة في أنّ بيانات عربة التسوّق ستتضمّن عناصر التحكّم في الوصول هذه، حتى إذا كان الآخرون يعدّلون القواعد.
ولكن انتظر، هناك المزيد!
إذا واصلت القراءة، ستتعرّف على ما يلي:
- كيفية كتابة دالة يتم تشغيلها بواسطة حدث 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" باتّباع الخطوات التالية:
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/) وأضِف منتجًا إلى سلة التسوّق.
تأكَّد من أنّ سلّة التسوّق يتم تعديلها لتضمين المبلغ الإجمالي الصحيح. رائع!
ملخّص
لقد استعرضت حالة اختبار معقّدة بين "وظائف السحابة الإلكترونية لبرنامج Firebase" وCloud Firestore. لقد كتبت Cloud Function لاجتياز الاختبار. لقد تأكّدت أيضًا من أنّ الوظيفة الجديدة تعمل في واجهة المستخدم. لقد أجريت كل ذلك محليًا، وشغّلت المحاكيات على جهازك.
لقد أنشأت أيضًا برنامجًا على الويب يعمل على المحاكيات المحلية، وصمّمت قواعد أمان مخصّصة لحماية البيانات، واختبرت قواعد الأمان باستخدام المحاكيات المحلية.