Cloud Functions: كود يعمل على خادم Firebase
كل ما كتبته حتى الآن يعمل على جهاز المستخدم، وقواعد الأمان تحرس البيانات. لكن بعض المهام لا تصلح هناك: عدّاد لا يجوز أن يغيّره المستخدم بنفسه، أو عمل يجب أن يحدث تلقائياً كلما أُضيف مستند. هذه المهام تحتاج كوداً موثوقاً يعمل على خادم. Cloud Functions تعطيك ذلك دون أن تدير خادماً: تكتب دالة JavaScript، وتنشرها، و Google تشغّلها عند الحاجة فقط.
تخيّل الدالة مثل موظف تحت الطلب: لا يجلس في المكتب طوال اليوم، بل يحضر عند وقوع حدث معيّن، ينجز مهمته، ثم ينصرف. وأنت تدفع مقابل وقت عمله فقط.
التجهيز
Section titled “التجهيز”داخل مجلد مشروعك:
firebase init functionsاختر JavaScript، ووافق على تثبيت الحزم. ستنشئ الأداة مجلداً اسمه functions فيه ملف index.js (تكتب فيه دوالك) وملف package.json خاص به. هذا المجلد مشروع Node.js مستقل: كوده يعمل على الخادم، لا في المتصفح.
الدالة الأولى: دالة HTTP
Section titled “الدالة الأولى: دالة HTTP”أبسط نوع: دالة لها رابط، تعمل كلما زاره أحد.
const { onRequest } = require('firebase-functions/v2/https');
exports.hello = onRequest((req, res) => { const name = req.query.name || 'زائر'; res.json({ message: `أهلاً يا ${name}` });});exports.hello: اسم التصدير هو اسم الدالة.reqالطلب القادم، وresالرد. وreq.query.nameيقرأ القيمة من الرابط بعد علامة الاستفهام.
شغّل المحاكي:
firebase emulators:start+ functions: Loaded functions definitions from source: hello.+ functions[us-central1-hello]: http function initialized (http://127.0.0.1:5001/school-app-4f2a1/us-central1/hello).افتح الرابط في المتصفح، ثم أضف إليه ?name=سارة:
{"message":"أهلاً يا زائر"}{"message":"أهلاً يا سارة"}والرابط يتكون من معرّف المشروع، ثم المنطقة (Region) التي تعمل فيها الدالة، ثم اسمها.
دالة تعمل تلقائياً عند حدث
Section titled “دالة تعمل تلقائياً عند حدث”النوع الثاني لا يستدعيه أحد: يعمل وحده عند وقوع حدث في Firebase. نريد عدّاداً لعدد المنشورات يزيد واحداً مع كل منشور جديد:
const { onDocumentCreated } = require('firebase-functions/v2/firestore');const { initializeApp } = require('firebase-admin/app');const { getFirestore, FieldValue } = require('firebase-admin/firestore');
initializeApp();const db = getFirestore();
exports.onPostCreated = onDocumentCreated('posts/{postId}', async (event) => { const post = event.data.data(); console.log('منشور جديد:', event.params.postId, post.text);
await db.doc('stats/posts').set( { count: FieldValue.increment(1) }, { merge: true } );});'posts/{postId}': راقب كل مستند يُنشأ فيposts.event.data.data()بيانات المستند الجديد، وevent.params.postIdمعرّفه.firebase-adminمكتبة الخادم: تتجاوز قواعد الأمان كلها، لأن كودك على الخادم موثوق.
أضفنا منشورين من التطبيق، فظهر في سجل المحاكي:
i functions: Beginning execution of "us-central1-onPostCreated"> منشور جديد: 4gOpCLV8l203f5J6as0L أول منشورi functions: Finished "us-central1-onPostCreated" in 1121.6449msi functions: Beginning execution of "us-central1-onPostCreated"> منشور جديد: JxPwKGHXUctwtS4sJE9c ثاني منشورi functions: Finished "us-central1-onPostCreated" in 7.3757msوالمستند stats/posts لم يكن موجوداً قبلها، وبعدها صار { count: 2 }. لاحظ زمن التنفيذ: التشغيل الأول أبطأ بكثير من الثاني، لأن الدالة «تستيقظ» أول مرة. هذا اسمه البداية الباردة (Cold Start).
وتوجد أخوات لها: onDocumentUpdated و onDocumentDeleted و onDocumentWritten لأي تغيير.
دالة يستدعيها التطبيق: onCall
Section titled “دالة يستدعيها التطبيق: onCall”في درس قواعد الأمان بقي عدّاد الإعجابات مشكلة: كيف نمنع المستخدم من كتابة أي رقم؟ الحل الأنظف: امنع تعديله من التطبيق تماماً، واجعل دالة على الخادم تغيّره:
const { onCall, HttpsError } = require('firebase-functions/v2/https');
exports.likePost = onCall(async (request) => { if (!request.auth) { throw new HttpsError('unauthenticated', 'سجّل دخولك أولاً'); }
const postId = request.data.postId; if (typeof postId !== 'string' || postId === '') { throw new HttpsError('invalid-argument', 'رقم المنشور مطلوب'); }
const likeRef = db.doc(`posts/${postId}/likes/${request.auth.uid}`); if ((await likeRef.get()).exists) { throw new HttpsError('already-exists', 'أعجبت بهذا المنشور من قبل'); }
await likeRef.set({ at: FieldValue.serverTimestamp() }); await db.doc(`posts/${postId}`).update({ likes: FieldValue.increment(1) });
return { ok: true };});request.auth: هوية المستخدم، ترسلها مكتبة Firebase تلقائياً ويتحقق منها الخادم. لا يمكن تزويرها.request.data: ما أرسله التطبيق. لا تثق به: افحص نوعه وقيمته كما فعلنا.HttpsError: خطأ منظم برمز ورسالة يصلان إلى التطبيق.- الدالة تسجّل إعجاب كل مستخدم في مجموعة فرعية
likes، فلا يعجب بالمنشور نفسه مرتين.
ومن التطبيق تُستدعى كأنها دالة عادية:
import { getFunctions, httpsCallable } from 'firebase/functions';
const functions = getFunctions(app);const likePost = httpsCallable(functions, 'likePost');
const result = await likePost({ postId: post.id });console.log(result.data);
try { await likePost({ postId: post.id });} catch (e) { console.log(e.code);}{ ok: true }functions/already-existsجرّبنا كل الحالات على المحاكي:
| المحاولة | النتيجة |
|---|---|
| سارة تعجب بالمنشور | نجحت، و likes صار 1 |
| سارة تعجب به مرة ثانية | functions/already-exists، و likes بقي 1 |
استدعاء بلا postId |
functions/invalid-argument |
| زائر يستدعي الدالة | functions/unauthenticated |
سارة تكتب likes: 1000 من التطبيق مباشرة |
permission-denied |
الصف الأخير من قواعد Firestore: لم نكتب أي allow update للمنشورات، فلا طريق إلى العدّاد إلا عبر الدالة.
firebase deploy --only functionsبعد النشر تطبع الأداة الرابط الحقيقي لدالة HTTP، وتبدأ الدوال العمل مع مشروعك الحقيقي. وسجلات console.log تجدها في Console، أو بالأمر firebase functions:log.
جرّب بنفسك
Section titled “جرّب بنفسك”- أنشئ دالة HTTP اسمها
timeترد بالوقت الحالي:res.json({ now: new Date().toISOString() }). - اكتب دالة
onDocumentDeletedعلىposts/{postId}تنقص عدّادstats/postsبواحد:FieldValue.increment(-1). - جرّب الدالتين على المحاكي: احذف منشوراً وراقب السجل والعدّاد.
- تحدٍّ: اكتب دالة
onCallاسمهاunlikePostتحذف إعجاب المستخدم وتنقص العدّاد، وترفض الطلب بالرمزnot-foundإذا لم يكن معجباً بالمنشور أصلاً.
أخطاء شائعة
Section titled “أخطاء شائعة”- الثقة ببيانات التطبيق: لا ترسل
uidداخلrequest.dataوتصدّقه. الهوية الموثوقة فيrequest.authفقط. - نسيان
await: تنتهي الدالة قبل اكتمال الكتابة، فتضيع البيانات أحياناً دون رسالة خطأ. - حلقة لا تنتهي: دالة تعمل عند تعديل مستند ثم تعدّل المستند نفسه تستدعي نفسها بلا توقف، وتدفع ثمن كل تشغيل. اكتب في مستند آخر، أو تحقق أولاً أن التغيير مطلوب.
- دالة لكل شيء: القراءة والكتابة العادية أسرع وأرخص مباشرة من التطبيق مع قواعد الأمان. استخدم الدوال لما يحتاج كوداً موثوقاً فقط.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
أي نوع من الدوال يعمل وحده كلما أُنشئ مستند في مجموعة؟
onDocumentCreated تراقب مساراً في Firestore وتعمل تلقائياً مع كل مستند جديد فيه.
سؤال
داخل دالة onCall، من أين نعرف هوية المستخدم بشكل موثوق؟
request.auth يأتي من رمز الدخول الذي ترسله مكتبة Firebase ويتحقق منه الخادم، أما request.data فيكتب فيه المستخدم ما يشاء.
الخلاصة
Section titled “الخلاصة”- Cloud Functions كود موثوق يعمل على خادم Google عند الحاجة فقط.
onRequestدالة برابط، وonDocumentCreatedوأخواتها تعمل تلقائياً عند أحداث Firestore.onCallيستدعيها التطبيق بـhttpsCallable، وهوية المستخدم تصلها فيrequest.auth.firebase-adminتتجاوز قواعد الأمان، فافحص كل ما يصل من التطبيق.- مبروك! أنهيت كورس Firebase: قاعدة بيانات فورية، ومصادقة، واستضافة، وقواعد أمان، وتخزين ملفات، ودوال سحابية، وكلها بلا خادم تديره.