Cloud Functions — مشغّلات Firestore والمنطق في الخلفية
Cloud Functions — مشغّلات Firestore والمنطق في الخلفية
تتيح لك Cloud Functions for Firebase تشغيل كود من جانب الخادم استجابةً للأحداث التي تُصدرها منتجات Firebase، دون الحاجة إلى إدارة خادمك الخاص. عند دمجها مع Firestore، يمكنك الاستجابة لتغييرات المستندات — الإنشاء والتحديث والحذف — وتنفيذ مهام في الخلفية مثل إلغاء تطبيع البيانات (denormalization) وإرسال رسائل بريد إلكتروني للمعاملات، أو تطبيق قواعد الأعمال التي يجب ألا تقيم على العميل.
المشغّلات الأساسية الثلاثة لـ Firestore
يُعرّض البانئ functions.firestore.document(path) ثلاثة معالجات للأحداث:
- onCreate — يُطلَق عند إنشاء مستند لأول مرة في المسار المحدد.
- onUpdate — يُطلَق عند الكتابة إلى مستند موجود (ليس الإنشاء أو الحذف).
- onDelete — يُطلَق عند حذف مستند.
- onWrite — يُطلَق عند الإنشاء أو التحديث أو الحذف (شامل، يُستخدم بشكل أقل للمنطق الخاص بالمشغّل).
onCreate — بريد ترحيبي عند إنشاء ملف مستخدم جديد
// functions/src/index.ts
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import * as nodemailer from 'nodemailer';
admin.initializeApp();
// يُطلَق في كل مرة يُنشأ فيها مستند تحت /users/{userId}
export const onUserCreated = functions.firestore
.document('users/{userId}')
.onCreate(async (snapshot, context) => {
const userId = context.params.userId;
const data = snapshot.data(); // المستند المُنشأ حديثاً
const email = data?.email as string;
const username = data?.username as string;
// إرسال بريد ترحيبي عبر محوّل SMTP
const transporter = nodemailer.createTransport({ /* smtp config */ });
await transporter.sendMail({
to: email,
subject: `مرحباً، ${username}!`,
text: 'تم إنشاء حسابك بنجاح.',
});
// الكتابة الاختيارية مجدداً إلى Firestore (تجنّب الحلقات اللانهائية!)
await admin.firestore()
.collection('users')
.doc(userId)
.update({ welcomeEmailSentAt: admin.firestore.FieldValue.serverTimestamp() });
console.log(`Welcome email sent to ${email}`);
});
إلغاء تطبيع البيانات مع onUpdate
Firestore قاعدة بيانات NoSQL؛ كثيراً ما تُلغي تطبيع البيانات — تخزين نسخ من نفس القيمة في مستندات متعددة لجعل القراءات سريعة وملائمة للاستعلام. عند تغيير مستند المصدر، يمكن لـ Cloud Function نشر هذا التغيير تلقائياً إلى جميع المستندات التابعة.
onUpdate — نشر تغيير اسم المستخدم إلى جميع منشوراته
// يُطلَق عند تحديث مستند /users/{userId}
export const onUserUpdated = functions.firestore
.document('users/{userId}')
.onUpdate(async (change, context) => {
const before = change.before.data(); // حالة المستند قبل الكتابة
const after = change.after.data(); // حالة المستند بعد الكتابة
// لا تنفّذ إلا إذا تغيّر اسم المستخدم فعلاً
if (before?.username === after?.username) {
console.log('Username unchanged — skipping denormalization.');
return null;
}
const userId = context.params.userId;
const newUsername = after?.username as string;
// ابحث عن جميع منشورات هذا المستخدم وحدّث اسم المؤلف المخزّن
const db = admin.firestore();
const posts = await db
.collection('posts')
.where('authorId', '==', userId)
.get();
if (posts.empty) return null;
const batch = db.batch();
posts.docs.forEach((doc) => {
batch.update(doc.ref, { authorUsername: newUsername });
});
await batch.commit();
console.log(`Updated authorUsername on ${posts.size} posts for user ${userId}`);
return null;
});
db.batch()) عند تحديث مستندات متعددة في وقت واحد. الدُّفعة ذرية (تنجح كلها أو تفشل كلها) وتُحتسب كعملية كتابة واحدة لأغراض الفوترة بدلاً من N كتابة فردية.التنظيف مع onDelete
عند حذف مستند، كثيراً ما تحتاج إلى تنظيف المجموعات الفرعية ذات الصلة وملفات التخزين أو النسخ المخزّنة مؤقتاً. تجعل Cloud Functions هذا الأمر تلقائياً وموثوقاً — لا يحتاج العميل أبداً إلى تنسيق عمليات الحذف المتتالية.
onDelete — حذف تتالٍ للمجموعات الفرعية للمستخدم والتخزين
import * as storage from '@google-cloud/storage';
export const onUserDeleted = functions.firestore
.document('users/{userId}')
.onDelete(async (snapshot, context) => {
const userId = context.params.userId;
const data = snapshot.data(); // المستند المحذوف
const db = admin.firestore();
// 1. حذف مجموعة منشورات المستخدم الفرعية
const posts = await db
.collection('posts')
.where('authorId', '==', userId)
.get();
const batch = db.batch();
posts.docs.forEach((doc) => batch.delete(doc.ref));
await batch.commit();
// 2. حذف صورة المستخدم الرمزية من Cloud Storage
const avatarPath = data?.avatarPath as string | undefined;
if (avatarPath) {
const bucket = admin.storage().bucket();
await bucket.file(avatarPath).delete().catch(() => {
// قد تكون الملف محذوفاً بالفعل — تجاهل الخطأ
console.warn(`Avatar not found at ${avatarPath}`);
});
}
console.log(`Cleaned up data for deleted user ${userId}`);
return null;
});
فهم البدء البارد (Cold Starts)
تعمل Cloud Functions عند الطلب. عندما لا يكون هناك مثيل لدالتك يعمل حالياً، يجب على Google توفير حاوية جديدة — ويُسمى هذا البدء البارد. تضيف عمليات البدء البارد زمن استجابة (غالباً 1–5 ثوانٍ) قبل بدء تنفيذ دالتك. الآثار الرئيسية:
- تزيد تعليمات
require/importالثقيلة على مستوى الوحدة (مثل تحميل SDKs كبيرة) من وقت البدء البارد. قم بتهيئة ما تحتاجه فقط على المستوى الأعلى. - استدعِ
admin.initializeApp()مرة واحدة فقط، محاطاً بفحص لعدم إعادة التهيئة في الاستدعاءات الدافئة. - المشغّلات في الخلفية (مثل مشغّلات Firestore) أكثر تسامحاً مع عمليات البدء البارد من دوال HTTP التي يستدعيها العميل مباشرة، لأن المستخدم لا ينتظر الاستجابة.
- فكر في تعيين الحد الأدنى للمثيلات في تكوين الدالة للحفاظ على مثيل دافئ نشط لمهام الخلفية الحساسة للزمن.
async/await). إذا أعدت undefined بينما لا يزال هناك عمل غير متزامن معلق، قد تنهي Cloud Functions الحاوية قبل اكتمال ذلك العمل، مما يؤدي إلى فقدان صامت للبيانات.تجنّب الحلقات اللانهائية
دالة Cloud Function تكتب مجدداً إلى نفس المستند الذي أطلقتها ستُشغّل نفسها مجدداً — مما يخلق حلقة لانهائية تستنزف حصتك وميزانيتك. احرص دائماً على حماية الكتابات العائدة:
- تحقق مما إذا كان الحقل ذو الصلة قد تغيّر فعلاً قبل الكتابة (
before.field === after.field). - اضبط حقل دلالي (مثل
processedAt) وتخطَّ التنفيذ إذا كان مضبوطاً بالفعل. - اكتب إلى مسار مختلف أو مجموعة فرعية بحيث لا يُطلَق نفس المشغّل.
الخلاصة
تُطلق مشغّلات Firestore أتمتة قوية من جانب الخادم دون إدارة بنية تحتية. استخدم onCreate للتأثيرات الجانبية على البيانات الجديدة (رسائل البريد الإلكتروني والعدادات)، وonUpdate لنشر النسخ غير المطبّعة، وonDelete للتنظيف التتالي. ضع في اعتبارك آثار البدء البارد — قم بتهيئة SDKs على نطاق الوحدة، وتجنّب الاستيرادات الثقيلة الكسولة، وأعد دائماً Promise محلولة حتى يعلم وقت تشغيل Google متى انتهى عملك.