المستوى: مبتدئ
لو زائر موقعك فقد الإنترنت لحظة وضغط على رابط داخلي، شاشة "هذا الموقع غير متاح" بتطلع له فوراً وبيقفل التبويب. Service Worker بسطر تسجيل واحد وحوالي 30 سطر cache logic بيخلّي الموقع يفتح حتى من غير شبكة، وبيخلّي الزيارة الثانية تنزّل من 1.6 ثانية لـ 180ms على نفس الجهاز ونفس الكود.
المشكلة باختصار
المتصفح بيرجع كل ملف من السيرفر في كل زيارة، حتى لو الملف ده ما اتغيّرش من شهرين. لو الإنترنت ضعيف أو مقطوع لحظة، الموقع بيموت ويرجع رسالة الخطأ المعروفة. Service Worker بيشتغل كطبقة وسيطة بين المتصفح والسيرفر: بيخزّن الملفات اللي شفتها قبل كده في ذاكرة المتصفح، وبيرجّعها للزائر في ميلي ثواني — حتى لو السيرفر نفسه واقع.
المفهوم بمثال بسيط: محل البقالة
تخيّل إنك بتشتري نفس العيش والحليب من محل البقالة كل صبح. أول مرة بتدخل المحل، صاحبه بيقعد يدور على المنتجات في المخزن — بياخد دقيقتين. لكن بعد ما يعرفك ويعرف طلبك الثابت، بيحطّ نسخة جاهزة على الكاونتر قدامه مباشرة. تيجي تاني يوم؟ تستلم في 5 ثواني من غير ما يدخل المخزن أصلاً.
Service Worker هو الكاونتر اللي بين العميل (زائر الموقع) والمخزن (السيرفر). بيحتفظ بنسخة من ملفات الموقع اللي اتطلبت قبل كده، وبيقدّمها للزائر فوراً بدل ما يستنّى السيرفر يرد. لو السيرفر مش متاح أصلاً (الإنترنت مقطوع)، الكاونتر بيكمّل شغل لوحده من النسخ المخزّنة.
التعريف العلمي الدقيق
طبقاً لتوثيق W3C الرسمي للـ Service Workers، الـ Service Worker هو سكربت JavaScript بيسجّله المتصفح وبيشغّله في خلفية مستقلة تماماً عن الصفحة (worker thread منفصل). الـ Service Worker بيقدر يعترض كل HTTP request جايّ من الموقع عن طريق event اسمه fetch، وبيقدر يرجّع رد جاهز من ذاكرة الـ Cache API بدلاً من ضرب الشبكة. ده يعني الصفحة ممكن تشتغل offline بالكامل لو الملفات اتخزّنت قبل كده، وبيشتغل برضه كـ proxy ذكي بيختار يستخدم الكاش أو الشبكة حسب الحالة.
الكود في 40 سطر
محتاج ملفين فقط: sw.js فيه الـ Service Worker نفسه، وindex.html فيه سطر تسجيل واحد.
// sw.js
const CACHE_NAME = 'site-v1';
const URLS_TO_CACHE = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/logo.png'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(URLS_TO_CACHE);
})
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request);
})
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((names) =>
Promise.all(
names
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
)
)
);
});