تخطَّ إلى المحتوى

Cloud Functions: كود يعمل على خادم Firebase

الدرس 9 من 9 · Firebase

كل ما كتبته حتى الآن يعمل على جهاز المستخدم، وقواعد الأمان تحرس البيانات. لكن بعض المهام لا تصلح هناك: عدّاد لا يجوز أن يغيّره المستخدم بنفسه، أو عمل يجب أن يحدث تلقائياً كلما أُضيف مستند. هذه المهام تحتاج كوداً موثوقاً يعمل على خادم. Cloud Functions تعطيك ذلك دون أن تدير خادماً: تكتب دالة JavaScript، وتنشرها، و Google تشغّلها عند الحاجة فقط.

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

داخل مجلد مشروعك:

نافذة طرفيّة
firebase init functions

اختر JavaScript، ووافق على تثبيت الحزم. ستنشئ الأداة مجلداً اسمه functions فيه ملف index.js (تكتب فيه دوالك) وملف package.json خاص به. هذا المجلد مشروع Node.js مستقل: كوده يعمل على الخادم، لا في المتصفح.

أبسط نوع: دالة لها رابط، تعمل كلما زاره أحد.

functions/index.js
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. نريد عدّاداً لعدد المنشورات يزيد واحداً مع كل منشور جديد:

functions/index.js
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.6449ms
i 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”

في درس قواعد الأمان بقي عدّاد الإعجابات مشكلة: كيف نمنع المستخدم من كتابة أي رقم؟ الحل الأنظف: امنع تعديله من التطبيق تماماً، واجعل دالة على الخادم تغيّره:

functions/index.js
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.

  1. أنشئ دالة HTTP اسمها time ترد بالوقت الحالي: res.json({ now: new Date().toISOString() }).
  2. اكتب دالة onDocumentDeleted على posts/{postId} تنقص عدّاد stats/posts بواحد: FieldValue.increment(-1).
  3. جرّب الدالتين على المحاكي: احذف منشوراً وراقب السجل والعدّاد.
  4. تحدٍّ: اكتب دالة onCall اسمها unlikePost تحذف إعجاب المستخدم وتنقص العدّاد، وترفض الطلب بالرمز not-found إذا لم يكن معجباً بالمنشور أصلاً.
  • الثقة ببيانات التطبيق: لا ترسل uid داخل request.data وتصدّقه. الهوية الموثوقة في request.auth فقط.
  • نسيان await: تنتهي الدالة قبل اكتمال الكتابة، فتضيع البيانات أحياناً دون رسالة خطأ.
  • حلقة لا تنتهي: دالة تعمل عند تعديل مستند ثم تعدّل المستند نفسه تستدعي نفسها بلا توقف، وتدفع ثمن كل تشغيل. اكتب في مستند آخر، أو تحقق أولاً أن التغيير مطلوب.
  • دالة لكل شيء: القراءة والكتابة العادية أسرع وأرخص مباشرة من التطبيق مع قواعد الأمان. استخدم الدوال لما يحتاج كوداً موثوقاً فقط.

سؤال

أي نوع من الدوال يعمل وحده كلما أُنشئ مستند في مجموعة؟

سؤال

داخل دالة onCall، من أين نعرف هوية المستخدم بشكل موثوق؟

  • Cloud Functions كود موثوق يعمل على خادم Google عند الحاجة فقط.
  • onRequest دالة برابط، و onDocumentCreated وأخواتها تعمل تلقائياً عند أحداث Firestore.
  • onCall يستدعيها التطبيق بـ httpsCallable، وهوية المستخدم تصلها في request.auth.
  • firebase-admin تتجاوز قواعد الأمان، فافحص كل ما يصل من التطبيق.
  • مبروك! أنهيت كورس Firebase: قاعدة بيانات فورية، ومصادقة، واستضافة، وقواعد أمان، وتخزين ملفات، ودوال سحابية، وكلها بلا خادم تديره.