الغوص العميق في استراتيجيات Workbox
في هذا الدرس، سنستكشف تقنيات التخزين المؤقت المتقدمة باستخدام إضافات Workbox والاستراتيجيات المخصصة لبناء PWAs قوية وعالية الأداء.
فهم معاملات الاستراتيجية
جميع استراتيجيات Workbox تقبل كائن خيارات بمعاملات مشتركة:
new workbox.strategies.CacheFirst({
// اسم الذاكرة المؤقتة لهذه الاستراتيجية
cacheName: 'my-cache',
// مصفوفة الإضافات المستخدمة
plugins: [
// الإضافات هنا
],
// خيارات fetch إضافية
fetchOptions: {
mode: 'cors',
credentials: 'include'
},
// خيارات الذاكرة المؤقتة الإضافية
matchOptions: {
ignoreSearch: true,
ignoreVary: true
}
});
إضافة انتهاء صلاحية الذاكرة المؤقتة
تقوم ExpirationPlugin تلقائياً بإزالة الإدخالات المخزنة مؤقتاً بناءً على العمر أو عدد الإدخالات:
workbox.routing.registerRoute(
({ request }) => request.destination === 'image',
new workbox.strategies.CacheFirst({
cacheName: 'images',
plugins: [
new workbox.expiration.ExpirationPlugin({
// الحد الأقصى لعدد الإدخالات للتخزين المؤقت
maxEntries: 50,
// الحد الأقصى لعمر الإدخالات بالثواني (30 يوماً)
maxAgeSeconds: 30 * 24 * 60 * 60,
// التنظيف التلقائي عند تجاوز الحصة
purgeOnQuotaError: true
})
]
})
);
ملاحظة: فحص انتهاء الصلاحية يحدث عند الوصول إلى الذاكرة المؤقتة، وليس بشكل مستمر في الخلفية. يتم إزالة الإدخالات القديمة عندما تؤدي الطلبات الجديدة إلى تشغيل الاستراتيجية.
إضافة الاستجابة القابلة للتخزين المؤقت
تضمن هذه الإضافة تخزين الاستجابات الناجحة فقط مؤقتاً:
workbox.routing.registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new workbox.strategies.NetworkFirst({
cacheName: 'api-cache',
plugins: [
new workbox.cacheableResponse.CacheableResponsePlugin({
// تخزين الاستجابات مؤقتاً مع رموز الحالة هذه
statuses: [0, 200],
// تخزين الاستجابات مؤقتاً مع هذه العناوين
headers: {
'x-custom-header': 'cacheable'
}
})
]
})
);
رمز الحالة 0 يُستخدم للاستجابات الغامضة (الطلبات عبر المصادر بدون CORS):
// مثال: تخزين خطوط Google مؤقتاً (استجابات غامضة)
workbox.routing.registerRoute(
({ url }) => url.origin === 'https://fonts.gstatic.com',
new workbox.strategies.CacheFirst({
cacheName: 'google-fonts',
plugins: [
new workbox.cacheableResponse.CacheableResponsePlugin({
statuses: [0, 200] // تضمين 0 للاستجابات الغامضة
}),
new workbox.expiration.ExpirationPlugin({
maxEntries: 30,
maxAgeSeconds: 60 * 60 * 24 * 365 // سنة واحدة
})
]
})
);
إضافة تحديث البث
تُخطر BroadcastUpdatePlugin تطبيقك عندما يتم تحديث البيانات المخزنة مؤقتاً:
// في Service Worker الخاص بك
workbox.routing.registerRoute(
({ url }) => url.pathname.startsWith('/api/posts/'),
new workbox.strategies.StaleWhileRevalidate({
cacheName: 'posts-cache',
plugins: [
new workbox.broadcastUpdate.BroadcastUpdatePlugin({
// اسم القناة لـ postMessage
channelName: 'api-updates',
// خيارات لمقارنة الاستجابات
headersToCheck: ['content-length', 'etag', 'last-modified']
})
]
})
);
// في JavaScript الخاص بصفحتك
const updateChannel = new BroadcastChannel('api-updates');
updateChannel.addEventListener('message', (event) => {
console.log('تم تحديث الذاكرة المؤقتة:', event.data);
const { cacheName, updatedURL } = event.data.payload;
// إظهار إشعار للمستخدم
showUpdateNotification('محتوى جديد متاح. قم بالتحديث لرؤية التحديثات.');
// أو تحديث المحتوى تلقائياً
fetchAndUpdateContent(updatedURL);
});
نصيحة: استخدم BroadcastUpdatePlugin مع StaleWhileRevalidate لإظهار إشعار للمستخدمين عندما يكون المحتوى الطازج متاحاً، مما يسمح لهم بإعادة التحميل دون إجبارهم.
إضافة المزامنة في الخلفية
تقوم BackgroundSyncPlugin بوضع الطلبات الفاشلة في قائمة انتظار وإعادة المحاولة عندما تكون الشبكة متاحة:
// قائمة انتظار طلبات POST الفاشلة
workbox.routing.registerRoute(
({ url, request }) =>
url.pathname.startsWith('/api/') &&
request.method === 'POST',
new workbox.strategies.NetworkOnly({
plugins: [
new workbox.backgroundSync.BackgroundSyncPlugin('api-queue', {
maxRetentionTime: 24 * 60 // إعادة المحاولة لمدة تصل إلى 24 ساعة
})
]
})
);
// الاستماع للمزامنة الناجحة
self.addEventListener('sync', (event) => {
if (event.tag === 'api-queue') {
console.log('تمت المزامنة في الخلفية');
}
});
إضافة طلب النطاق
تمكن RangeRequestsPlugin التخزين المؤقت للمحتوى الجزئي لملفات الوسائط:
workbox.routing.registerRoute(
({ request }) => request.destination === 'video',
new workbox.strategies.CacheFirst({
cacheName: 'video-cache',
plugins: [
new workbox.rangeRequests.RangeRequestsPlugin(),
new workbox.cacheableResponse.CacheableResponsePlugin({
statuses: [200, 206] // 206 = محتوى جزئي
})
]
})
);
ملاحظة: طلبات النطاق ضرورية لملفات الفيديو والصوت، مما يسمح للمستخدمين بالانتقال إلى مواضع مختلفة دون تنزيل الملف بأكمله.
الإضافات المخصصة
يمكنك إنشاء إضافات مخصصة لتوسيع وظائف Workbox:
// إضافة مخصصة تسجل عمليات الذاكرة المؤقتة
const loggingPlugin = {
// يتم استدعاؤها قبل استخدام استجابة مخزنة مؤقتاً
cachedResponseWillBeUsed: async ({ cacheName, request, cachedResponse }) => {
console.log(`استخدام استجابة مخزنة مؤقتاً من ${cacheName}`, request.url);
return cachedResponse;
},
// يتم استدعاؤها قبل تخزين استجابة مؤقتاً
cacheWillUpdate: async ({ response, request }) => {
console.log(`تخزين استجابة جديدة مؤقتاً لـ ${request.url}`);
// تخزين الاستجابات الناجحة فقط مؤقتاً
if (response.status === 200) {
return response;
}
return null; // عدم التخزين المؤقت
},
// يتم استدعاؤها قبل طلب fetch
requestWillFetch: async ({ request }) => {
console.log(`جلب: ${request.url}`);
return request;
},
// يتم استدعاؤها بعد اكتمال fetch
fetchDidSucceed: async ({ response, request }) => {
console.log(`نجح الجلب: ${request.url}`);
return response;
},
// يتم استدعاؤها عند فشل fetch
fetchDidFail: async ({ request, error }) => {
console.error(`فشل الجلب لـ ${request.url}:`, error);
}
};
// استخدام الإضافة المخصصة
workbox.routing.registerRoute(
({ request }) => request.destination === 'image',
new workbox.strategies.CacheFirst({
cacheName: 'images',
plugins: [loggingPlugin]
})
);
إنشاء استراتيجيات مخصصة
بينما توفر Workbox خمس استراتيجيات مدمجة، يمكنك إنشاء استراتيجيات مخصصة:
// استراتيجية مخصصة: جرب الذاكرة المؤقتة، ثم الشبكة، ثم البديل
class CacheNetworkFallback {
constructor(options = {}) {
this.cacheName = options.cacheName || 'fallback-cache';
this.fallbackURL = options.fallbackURL;
}
async handle({ request }) {
// جرب الذاكرة المؤقتة أولاً
const cachedResponse = await caches.match(request);
if (cachedResponse) {
console.log('التقديم من الذاكرة المؤقتة');
return cachedResponse;
}
try {
// جرب الشبكة
const networkResponse = await fetch(request);
// تخزين الاستجابات الناجحة مؤقتاً
if (networkResponse && networkResponse.status === 200) {
const cache = await caches.open(this.cacheName);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
// إرجاع البديل
console.log('تقديم البديل');
return caches.match(this.fallbackURL);
}
}
}
// استخدام الاستراتيجية المخصصة
workbox.routing.registerRoute(
({ request }) => request.destination === 'document',
new CacheNetworkFallback({
cacheName: 'pages',
fallbackURL: '/offline.html'
})
);
أنماط التوجيه المتقدمة
استراتيجيات متعددة لنفس المسار
// استخدام استراتيجيات مختلفة بناءً على خصائص الطلب
workbox.routing.registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
({ url, request }) => {
// استخدام NetworkOnly للتغييرات
if (request.method !== 'GET') {
return new workbox.strategies.NetworkOnly().handle({ request });
}
// استخدام NetworkFirst للبيانات الحرجة
if (url.pathname.includes('/critical/')) {
return new workbox.strategies.NetworkFirst({
networkTimeoutSeconds: 3
}).handle({ request });
}
// استخدام StaleWhileRevalidate لكل شيء آخر
return new workbox.strategies.StaleWhileRevalidate({
cacheName: 'api-cache'
}).handle({ request });
}
);
التخزين المؤقت الشرطي
// تخزين الصور من نطاقك فقط مؤقتاً
workbox.routing.registerRoute(
({ url, request }) => {
return request.destination === 'image' &&
url.origin === location.origin;
},
new workbox.strategies.CacheFirst({
cacheName: 'local-images'
})
);
// تخزين صور الطرف الثالث بشكل مختلف مؤقتاً
workbox.routing.registerRoute(
({ url, request }) => {
return request.destination === 'image' &&
url.origin !== location.origin;
},
new workbox.strategies.CacheFirst({
cacheName: 'external-images',
plugins: [
new workbox.expiration.ExpirationPlugin({
maxEntries: 30,
maxAgeSeconds: 7 * 24 * 60 * 60 // 7 أيام
})
]
})
);
إصدارات الذاكرة المؤقتة
نفذ إصدارات الذاكرة المؤقتة للتعامل مع التحديثات بشكل نظيف:
const CACHE_VERSION = 'v2';
// تعيين تفاصيل اسم الذاكرة المؤقتة مع الإصدار
workbox.core.setCacheNameDetails({
prefix: 'my-app',
suffix: CACHE_VERSION,
precache: 'precache',
runtime: 'runtime'
});
// تنظيف الذاكرات المؤقتة القديمة عند التفعيل
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
// حذف الذاكرات المؤقتة التي لا تطابق الإصدار الحالي
if (!cacheName.includes(CACHE_VERSION)) {
console.log('حذف الذاكرة المؤقتة القديمة:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
});
استراتيجية تدفئة الذاكرة المؤقتة
تدفئة الذاكرة المؤقتة مسبقاً بالموارد الحرجة قبل طلبها:
// تدفئة الذاكرة المؤقتة عند تثبيت Service Worker
self.addEventListener('install', (event) => {
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/app.js',
'/images/hero.jpg'
];
event.waitUntil(
caches.open('warm-cache-v1').then((cache) => {
return cache.addAll(urlsToCache);
})
);
});
// لاحقاً، استخدم الذاكرة المؤقتة الدافئة
workbox.routing.registerRoute(
({ url }) => [
'/',
'/styles/main.css',
'/scripts/app.js'
].includes(url.pathname),
new workbox.strategies.CacheFirst({
cacheName: 'warm-cache-v1'
})
);
تمرين:
- أنشئ استراتيجية تخزين مؤقت لطلبات API التي:
- تستخدم NetworkFirst مع مهلة 3 ثوانٍ
- تخزن الاستجابات الناجحة فقط مؤقتاً (حالة 200)
- تنتهي صلاحية الإدخالات بعد 24 ساعة
- تحد الذاكرة المؤقتة إلى 50 إدخال
- أضف BroadcastUpdatePlugin للإخطار عند تحديث بيانات API
- أنشئ إضافة مخصصة تضيف رأس مخصص لجميع الاستجابات المخزنة مؤقتاً
- نفذ إصدارات الذاكرة المؤقتة ونظف الذاكرات المؤقتة القديمة عند التفعيل
- اختبر التنفيذ الخاص بك بالانتقال إلى وضع عدم الاتصال والتحقق من سلوك الذاكرة المؤقتة
مراقبة الأداء
// تتبع معدلات نجاح الذاكرة المؤقتة
let cacheHits = 0;
let cacheMisses = 0;
const performancePlugin = {
cachedResponseWillBeUsed: async ({ cachedResponse }) => {
if (cachedResponse) {
cacheHits++;
} else {
cacheMisses++;
}
// تسجيل الإحصائيات كل 50 طلب
if ((cacheHits + cacheMisses) % 50 === 0) {
const hitRate = (cacheHits / (cacheHits + cacheMisses) * 100).toFixed(2);
console.log(`معدل نجاح الذاكرة المؤقتة: ${hitRate}%`);
}
return cachedResponse;
}
};
تحذير: كن حذراً مع أحجام الذاكرة المؤقتة وأوقات انتهاء الصلاحية. التخزين المؤقت العدواني جداً يمكن أن يؤدي إلى بيانات قديمة، بينما التخزين المؤقت المحافظ جداً يهزم الغرض من الوظائف دون اتصال. راقب استخدام الذاكرة المؤقتة الخاص بك واضبط الاستراتيجيات بناءً على أنماط الاستخدام الواقعية.