تعرض العديد من التطبيقات المحتوى نفسه لجميع المستخدمين عند تحميل الصفحة الأولى. على سبيل المثال، قد يعرض موقع إخباري آخر الأخبار، أو قد يعرض موقع للتجارة الإلكترونية السلع الأكثر مبيعًا.
إذا تم عرض هذا المحتوى من Cloud Firestore، سيُصدر كل مستخدم طلب بحث جديدًا للحصول على النتائج نفسها عند تحميل التطبيق. وبما أنّ هذه النتائج لا يتم تخزينها مؤقتًا بين المستخدمين، يكون التطبيق أبطأ وأكثر تكلفة مما ينبغي.
الحل: الحِزم
تتيح لك حِزم Cloud Firestore تجميع حِزم البيانات من نتائج طلبات البحث الشائعة في الخلفية باستخدام مدير SDK في Firebase، وعرض هذه البيانات الثنائية الكبيرة المحسوبة مسبقًا والمخزّنة مؤقتًا على شبكة توصيل المحتوى (CDN). يمنح ذلك المستخدمين تجربة تحميل أولى أسرع بكثير ويقلّل من تكاليف طلبات البحث في Cloud Firestore.
في هذا الدليل، سنستخدم Cloud Functions لإنشاء الحِزم وFirebase Hosting لتخزين محتوى الحِزم مؤقتًا وعرضه بشكل ديناميكي. تتوفّر مزيد من المعلومات حول الحِزم في الأدلة.
أولاً، أنشئ دالة HTTP عامة بسيطة لطلب آخر 50 "قصة" وعرض النتيجة كحزمة.
Node.js
exports.createBundle = functions.https.onRequest(async (request, response) => { // Query the 50 latest stories const latestStories = await db.collection('stories') .orderBy('timestamp', 'desc') .limit(50) .get(); // Build the bundle from the query results const bundleBuffer = db.bundle('latest-stories') .add('latest-stories-query', latestStories) .build(); // Cache the response for up to 5 minutes; // see https://firebase.google.com/docs/hosting/manage-cache response.set('Cache-Control', 'public, max-age=300, s-maxage=600'); response.end(bundleBuffer); });
Java
package com.example; import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.firestore.Firestore; import com.google.cloud.firestore.FirestoreBundle; import com.google.cloud.firestore.Query.Direction; import com.google.cloud.firestore.QuerySnapshot; import com.google.cloud.functions.HttpFunction; import com.google.cloud.functions.HttpRequest; import com.google.cloud.functions.HttpResponse; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.cloud.FirestoreClient; import java.io.BufferedWriter; import java.io.IOException; public class ExampleFunction implements HttpFunction { public static FirebaseApp initializeFirebase() throws IOException { if (FirebaseApp.getApps().isEmpty()) { FirebaseOptions options = FirebaseOptions.builder() .setCredentials(GoogleCredentials.getApplicationDefault()) .setProjectId("YOUR-PROJECT-ID") .build(); FirebaseApp.initializeApp(options); } return FirebaseApp.getInstance(); } @Override public void service(HttpRequest request, HttpResponse response) throws Exception { // Get a Firestore instance FirebaseApp app = initializeFirebase(); Firestore db = FirestoreClient.getFirestore(app); // Query the 50 latest stories QuerySnapshot latestStories = db.collection("stories") .orderBy("timestamp", Direction.DESCENDING) .limit(50) .get() .get(); // Build the bundle from the query results FirestoreBundle bundle = db.bundleBuilder("latest-stores") .add("latest-stories-query", latestStories) .build(); // Cache the response for up to 5 minutes // see https://firebase.google.com/docs/hosting/manage-cache response.appendHeader("Cache-Control", "public, max-age=300, s-maxage=600"); // Write the bundle to the HTTP response BufferedWriter writer = response.getWriter(); writer.write(new String(bundle.toByteBuffer().array())); } }
بعد ذلك، عليك ضبط استضافة Firebase لعرض دالة Cloud Function هذه وتخزينها مؤقتًا من خلال تعديل firebase.json. من خلال هذا الإعداد، ستعرض شبكة توصيل المحتوى (CDN) في Firebase Hosting
محتوى الحزمة وفقًا لإعدادات ذاكرة التخزين المؤقت التي تم ضبطها من خلال
Cloud Function. عند انتهاء صلاحية ذاكرة التخزين المؤقت، سيتم إعادة تحميل المحتوى من خلال تشغيل
الدالة مرة أخرى.
firebase.json
{
"hosting": {
// ...
"rewrites": [{
"source": "/createBundle",
"function": "createBundle"
}]
},
// ...
}
أخيرًا، في تطبيق الويب، يمكنك جلب المحتوى المجمّع من شبكة توصيل المحتوى وتحميله في حزمة تطوير البرامج (SDK) لـ Firestore.
// If you are using module bundlers.
import firebase from "firebase/app";
import "firebase/firestore";
import "firebase/firestore/bundle" // This line enables bundle loading as a side effect.
async function fetchFromBundle() {
// Fetch the bundle from Firebase Hosting, if the CDN cache is hit the 'X-Cache'
// response header will be set to 'HIT'
const resp = await fetch('/createBundle');
// Load the bundle contents into the Firestore SDK
await db.loadBundle(resp.body);
// Query the results from the cache
// Note: omitting "source: cache" will query the Firestore backend.
const query = await db.namedQuery('latest-stories-query');
const storiesSnap = await query.get({ source: 'cache' });
// Use the results
// ...
}
نسبة التوفير المقدّرة
لنفترض أنّ هناك موقعًا إلكترونيًا إخباريًا يزوره 100,000 مستخدم يوميًا، ويحمّل كل مستخدم أهم 50 خبرًا عند التحميل الأولي. بدون أي تخزين مؤقت، سيؤدي ذلك إلى 50 × 100,000 = 5,000,000 عملية قراءة للمستندات يوميًا من Cloud Firestore.
لنفترض الآن أنّ الموقع الإلكتروني يعتمد الأسلوب المذكور أعلاه ويخزّن هذه النتائج الخمسين مؤقتًا لمدة تصل إلى 5 دقائق. لذلك بدلاً من تحميل نتائج طلب البحث لكل مستخدم، يتم تحميل النتائج 12 مرة بالضبط في الساعة. بغض النظر عن عدد المستخدمين الذين يصلون إلى الموقع الإلكتروني، يبقى عدد طلبات البحث في Cloud Firestore كما هو. بدلاً من 5,000,000 عملية قراءة للمستندات، ستستخدم هذه الصفحة 12 × 24 × 50 = 14,400 عملية قراءة للمستندات في اليوم. يمكن بسهولة تعويض التكاليف الإضافية البسيطة لخدمة "استضافة Firebase" وCloud Functions من خلال توفير تكاليف Cloud Firestore.
على الرغم من أنّ المطوّر يستفيد من توفير التكاليف، فإنّ المستفيد الأكبر هو المستخدم. إنّ تحميل هذه المستندات الخمسين من شبكة توصيل المحتوى (CDN) في "استضافة Firebase" بدلاً من تحميلها من Cloud Firestore مباشرةً يمكن أن يقلّل بسهولة من وقت تحميل المحتوى على الصفحة بمقدار 100 إلى 200 ملي ثانية أو أكثر. أظهرت الدراسات مرارًا وتكرارًا أنّ الصفحات السريعة تعني مستخدمين أكثر سعادة.