تطبيقات الويب التقدمية

تسجيل وتثبيت Service Worker

20 دقيقة الدرس 4 من 30

تسجيل Service Worker

قبل أن يتمكن service worker من التحكم في صفحاتك واعتراض الطلبات، يجب تسجيله. التسجيل يخبر المتصفح أين يقع ملف service worker الخاص بك وما النطاق الذي يجب أن يتحكم فيه.

التسجيل الأساسي

// main.js - شغّل هذا في ملف JavaScript الرئيسي الخاص بك if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js') .then(registration => { console.log('Service Worker مسجل بنجاح'); console.log('النطاق:', registration.scope); }) .catch(error => { console.error('فشل تسجيل Service Worker:', error); }); }); }
لماذا الانتظار لحدث 'load'؟

التسجيل على window.addEventListener('load') يضمن أن تسجيل service worker لا يتنافس مع موارد تحميل الصفحة الأولية، مما يحسن الأداء.

التسجيل مع الخيارات

// التسجيل مع نطاق مخصص navigator.serviceWorker.register('/sw.js', { scope: '/app/' }) .then(registration => { console.log('SW مسجل مع النطاق:', registration.scope); }); // التسجيل مع فحوصات التحديث navigator.serviceWorker.register('/sw.js', { updateViaCache: 'none' // 'imports', 'all', 'none' }) .then(registration => { // تحقق من التحديثات كل ساعة setInterval(() => { registration.update(); }, 3600000); });
خيارات updateViaCache:
  • 'imports': (افتراضي) تخزين service worker مؤقتاً لكن ليس السكريبتات المستوردة
  • 'all': تخزين service worker وجميع السكريبتات المستوردة مؤقتاً
  • 'none': لا تخزن service worker أو السكريبتات المستوردة مؤقتاً (جلب جديد دائماً)

مرحلة التثبيت

بعد التسجيل الناجح، يقوم المتصفح بتنزيل وتثبيت service worker. حدث install هو الوقت المثالي لتخزين الأصول الثابتة مؤقتاً.

حدث Install الأساسي

// sw.js const CACHE_NAME = 'my-pwa-cache-v1'; const urlsToCache = [ '/', '/index.html', '/styles/main.css', '/scripts/app.js', '/images/logo.png' ]; self.addEventListener('install', event => { console.log('Service Worker: جاري التثبيت...'); // انتظر حتى يكتمل التخزين المؤقت event.waitUntil( caches.open(CACHE_NAME) .then(cache => { console.log('Service Worker: تخزين الملفات مؤقتاً'); return cache.addAll(urlsToCache); }) .then(() => { console.log('Service Worker: جميع الملفات مخزنة مؤقتاً'); }) ); });
فهم event.waitUntil():

الدالة waitUntil() تمدد عمر الحدث حتى يتم حل الـ promise. هذا يضمن أن service worker لن يُنهى قبل اكتمال التخزين المؤقت.

  • إذا تم حل promise ← ينجح التثبيت
  • إذا تم رفض promise ← يفشل التثبيت، يُتجاهل service worker
  • المتصفح يعيد محاولة التثبيت في تحميل الصفحة التالي

دوال Cache API

1. caches.open()

// فتح أو إنشاء ذاكرة تخزين مؤقت caches.open('my-cache-v1') .then(cache => { // كائن Cache جاهز للاستخدام });

2. cache.addAll()

// إضافة عدة URLs للذاكرة المؤقتة (عملية ذرية) cache.addAll([ '/', '/styles/main.css', '/scripts/app.js' ]) .then(() => { console.log('جميع الملفات مخزنة مؤقتاً'); }) .catch(error => { console.error('فشل تخزين الملفات مؤقتاً:', error); // إذا فشل أي ملف، تفشل العملية بأكملها });
cache.addAll() عملية ذرية:

إذا فشل حتى URL واحد في التخزين المؤقت (404، خطأ شبكة)، تفشل العملية بأكملها ويُرفض تثبيت service worker. استخدم cache.add() في حلقة مع معالجة الأخطاء للموارد غير الحرجة.

3. cache.add()

// إضافة URL واحد للذاكرة المؤقتة cache.add('/styles/main.css') .then(() => { console.log('الملف مخزن مؤقتاً'); }); // يعادل: fetch('/styles/main.css') .then(response => cache.put('/styles/main.css', response));

4. cache.put()

// تخزين زوج request/response يدوياً fetch('/api/data.json') .then(response => { // استنساخ response لأن body يمكن قراءته مرة واحدة فقط return cache.put('/api/data.json', response.clone()); });

نمط تثبيت متقدم: حرج مقابل اختياري

// sw.js const CACHE_NAME = 'pwa-v1'; // الأصول الحرجة - يفشل التثبيت إذا لم يتم تخزينها const criticalAssets = [ '/', '/index.html', '/styles/main.css', '/scripts/app.js' ]; // الأصول الاختيارية - ينجح التثبيت حتى لو فشلت هذه const optionalAssets = [ '/images/banner.jpg', '/images/background.jpg', '/fonts/custom-font.woff2' ]; self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => { // تخزين الأصول الحرجة (ذري) return cache.addAll(criticalAssets) .then(() => { // تخزين الأصول الاختيارية بشكل فردي (غير ذري) return Promise.all( optionalAssets.map(url => { return cache.add(url).catch(error => { console.warn(`فشل تخزين ${url}:`, error); // لا ترفض - اسمح باستمرار التثبيت }); }) ); }); }) ); });

تخطي الانتظار

افتراضياً، service worker جديد ينتظر في حالة "waiting" حتى تُغلق جميع الصفحات التي تستخدم service worker القديم. skipWaiting() يجبر التفعيل الفوري.

// sw.js self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => cache.addAll(urlsToCache)) .then(() => { // إجبار service worker الجديد على التفعيل فوراً return self.skipWaiting(); }) ); });
اعتبارات skipWaiting():
  • الإيجابيات: المستخدمون يحصلون على التحديثات فوراً بدون إغلاق التبويبات
  • السلبيات: يمكن أن يسبب تناقض إذا تعارضت الإصدارات القديمة والجديدة
  • أفضل ممارسة: استخدم بحذر، ادمج مع موجه إعادة تحميل الصفحة
  • البديل: اطلب من المستخدم إعادة التحميل عندما يتوفر تحديث

مرحلة التفعيل

بعد التثبيت (والانتظار، إن وُجد)، يُفعّل service worker. حدث activate هو الوقت المثالي لتنظيف ذاكرة التخزين القديمة.

// sw.js const CACHE_NAME = 'pwa-v2'; self.addEventListener('activate', event => { console.log('Service Worker: جاري التفعيل...'); event.waitUntil( caches.keys() .then(cacheNames => { // حذف جميع ذاكرات التخزين ماعدا الإصدار الحالي return Promise.all( cacheNames.map(cacheName => { if (cacheName !== CACHE_NAME) { console.log('Service Worker: حذف ذاكرة تخزين قديمة:', cacheName); return caches.delete(cacheName); } }) ); }) .then(() => { console.log('Service Worker: اكتمل تنظيف ذاكرة التخزين'); }) ); });

المطالبة بالعملاء

افتراضياً، service worker المفعّل حديثاً لن يتحكم في الصفحات المحملة قبل تفعيله. clients.claim() يأخذ السيطرة الفورية.

// sw.js self.addEventListener('activate', event => { event.waitUntil( // تنظيف ذاكرات التخزين القديمة caches.keys() .then(cacheNames => { return Promise.all( cacheNames .filter(cacheName => cacheName !== CACHE_NAME) .map(cacheName => caches.delete(cacheName)) ); }) .then(() => { // السيطرة على جميع الصفحات فوراً return self.clients.claim(); }) ); });
حالات استخدام clients.claim():
  • التثبيت لأول مرة - ابدأ التحكم فوراً
  • تحديثات حرجة - طبّق التغييرات بدون إعادة تحميل
  • ادمج مع skipWaiting() للتحديثات الفورية
  • الأفضل مع إشعار إعادة تحميل الصفحة

إصدارات Service Worker

// sw.js // الإصدار في اسم ذاكرة التخزين const VERSION = 'v3.1.0'; const CACHE_NAME = `pwa-${VERSION}`; // أو استخدم الطابع الزمني const TIMESTAMP = new Date().getTime(); const CACHE_NAME = `pwa-${TIMESTAMP}`; // أو استخدم hash (في عملية البناء) const HASH = 'abc123def456'; const CACHE_NAME = `pwa-${HASH}`;
أفضل ممارسات الإصدارات:
  • غيّر اسم ذاكرة التخزين لتشغيل التحديث
  • استخدم إصدارات دلالية (v1.0.0, v1.1.0, إلخ)
  • أو استخدم طوابع زمنية/hashes للبناء
  • احتفظ بالإصدار في أعلى الملف لتحديثات سهلة
  • حدّث الإصدار مع كل تغيير في ذاكرة التخزين

مثال كامل للتسجيل والتثبيت

// ========== main.js (سكريبت الصفحة) ========== if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js') .then(registration => { console.log('SW مسجل:', registration.scope); // استمع للتحديثات registration.addEventListener('updatefound', () => { const newWorker = registration.installing; newWorker.addEventListener('statechange', () => { if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { // إصدار جديد متاح if (confirm('إصدار جديد متاح! إعادة تحميل للتحديث؟')) { newWorker.postMessage({ action: 'skipWaiting' }); window.location.reload(); } } }); }); }) .catch(error => { console.error('فشل تسجيل SW:', error); }); // استمع لتغيير المتحكم (SW جديد مُفعّل) navigator.serviceWorker.addEventListener('controllerchange', () => { console.log('service worker جديد مُفعّل'); window.location.reload(); }); }); } // ========== sw.js (Service Worker) ========== const VERSION = 'v1.0.0'; const CACHE_NAME = `pwa-${VERSION}`; const STATIC_ASSETS = [ '/', '/index.html', '/styles/main.css', '/scripts/app.js', '/manifest.json' ]; // حدث Install self.addEventListener('install', event => { console.log(`SW ${VERSION}: جاري التثبيت...`); event.waitUntil( caches.open(CACHE_NAME) .then(cache => cache.addAll(STATIC_ASSETS)) .then(() => self.skipWaiting()) .then(() => console.log(`SW ${VERSION}: مُثبّت`)) ); }); // حدث Activate self.addEventListener('activate', event => { console.log(`SW ${VERSION}: جاري التفعيل...`); event.waitUntil( caches.keys() .then(cacheNames => { return Promise.all( cacheNames .filter(name => name !== CACHE_NAME) .map(name => { console.log(`SW ${VERSION}: حذف ذاكرة تخزين ${name}`); return caches.delete(name); }) ); }) .then(() => self.clients.claim()) .then(() => console.log(`SW ${VERSION}: مُفعّل`)) ); }); // معالج الرسالة لـ skipWaiting self.addEventListener('message', event => { if (event.data && event.data.action === 'skipWaiting') { self.skipWaiting(); } });

تصحيح التثبيت

تصحيح Chrome DevTools:
  1. افتح DevTools ← Application ← Service Workers
  2. اختر "Update on reload" لإجبار التحديثات أثناء التطوير
  3. استخدم "Unregister" لإزالة service worker تماماً
  4. انقر على رابط مصدر service worker لرؤية سجلات install/activate
  5. اختر "Bypass for network" لتعطيل service worker مؤقتاً
  6. اعرض Cache Storage للتحقق من الملفات المخزنة مؤقتاً
تمرين:
  1. أنشئ ملف HTML جديد مع هيكل صفحة أساسي
  2. أنشئ ملف sw.js ونفّذ أحداث install/activate
  3. خزّن على الأقل 3 أصول ثابتة أثناء التثبيت
  4. سجّل service worker في JavaScript الخاص بصفحتك
  5. افتح Chrome DevTools وتحقق من نجاح التسجيل
  6. تحقق من Cache Storage لتأكيد تخزين الملفات
  7. غيّر إصدار ذاكرة التخزين، أعد التحميل، وتحقق من حذف ذاكرة التخزين القديمة
  8. نفّذ skipWaiting() و clients.claim() ولاحظ الفرق
  9. أضف عبارات console.log ولاحظ دورة الحياة في DevTools