Seriya: JavaScript Basics Dars 84

Service Worker

Service Worker nima, u oddiy Web Worker'dan qanday farq qiladi, install/activate lifecycle bosqichlari, "waiting" holati va skipWaiting, fetch event orqali proxy sifatida ishlashi, Cache API va Cache First / Network First / Stale-While-Revalidate strategiyalari.

84-dars

Service Worker

Web Worker'ni eslang: u — hisoblashni background'ga olib chiqadi, lekin tab yopilsa, u ham o'ladi, va u faqat shu tabga bog'liq.

Service Worker — bundan tubdan farq qiladi. U network va tab o'rtasida turuvchi proxy — ya'ni sizning ilovangiz fetch qilganda, so'rov to'g'ridan-to'g'ri internetga emas, balki avval Service Worker orqali o'tadi. Bu unga uchta noyob imkoniyat beradi:

  1. Offline ishlash — internet yo'q bo'lsa ham, keshdan javob qaytarish
  2. Push notification — tab yopiq bo'lsa ham, brauzer fon'da xabar ko'rsatishi
  3. Background sync — internet qaytganda, kechiktirilgan so'rovlarni avtomatik yuborish

1. Asosiy farq — lifecycle

Oddiy Worker: tab ochiq — u yashaydi, tab yopildi — u o'ladi. Juda oddiy, chiziqli.

Service Worker: tab bilan bog'liq emas. U brauzerning o'zida, alohida, "ilova process'idan tashqarida" ro'yxatdan o'tadi va yashaydi — hatto siz sahifani yopib qo'ysangiz ham.

// main.js — sahifada, oddiy script
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('SW ro\'yxatdan o\'tdi:', reg))
    .catch(err => console.error('Xato:', err));
}

Bu yerda register('/sw.js') chaqirilganda, brauzer uchta aniq bosqichdan o'tadi:

Scope — Service Worker qaysi sahifalarni nazorat qiladi

Service Worker faqat o'zi ro'yxatdan o'tgan joydan pastdagi (yoki teng darajadagi) URL'larni nazorat qila oladi — bu uning scopei deb ataladi. Standart holatda scope — sw.js fayli joylashgan papka:

navigator.serviceWorker.register('/sw.js');
// scope avtomatik '/' bo'ladi — butun sayt nazorat ostida

navigator.serviceWorker.register('/blog/sw.js');
// scope avtomatik '/blog/' bo'ladi — faqat /blog/... sahifalar nazorat ostida,
// masalan /about sahifasi bu SW'ga umuman tegishli bo'lmaydi

Scope'ni register()ning ikkinchi argumenti orqali qo'lda ham belgilash mumkin — lekin faqat fayl joylashgan papka yoki undan pastroq (tor) qilib, hech qachon kengroq qilib bo'lmaydi:

navigator.serviceWorker.register('/scripts/sw.js', { scope: '/' });
// fayl /scripts/ ichida tursa ham, scope'ni butun saytga ('/') kengaytirdik

Nega bu cheklov bor? Xavfsizlik uchun: agar /scripts/sw.js scope'ni /'gacha kengaytira olsa-yu, /scripts/ papkasiga yozish huquqi faqat ba'zi dasturchilarda bo'lsa, xatarli bo'lardi. Aksincha — sw.js qanchalik chuqurroq joylashgan bo'lsa, u shunchalik tor scope'ni tanlashi mumkin (masalan, /blog/sw.jsni { scope: '/blog/posts/' } bilan yanada torroq qilish mumkin), lekin hech qachon undan tashqariga chiqib ketolmaydi.

Amalda bu shuni anglatadi: agar loyihada bir nechta mustaqil Service Worker kerak bo'lsa (masalan, /blog/ va /shop/ uchun alohida-alohida offline mantiq), ularni mos ravishda /blog/sw.js va /shop/sw.js qilib joylashtirish kifoya — brauzer avtomatik ravishda har birini faqat o'z papkasiga bog'laydi.

Ro'yxatdan o'tgan scope'ni tekshirish uchun:

navigator.serviceWorker.register('/blog/sw.js').then(reg => {
  console.log(reg.scope); // "https://sayt.uz/blog/"
});

1-bosqich: Download

Brauzer sw.js faylini yuklab oladi. Agar bu URL avval ro'yxatdan o'tgan bo'lsa, brauzer eski va yangi faylni bayt-bayt taqqoslaydi — bir harf ham farqlansa, "yangi versiya" deb hisoblanadi.

2-bosqich: Install

// sw.js
self.addEventListener('install', (event) => {
  console.log('Install bosqichi');
  event.waitUntil(
    caches.open('v1').then(cache => {
      return cache.addAll(['/index.html', '/style.css', '/app.js']);
    })
  );
});

install — Service Worker birinchi marta ro'yxatdan o'tganda (yoki yangi versiya paydo bo'lganda) ishga tushadi. Bu yerda odatda kerakli fayllar keshga oldindan yuklab qo'yiladi (pre-caching).

event.waitUntil(promise) — muhim mexanizm: u brauzerga aytadi — "bu Promise tugamaguncha, install bosqichini 'tugadi' deb hisoblama". Agar buni yozmasangiz, brauzer keshlash tugashini kutmasdan keyingi bosqichga o'tib ketishi mumkin.

3-bosqich: Activate

self.addEventListener('activate', (event) => {
  console.log('Activate bosqichi');
  event.waitUntil(
    caches.keys().then(keys => {
      return Promise.all(
        keys.filter(key => key !== 'v1').map(key => caches.delete(key))
      );
    })
  );
});

activate — yangi Service Worker eski versiyani almashtirib, nazoratni qo'lga olganda ishga tushadi. Bu yerda odatda eski keshlarni tozalash ishlari qilinadi.


2. Nega ikkita bosqich kerak — "waiting" holati

Bu yerda eng ko'p chalkashtiradigan narsa: yangi Service Worker installdan o'tgach, u darhol eskisini almashtirmaydi. U "waiting" holatida to'xtab qoladi:

Eski SW (v1) — hali barcha ochiq tab'larni boshqaryapti
     |
Yangi SW (v2) — install bo'ldi, lekin "waiting"da kutyapti
     |
Faqat BARCHA eski tab'lar yopilgandan keyin →
     |
v2 activate bo'ladi va nazoratni oladi

Bunga sabab — agar sahifa v1 kodini ishlatib turgan bo'lsa-yu, fon'da v2 birdan boshqarishni boshlab yuborsa, eski sahifa yangi keshlangan (yoki formatga mos kelmaydigan) resurslarni olishi mumkin — bu versiyalar aralashib ketishiga olib keladi. Shuning uchun brauzer xavfsizlik uchun eski tab'lar yopilishini kutadi.

Buni majburiy tezlashtirish mumkin:

self.addEventListener('install', (event) => {
  self.skipWaiting(); // "waiting"ni o'tkazib yuborish, darhol activate'ga o'tish
});

3. Asosiy mexanizm — fetch event (proxy sifatida ishlashi)

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then(cachedResponse => {
      if (cachedResponse) {
        return cachedResponse; // keshdan qaytaramiz, network umuman ishlatilmaydi
      }
      return fetch(event.request); // keshda yo'q — internetga chiqamiz
    })
  );
});

event.respondWith(promise) — bu brauzerga aytadi: "bu so'rovga javobni odatdagidek internetdan emas, men beraman". Bu — Service Worker'ning "proxy" bo'lishining texnik ma'nosi.


4. Chuqurroq — bu qanday arxitektura darajasida ishlaydi (browser engine)

Bu yerda muhim tushuncha bor: Service Worker — bu oddiy Web Worker emas, u butunlay boshqa poydevorga qurilgan:

Web Worker Service Worker
Kim boshqaradi Tab (JS process) Brauzerning network stacki
Qachon ishga tushadi new Worker() chaqirilganda Tegishli event (fetch, push, sync) kelganda
Doim ishlaydimi Ha, yaratilgandan o'lguncha Yo'q — event yo'q bo'lsa, o'ldiriladi

Bu oxirgi qator — eng muhim texnik nuqta: Service Worker doimiy jonli thread emas. Brauzer uni event-driven, "ephemeral" (vaqtinchalik) qilib ishga tushiradi:

  1. fetch (yoki push, sync) eventi keladi
  2. Agar Service Worker "uxlab" (terminated) yotgan bo'lsa, brauzer uni qayta ishga tushiradi (spin up) — bu yangi V8 kontekst yaratish, kodni qayta bajarish
  3. Event handler ishlaydi
  4. Agar bir necha soniya davomida yangi event kelmasa, brauzer xotira tejash uchun Service Worker'ni yana o'chiradi (terminate)

Bu — nega Service Worker kodida global o'zgaruvchilarga ishonib bo'lmasligining sababi:

let counter = 0; // XATO YONDASHUV

self.addEventListener('fetch', (event) => {
  counter++; // Bu doim ishonchli o'smaydi — SW o'rtada o'chirilib qayta tug'ilishi mumkin
});

Har safar qayta ishga tushganda, counter 0'ga qaytadi — chunki butun JS konteksti (V8 heap'i) tozalanadi. Doimiy holat kerak bo'lsa, IndexedDB ishlatiladi (keyingi mavzu) — u Service Worker o'chib-yonganda ham saqlanib qoladi.


5. Cache API — nega localStorage emas

localStorage sinxron (ishlaydi va thread'ni bloklaydi), va Service Worker'da umuman mavjud emas (chunki u thread'ni bloklashi mumkin, bu esa event-driven arxitekturaga zid). Shuning uchun maxsus Cache API ishlatiladi — bu to'liq asinxron, Promise-based:

caches.open('v1').then(cache => {
  cache.put('/api/data', new Response(JSON.stringify({ x: 1 })));
});

Cache obyekti — bu aslida Request → Response juftliklarining xaritasi (map), brauzer diskida saqlanadi, Response obyektining o'zi (headers, status, body) to'liq saqlanadi — shunchaki matn emas.


6. Cache API metodlari — to'liq ro'yxat

Cache API ikki qavatli: caches — barcha keshlar ustidan ishlaydigan global obyekt (CacheStorage), cache — caches.open() orqali olingan bitta aniq kesh.

CacheStorage metodlari (caches.*)

Metod Vazifasi Qanday ishlatiladi
caches.open(name) Berilgan nomdagi keshni ochadi; mavjud bo'lmasa — yaratadi. Cache obyektiga Promise qaytaradi. caches.open('v1').then(cache => { /* cache bilan ishlash */ });
caches.match(request) Barcha keshlar bo'ylab qidiradi (nom ko'rsatilmaydi) — birinchi mos Responseni topib qaytaradi. Qaysi keshda ekanini bilish shart bo'lmaganda qulay. caches.match('/index.html').then(response => { if (response) return response; });
caches.has(name) Berilgan nomdagi kesh mavjudligini tekshiradi, true/false qaytaradi. caches.has('v1').then(exists => console.log(exists)); // true
caches.delete(name) Butun keshni (ichidagi hammasi bilan) o'chiradi. Muvaffaqiyatli bo'lsa true qaytaradi. Odatda activate bosqichida eski versiyalarni tozalashda ishlatiladi. caches.delete('v0').then(deleted => console.log(deleted)); // true
caches.keys() Mavjud barcha keshlarning nomlari ro'yxatini (string[]) qaytaradi. caches.keys().then(names => console.log(names)); // ['v1', 'v2']

Cache metodlari (cache.*, ya'ni caches.open() natijasi)

Metod Vazifasi Qanday ishlatiladi
cache.match(request, options?) Shu bitta kesh ichida so'rovga mos Responseni qidiradi. Topilmasa undefined qaytaradi. cache.match('/style.css').then(response => { if (response) return response; });
cache.matchAll(request?, options?) Bir nechta mos keluvchi Responselarni massiv qilib qaytaradi (masalan, query-parametr farq qiladigan bir nechta variant bo'lsa). Argument berilmasa — keshdagi hammasi qaytadi. cache.matchAll('/api/data').then(responses => console.log(responses.length));
cache.add(request) Berilgan URL'ni fetch qiladi va natijani avtomatik keshga yozadi — fetch + putning qisqartmasi. Faqat muvaffaqiyatli (status 200) javoblar saqlanadi. cache.add('/logo.png'); // fetch qilib, o'zi keshga yozadi
cache.addAll(requestsArray) add()ning ko'plik varianti — bir nechta URL'ni bitta chaqiruvda yuklab, keshlaydi. Muhim: agar ulardan bittasi ham muvaffaqiyatsiz bo'lsa (masalan, 404), hech nima keshlanmaydi (all-or-nothing, atomik operatsiya). cache.addAll(['/index.html', '/style.css', '/app.js']);
cache.put(request, response) Tayyor Request/Response juftligini qo'lda keshga yozadi. add()dan farqi — bu yerda Responseni siz o'zingiz (masalan, fetch() orqali) olib kelasiz, put faqat saqlaydi, fetch qilmaydi. fetch('/api/data').then(response => cache.put('/api/data', response.clone()));
cache.delete(request, options?) Berilgan so'rovga mos yozuvni keshdan o'chiradi. Topilib o'chirilsa true, topilmasa false qaytaradi. cache.delete('/old-file.js').then(deleted => console.log(deleted));
cache.keys(request?, options?) Kesh ichidagi barcha Request obyektlarini (kalitlarni) qaytaradi — qaysi URL'lar saqlanganini ko'rish uchun. cache.keys().then(requests => requests.forEach(req => console.log(req.url)));

match/matchAll/delete/keysning options parametri

Option Vazifasi Qanday ishlatiladi
ignoreSearch true bo'lsa, URL'dagi ?query=parametr qismini e'tiborsiz qoldiradi (/page?id=1 va /page?id=2 bir xil deb hisoblanadi). cache.match('/page?id=1', { ignoreSearch: true }); // /page?id=999 ham mos keladi
ignoreMethod true bo'lsa, HTTP metodni (GET/POST) tekshirmaydi. cache.match(request, { ignoreMethod: true }); // POST so'rov ham GET bilan keshlangan javobga mos keladi
ignoreVary true bo'lsa, Vary headerini hisobga olmaydi (odatda Vary bir xil URL, lekin turli javoblarni farqlash uchun ishlatiladi). cache.match(request, { ignoreVary: true }); // Vary header'dagi farqni e'tiborsiz qoldiradi

7. Strategiyalar — qachon kesh, qachon network

Real loyihalarda bir nechta klassik strategiya bor:

Strategiya Mantiq Qachon ishlatiladi
Cache First Avval keshga qara, bo'lmasa network Statik fayllar (CSS, logo)
Network First Avval networkka urin, xato bo'lsa kesh Yangilanib turadigan API data
Stale-While-Revalidate Keshdan darhol qaytar, fon'da yangilab qo'y Yangilik lentasi kabi

Stale-While-Revalidate misoli — eng "aqlli" yondashuv:

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.open('v1').then(cache => {
      return cache.match(event.request).then(cachedResponse => {
        const fetchPromise = fetch(event.request).then(networkResponse => {
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        });
        // foydalanuvchiga DARHOL eski data ko'rsatiladi,
        // fon'da esa yangisi yuklanib, keshga yoziladi
        return cachedResponse || fetchPromise;
      });
    })
  );
});

Diqqat: networkResponse.clone() — chunki Response body'si faqat bir marta o'qilishi mumkin (stream). Bir nusxasi keshga yoziladi, ikkinchisi sahifaga qaytariladi.


Xulosa

Web Worker — CPUni bo'shatadi (hisoblash uchun), Service Worker — networkni boshqaradi (offline va kesh uchun), va ikkalasi ham asosiy printsipda bir xil — main thread'dan mustaqil, alohida V8 konteksti, lekin Service Worker'ning hayot davri event'larga bog'liq, doimiy emas.