IlmHamroh
JavaScript Full-stack/21-qism. React: media, integratsiyalar, PWA30/32-dars20 daqiqa
Mundarija (30)

Web push bildirishnomalar: ruxsat, obuna, VAPID va Service Worker'da qabul qilish

Qisqacha: Web push — sayt yopiq bo'lsa ham foydalanuvchiga bildirishnoma yuborish. Oqim: sahifa ruxsat so'raydi (faqat bosishdan keyin) va pushManager.subscribe bilan obuna oladi. Obuna serverga beriladi; server xabarni brauzerning push xizmatiga (Chrome — Google FCM) VAPID kaliti bilan imzolab yuboradi; push xizmati SW'ni uyg'otadi, SW esa showNotification qiladi. iPhone'da faqat bosh ekranga qo'shilgan PWA'da. Halol holat: kanon VAPID kalitini va imzoni lokal yaratdi, lekin push'ni lokal yetkaza olmadi — bu darsdagi oqim hujjat bo'yicha, vazifalar ga kirmadi.

Bu darsda

  • Push oqimining to'rt ishtirokchisini va har qadamda nima ketishini chiza olasiz.
  • Ruxsatni "yumshoq so'rash" naqshi bilan, to'g'ri vaqtda so'raydigan komponent yozasiz.
  • VAPID kalit juftligi, applicationServerKey va JWT imzosini Node'da yaratib ko'rasiz.
  • SW'dagi push va notificationclick ni ochiq oynaga fokus beradigan qilib yozasiz.
  • FCM, iOS cheklovlari va obuna eskirganda (410) nima qilishni bilasiz.

Oldin bilishingiz kerak: Permissions, Notifications va Geolocation, PWA: o'rnatiladigan veb ilova (push asoslari), Service Worker hayot sikli, PWA React loyihada.

1. Nega bu kerak?

«Bahor» oshxonasida buyurtma tayyor bo'lganda mehmon telefonida "Oshingiz tayyor!" chiqishi kerak. Mehmon ilovani yopib, Telegram'da o'tiribdi. Notifications darsidagi new Notification(…) bu yerda yordam bermaydi — u faqat sahifa ochiq bo'lganda ishlaydi.

PWA darsida push'ning asosini ko'rgan edik: obuna, VAPID, SW'dagi push hodisasi. Bu darsda butun zanjirni quramiz va uch savolga javob beramiz: ruxsatni qachon so'rash (eng ko'p xato shu yerda), kalitlar qanday ishlaydi, va iPhone hamda Android'da nima farq qiladi.

Hayotiy o'xshatish: shifoxona navbati. Siz ro'yxatdan o'tasiz (obuna), qabulxona sizning raqamingizni yozadi (server saqlaydi). Navbatingiz kelganda qabulxona to'g'ridan-to'g'ri sizga emas, pochta xizmatiga xat beradi (push xizmati). Pochta sizni qayerda bo'lsangiz ham topadi.

Bitta halol ogohlantirish oldindan. Push uchun brauzer kompaniyasining serveri kerak — Chrome uchun Google'ning FCM'i. Kursning qoidasi: hech qanday hisob va kalit yaratmaymiz. Kanon kalitlarni va imzoni lokal yaratdi, lekin xabarni lokal yetkazish urinishi natija bermadi. Shuning uchun bu darsdagi to'liq oqim — hujjatlar bo'yicha. Qaysi qism o'lchangani har joyda aytiladi.

2. Butun oqim

2.1 To'rt ishtirokchi

sequenceDiagram
  participant P as Sahifa
  participant W as Service Worker
  participant S as Server («Bahor»)
  participant X as Push xizmati (FCM)
  P->>P: tugma bosildi → requestPermission
  P->>X: subscribe(applicationServerKey)
  X-->>P: obuna: endpoint + kalitlar
  P->>S: POST /obuna (obuna JSON)
  Note over S: osh tayyor!
  S->>X: shifrlangan xabar + VAPID JWT
  X->>W: push hodisasi (sahifa yopiq bo'lsa ham)
  W->>W: showNotification
  Note over W: bosildi → notificationclick

Diagrammaga qarang: server hech qachon telefonga to'g'ridan-to'g'ri ulanmaydi. U faqat push xizmatini biladi. Push xizmati esa brauzer bilan doimiy aloqada — telefon uxlab yotganda ham.

2.2 Obuna ichida nima bor

pushManager.subscribe qaytargan obunani JSON.stringify qilsangiz, hujjat bo'yicha shunday shakl:

json
{
  "endpoint": "https://fcm.googleapis.com/fcm/send/…",
  "expirationTime": null,
  "keys": {
    "p256dh": "BN…(base64url, 65 bayt)",
    "auth": "…(base64url, 16 bayt)"
  }
}
  • endpoint — push xizmatidagi shu brauzerning "pochta qutisi". Bu manzilni bilgan har kim unga xabar yubormoqchi bo'ladi — shuning uchun u maxfiy saqlanadi va VAPID imzosi talab qilinadi.
  • p256dh va auth — xabarni shifrlash uchun brauzerning kalitlari. Server xabarni shu kalitlar bilan shifrlaydi (RFC 8291). Push xizmati — Google ham — xabar matnini o'qiy olmaydi, faqat yetkazadi.

Bu JSON — bitta foydalanuvchi, bitta brauzer. Foydalanuvchining telefoni va kompyuteri — ikki obuna. Server ularni foydalanuvchiga bog'lab saqlaydi (25-qism).

3. Ruxsat: qachon va qanday so'rash

3.1 Uch holat va bitta imkoniyat

Notification.permission uch qiymat oladi (Permissions darsida ko'rganmiz): "default" (so'ralmagan), "granted", "denied". Eng muhim fakt: "denied" dan keyin sayt qayta so'ray olmaydi. requestPermission() darhol "denied" qaytaradi, oyna chiqmaydi. Foydalanuvchi faqat brauzer sozlamalaridan o'zi qaytarishi mumkin.

Ya'ni brauzerning ruxsat oynasi — bitta imkoniyat. Uni sahifa ochilishi bilan, mehmon hali nima uchunligini bilmay turib ko'rsatsangiz — "Bloklash" ni bosadi va imkoniyat yo'qoladi. Chrome hujjatiga ko'ra, ruxsat so'rovlari ko'p rad etiladigan saytlarda brauzer oynani "jimroq" ko'rinishga o'tkazadi — manzil satridagi kichik belgi.

3.2 "Yumshoq so'rash" naqshi

To'g'ri yo'l — ikki qadam:

  1. O'z UI'ingiz — foydali paytda: mehmon buyurtma berdi. "Tayyor bo'lganda xabar beraylikmi?" va ikki tugma. Bu — sizning tugmangiz; «Yo'q» bosilsa, hech narsa yo'qolmaydi, keyinroq qayta taklif qilish mumkin.
  2. Brauzer oynasi — faqat «Ha» bosilgandan keyin. Foydalanuvchi nima uchunligini biladi va "Ruxsat berish" ehtimoli yuqori.

Natija oynasida haqiqiy ruxsat oynasi yo'q (sandbox iframe). Shuning uchun brauzer oynasini soraRuxsat funksiyasi bilan taqlid qilamiz — haqiqiy ilovada uning o'rnida Notification.requestPermission() turadi:

jsx
import { useState } from "react";

// Taqlid: brauzer oynasi. Ilovada — Notification.requestPermission()
const soraRuxsat = () =>
  new Promise((hal) => setTimeout(() => hal("granted"), 100));

function EslatmaTaklifi() {
  const [holat, setHolat] = useState("default");
  const [yopildi, setYopildi] = useState(false);

  async function yoq() {
    const javob = await soraRuxsat();
    console.log("brauzer javobi:", javob);
    setHolat(javob);
    // granted bo'lsa — keyingi qadam: pushManager.subscribe
  }

  if (holat === "granted") {
    return <p>Tayyor bo'lganda xabar beramiz.</p>;
  }
  if (holat === "denied") {
    return (
      <p>
        Bildirishnomalar o'chiq. Yoqish: manzil satridagi qulf
        belgisi → Bildirishnomalar → Ruxsat.
      </p>
    );
  }
  if (yopildi) return null;
  return (
    <section aria-label="Eslatma taklifi">
      <p>Oshingiz tayyor bo'lganda telefonga xabar beraylikmi?</p>
      <button onClick={yoq}>Ha, xabar bering</button>{" "}
      <button onClick={() => setYopildi(true)}>Hozir emas</button>
    </section>
  );
}

export default function App() {
  return (
    <>
      <p>Buyurtma qabul qilindi: osh, 1 porsiya.</p>
      <EslatmaTaklifi />
    </>
  );
}

Konsolda:

text
brauzer javobi: granted

Ekranda:

text
Buyurtma qabul qilindi: osh, 1 porsiya.
Tayyor bo'lganda xabar beramiz.

Uch holat — uch xil UI: taklif, "yoqildi", va denied da — yo'riqnoma (qayta so'rash befoyda). «Hozir emas» brauzer oynasini umuman ochmaydi — imkoniyat saqlanib qoladi.

Real ilovada yana ikki narsa: taklif qachon rad etilganini localStorage da eslab qolish (har sahifada qayta chiqmasin) va sahifa ochilganda Notification.permission ni o'qib, boshlang'ich holatni olish.

Tekshirib ko'ring: Jasur aka shunday kod yozdi: useEffect(() => { Notification.requestPermission(); }, []). Nima noto'g'ri, va iPhone'da nima bo'ladi?

Javob

Ikki muammo. Birinchisi — ruxsat sahifa ochilishi bilan, sababsiz so'raladi: ko'p odam "Bloklash" ni bosadi va bu denied qaytarib bo'lmaydi. Ikkinchisi — iOS'da (va Firefox, Safari'da) ruxsat faqat foydalanuvchi harakati (bosish) ichida so'ralishi mumkin; effektdan chaqirilgan so'rov oynani umuman ko'rsatmaydi. To'g'ri joy — tugmaning onClick i.

4. VAPID: "bu xabarni men yubordim"

4.1 Kalit juftligi

Push xizmati endpoint ga kelgan har xabarni yetkazavermaydi. U so'raydi: "obunani yaratgan server sizmisiz?". Javob — VAPID (Voluntary Application Server Identification, RFC 8292): server kalit juftligiga ega. Ochiq kalit obuna paytida brauzerga beriladi (applicationServerKey), yopiq kalit faqat serverda turadi va har xabarni imzolaydi.

Kalit — P-256 egri chizig'idagi ECDSA juftligi (crypto: shifrlash va imzo — serverda chuqur). Node'ning o'zida yaratsa bo'ladi — kanon tajribasi aynan shunday qildi:

js
import { generateKeyPairSync, sign, verify } from "node:crypto";

const { publicKey, privateKey } = generateKeyPairSync("ec", {
  namedCurve: "prime256v1", // P-256
});

// Ochiq kalit: 0x04 + x (32 bayt) + y (32 bayt) — "siqilmagan nuqta"
const jwk = publicKey.export({ format: "jwk" });
const ochiq = Buffer.concat([
  Buffer.from([4]),
  Buffer.from(jwk.x, "base64url"),
  Buffer.from(jwk.y, "base64url"),
]);
const applicationServerKey = ochiq.toString("base64url");
console.log("ochiq kalit:", ochiq.length, "bayt", ochiq[0]);
console.log("base64url uzunligi:", applicationServerKey.length);

Konsolda:

text
ochiq kalit: 65 bayt 4
base64url uzunligi: 87

Kalitning o'zi har safar boshqa (tasodifiy), shuning uchun faqat uzunliklarni va birinchi baytni (4) chiqardik. 65 bayt — kanon o'lchovida ham aynan shu: VAPID ochiq kalit (applicationServerKey, base64url, 65 bayt). Yopiq kalitni kanon faylga yozmadi — u serverning maxfiy sozlamasi (.env), git'ga hech qachon tushmaydi.

Amalda kalitni web-push npm paketi bitta buyruq bilan yaratadi: npx web-push generate-vapid-keys. Kalit bir marta yaratiladi va o'zgartirilmaydi: o'zgarsa, eski obunalarning hammasi ishlamay qoladi.

4.2 Brauzer kalitni qanday oladi

subscribe ga kalit Uint8Array sifatida beriladi, server esa base64url satr beradi. Shuning uchun deyarli har loyihada shu kichik funksiya bor:

js
function base64urlBaytlarga(matn) {
  const toldirish = "=".repeat((4 - (matn.length % 4)) % 4);
  const base64 = (matn + toldirish)
    .replaceAll("-", "+")
    .replaceAll("_", "/");
  const xom = atob(base64);
  return Uint8Array.from(xom, (harf) => harf.charCodeAt(0));
}

const baytlar = base64urlBaytlarga("BAEC_-8");
console.log(baytlar.length, [...baytlar]);

Konsolda:

text
5 [ 4, 1, 2, 255, 239 ]

base64url — oddiy base64 ning manzilga mos varianti: + o'rniga -, / o'rniga _, oxiridagi = lar yo'q. atob faqat oddiy base64 ni tushunadi — shuning uchun avval qaytaramiz. _-8 → /+8 → 255, 239.

Obuna kodi esa (PWA darsidagidek), hujjat bo'yicha:

js
const reg = await navigator.serviceWorker.ready;
const obuna = await reg.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: base64urlBaytlarga(VAPID_OCHIQ_KALIT),
});
await fetch("/api/obuna", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(obuna),
});

userVisibleOnly: true — Chrome talabi: har push ko'rinadigan bildirishnoma bilan tugaydi. "Jim" push (foydalanuvchiga bildirmasdan fonda ishlash) ruxsat etilmaydi.

4.3 Imzo: JWT

Server har xabarga Authorization sarlavhasini qo'shadi: vapid t=<JWT>, k=<ochiq kalit>. JWT — uch qismli satr: sarlavha, tana, imzo (JWT anatomiyasi — serverda chuqur). Uni Node'da qo'lda yasab, imzoni tekshiramiz:

js
import { generateKeyPairSync, sign, verify } from "node:crypto";

const { publicKey, privateKey } = generateKeyPairSync("ec", {
  namedCurve: "prime256v1",
});
const b64u = (x) => Buffer.from(x).toString("base64url");

const sarlavha = b64u(JSON.stringify({ typ: "JWT", alg: "ES256" }));
const tana = b64u(
  JSON.stringify({
    aud: "https://fcm.googleapis.com", // push xizmati origin'i
    exp: Math.floor(Date.now() / 1000) + 12 * 60 * 60, // 12 soat
    sub: "mailto:admin@example.com", // muammo bo'lsa — kimga yozish
  }),
);
const imzo = sign("sha256", Buffer.from(`${sarlavha}.${tana}`), {
  key: privateKey,
  dsaEncoding: "ieee-p1363", // JWT uchun: r ‖ s, 64 bayt
});
const jwt = `${sarlavha}.${tana}.${b64u(imzo)}`;

console.log("qismlar:", jwt.split(".").length);
console.log("imzo:", imzo.length, "bayt,", b64u(imzo).length);
const togri = verify(
  "sha256",
  Buffer.from(`${sarlavha}.${tana}`),
  { key: publicKey, dsaEncoding: "ieee-p1363" },
  imzo,
);
console.log("imzo to'g'ri:", togri);

Konsolda:

text
qismlar: 3
imzo: 64 bayt, 86
imzo to'g'ri: true

Push xizmati aynan shu tekshiruvni qiladi: ochiq kalit (k=) bilan imzoni tekshiradi va u obunadagi applicationServerKey ga tengmi — solishtiradi. Kanon tajribasida ham imzo 86 belgi edi (natijalar/tajriba/30-web-push.txt). aud — push xizmati (endpoint origin'i), exp — 24 soatdan oshmasligi kerak (RFC 8292).

Amalda bularni web-push paketi o'zi qiladi — shifrlash bilan birga. Server qismi (hujjat bo'yicha; push yuborish — 29-qismda):

js
import webpush from "web-push";

webpush.setVapidDetails(
  "mailto:admin@example.com",
  process.env.VAPID_OCHIQ,
  process.env.VAPID_YOPIQ, // faqat serverda!
);

try {
  await webpush.sendNotification(
    obuna, // brauzerdan kelgan JSON
    JSON.stringify({ sarlavha: "«Bahor»", matn: "Oshingiz tayyor!" }),
    { TTL: 60 * 60 }, // 1 soatda yetkazilmasa — tashla
  );
} catch (xato) {
  if (xato.statusCode === 404 || xato.statusCode === 410) {
    await obunaniOchir(obuna.endpoint); // obuna endi yo'q
  }
}

Uchta tafsilot: TTL — telefon o'chiq bo'lsa push xizmati xabarni qancha saqlashi (RFC 8030); eskirgan "osh tayyor" ertalab kelsa, foydasi yo'q. 410 Gone — foydalanuvchi ruxsatni olib tashladi yoki brauzer obunani yangiladi: bunday obunani bazadan o'chiring, aks holda yuborishlar o'sib boradi. Hajm — RFC 8030 push xizmatlaridan kamida 4096 bayt xabarni yetkazishni talab qiladi; katta ma'lumotni xabarga emas, SW yuklab oladigan manzilga qo'ying.

Tekshirib ko'ring: Nega VAPID yopiq kaliti VITE_ o'zgaruvchiga yozilmasligi kerak, ochiq kalit esa bemalol brauzerga beriladi?

Javob

Ochiq kalit — "kim imzolaganini tekshirish" uchun: uni bilish xabar yuborishga imkon bermaydi. Yopiq kalit esa imzolash uchun. U VITE_ bilan build'ga tushsa (Vite env), har kim JS fayldan oladi va sizning obunachilaringizning hammasiga sizning nomingizdan push yubora oladi — endpoint lar sizdan o'g'irlansa yoki taxmin qilinsa. Bu — AI chat darsidagi API kalit qoidasining aynan o'zi.

5. Service Worker'da qabul qilish

5.1 push hodisasi

Kanon tajribasidagi SW (tajriba/push/sw.js) — push qismi (sinov uchun sahifaga ham xabar yuborgan qismi olib tashlandi, bir qatorli try telefon uchun ochildi):

js
self.addEventListener("push", (event) => {
  let x = { sarlavha: "Vazifalar", matn: "Yangi eslatma" };
  try {
    x = { ...x, ...event.data.json() };
  } catch {
    /* matn bo'lishi mumkin */
  }
  event.waitUntil(
    self.registration.showNotification(x.sarlavha, {
      body: x.matn,
      tag: "eslatma",
      data: { url: "./" },
    }),
  );
});
  • event.data.json() — xabar tanasi (push xizmati shifrni brauzerda ochgan). Xabar JSON bo'lmasa — try sukut matnini qoldiradi.
  • event.waitUntil — bildirishnoma chiqmaguncha SW uxlamasin (hayot sikli). Busiz Chrome "sayt fonda yangilandi" degan o'z bildirishnomasini ko'rsatishi mumkin — userVisibleOnly va'dasi buzilgan bo'ladi.
  • tag — bir xil tegli bildirishnomalar bir-birini almashtiradi. «Bahor» uch marta "buyurtma holati" yuborsa, ekranda uchta emas, bitta (oxirgisi) turadi.
  • data — bosilganda kerak bo'ladigan ma'lumot (qaysi sahifani ochish).

Boshqa foydali parametrlar (MDN bo'yicha): icon (katta rasm), badge (Android holat qatoridagi kichik oq-qora belgi), renotify: true (bir xil tag da qayta tovush), requireInteraction (o'zi yopilmasin), actions (tugmalar — hamma platformada emas).

5.2 notificationclick: ochiq oynani topish

Kanon tajribasi bosilganda doim yangi oyna ochardi:

js
self.addEventListener("notificationclick", (event) => {
  event.notification.close();
  event.waitUntil(self.clients.openWindow(event.notification.data.url));
});

Mehmonda ilova allaqachon ochiq bo'lsa — ikkinchi tab ochiladi. Yaxshiroq yo'l: avval ochiq oynalarni qidirish va topilsa — unga fokus berish. Qaror mantiqini sof funksiyaga ajratib, Node'da sinaymiz (3-mashq shu yerdan):

js
function oynaTanla(oynalar, manzil) {
  const aniq = oynalar.find((o) => o.url === manzil);
  if (aniq) return { amal: "fokus", url: aniq.url };
  const ilova = oynalar[0];
  if (ilova) return { amal: "o'tish", url: manzil };
  return { amal: "ochish", url: manzil };
}

const ilova = "https://example.com/vazifalar/";
console.log(oynaTanla([{ url: ilova }], ilova));
console.log(oynaTanla([{ url: ilova + "statistika" }], ilova));
console.log(oynaTanla([], ilova));

Konsolda:

text
{ amal: 'fokus', url: 'https://example.com/vazifalar/' }
{ amal: "o'tish", url: 'https://example.com/vazifalar/' }
{ amal: 'ochish', url: 'https://example.com/vazifalar/' }

SW'da uni shunday ulaysiz (hujjat bo'yicha: clients.matchAll, client.focus(), client.navigate()):

js
self.addEventListener("notificationclick", (event) => {
  event.notification.close();
  const { url } = event.notification.data;
  const manzil = new URL(url, self.location).href;
  event.waitUntil(
    (async () => {
      const oynalar = await self.clients.matchAll({ type: "window" });
      const qaror = oynaTanla(oynalar, manzil);
      if (qaror.amal === "ochish") {
        return self.clients.openWindow(manzil);
      }
      const oyna =
        oynalar.find((o) => o.url === manzil) ?? oynalar[0];
      if (qaror.amal === "o'tish") await oyna.navigate(manzil);
      return oyna.focus();
    })(),
  );
});

clients.openWindow va focus faqat notificationclick ichida ishlaydi — brauzer "foydalanuvchi bosdi" deb hisoblaydigan qisqa vaqt ichida.

6. Platformalar: FCM va iOS

6.1 Firebase Cloud Messaging

Chrome'da endpoint doim fcm.googleapis.com — ya'ni Google'ning FCM'i. Lekin buning uchun Firebase hisobi shart emas: VAPID bilan standart Web Push to'g'ridan-to'g'ri FCM endpoint'iga ishlaydi. Firebase'ning o'z SDK'si (firebase/messaging) — boshqa yo'l: u getToken(messaging, { vapidKey, serviceWorkerRegistration }) bilan "token" beradi, server esa Firebase Admin SDK orqali yuboradi. Afzalligi — Android/iOS ilovalari bilan bitta tizim. Narxi — Firebase hisobi, loyiha sozlamasi va qo'shimcha SDK. Kanon Firebase loyihasi yaratmadi (topshiriq: hisob yo'q) — bu yo'l faqat hujjat bo'yicha.

Brauzer Push xizmati
Chrome, Edge (Android ham) Google FCM / Microsoft (Edge)
Firefox Mozilla Push Service
Safari (macOS, iOS) Apple Push Notification service

Standart Web Push (VAPID + RFC 8291 shifrlash) uchalasida ham bir xil kod bilan ishlaydi — farqni web-push paketi o'zi hal qiladi.

6.2 iPhone va iPad

PWA darsidagi ogohlantirish bu yerda asosiy talab:

  • iOS 16.4 dan, va faqat bosh ekranga qo'shilgan PWA'da. Safari tabida ochilgan saytga push kelmaydi (WebKit blogi bo'yicha).
  • Ruxsat faqat foydalanuvchi bosishi ichida.
  • Manifestda display: "standalone" (yoki fullscreen) — o'rnatilgan ilova shunday ochiladi.

Amalda bu shunday UI degani: iPhone'da, o'rnatilmagan sahifada «Eslatmalarni yoqish» tugmasi o'rniga «Avval bosh ekranga qo'shing: Ulashish → Bosh ekranga» yo'riqnomasi. Tekshiruv — "PushManager" in window va matchMedia("(display-mode: standalone)") (Ilova ichidamizmi?).

7. Biz sinab ko'rdik: nima ishladi, nima ishlamadi

Kanon tajribasi (tajriba/push/, faqat lokal, tashqi push xizmatisiz) natijasi — xom fayl aynan (JWT qisqartirilgan):

text
# 21/#30 web push tajribasi — 2026-10-17T17:57
VAPID ochiq kalit (applicationServerKey, base64url, 65 bayt): BEKRjE8nBpi2tQ2-gzEMqvLD… (yopiq kalit — faqat serverda, faylga yozilmadi)
VAPID JWT (Authorization: vapid t=…, k=…): eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL3B1c2guZXhhbXBsZS5pbnZhbGlkIiwiZXhwIjoxNzkyMzAzMDI5LCJzdWIiOiJtYWlsdG86bG9naW5AZXhhbXBsZS5jb20ifQ.h9vfrhCXFB… (86 belgi imzo, ES256)
Chrome/155.0.8059.82
ruxsat (so'ralmagan): default; PushManager bor: true
bosilgach: 
registrationId: null
CDP push → SW: sahifaga xabar null; getNotifications: [] (headless'da bildirishnoma ro'yxati bo'sh bo'lishi mumkin)
konsol xatolari: Failed to load resource: the server responded with a status of 404 (Not Found)

Qatorma-qator, halol:

  • Ishladi: VAPID kalit (65 bayt) va ES256 JWT imzosi (86 belgi) — node:crypto bilan. Yuqoridagi Node bloklari xuddi shuni takrorlaydi.
  • Ishladi: sahifa ochilganda ruxsat default, PushManager mavjud — ruxsat so'ralmagan, sahifa uni o'zi so'ramadi.
  • Chaqirilmadi: pushManager.subscribe — Chrome'da u Google FCM'ga ulanadi, bu esa tashqi xizmat. Topshiriq bo'yicha ataylab chaqirilmadi.
  • Ishlamadi: push'ni lokal yetkazish. Reja — Chrome DevTools Protocol'ning ServiceWorker.deliverPushMessage buyrug'i bilan SW'ga to'g'ridan-to'g'ri xabar berish edi. Lekin SW'ning registrationId si olinmadi (null), xabar yuborilmadi. «bosilgach:» qatori ham bo'sh — ruxsat tugmasi natijasi yozilmadi. Bu sinov vositasining muammosi, push'ning emas. Biz "push ishlaydi, sinadik" demaymiz.

Push'ni qo'lda, o'z kompyuteringizda sinash yo'li bor — PWA darsida ko'rgan DevTools → Application → Service Workers → Push maydoni. U SW'ga sinov xabarini beradi. Bu push ishlovchisini tekshiradi, lekin obuna, VAPID va push xizmatini emas.

7.1 Nega vazifalar da push yo'q

Push uchun server kerak: obunalarni saqlash, VAPID yopiq kaliti, yuborish. vazifalar ning o'z backend'i hali yo'q (u «Bahor» mashq API'si bilan ishlaydi). Push o'z serverimiz (25-qism) va push yuborish darsidan keyin qo'shiladi. Bu darsning kodi o'shanda tayyor turadi: ruxsat taklifi, base64urlBaytlarga, SW ishlovchilari.

8. Hujumchi nigohi

  • Yopiq kalit sizdi — hujumchi obunachilaringizga sizning nomingizdan "Hisobingiz bloklandi, shu havolani bosing" yuboradi. Kalit — faqat server .env ida; sizsa — yangisini yarating (eski obunalar yo'qoladi, qayta so'raysiz).
  • Bildirishnomadagi havola — notificationclick dagi data.url ni tekshiring: faqat o'z origin'ingiz (ochiq yo'naltirish). Yuqoridagi kodda new URL(…, self.location) nisbiy manzilni origin'ga bog'laydi; tashqi manzil bo'lsa — ochmang.
  • Obuna = shaxsiy ma'lumot. endpoint brauzerni aniqlaydi. Uni foydalanuvchi hisobiga bog'lab saqlang, chiqishda (logout) o'chiring — aks holda kompyuterdan chiqib ketgan odamga xabarlar kelaveradi.
  • Spam — "ruxsat berdi" — "hamma narsani yuborsa bo'ladi" degani emas. Foydalanuvchi bildirishnoma turlarini tanlay olsin (buyurtma holati — ha, aksiya — yo'q).

9. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
ruxsat oynasi chiqmaydi denied yoki bosishsiz so'rov holatni tekshiring; faqat onClick ichida
subscribe — applicationServerKey xatosi kalit satr, Uint8Array emas base64urlBaytlarga
Chrome o'zi "sayt fonda yangilandi" deydi waitUntil siz showNotification event.waitUntil(…)
uchta bir xil bildirishnoma tag yo'q bir xil tag
bosilganda ikkinchi tab doim openWindow avval clients.matchAll
server yuborishlari sekinlashib boradi 410 obunalar o'chirilmagan 404/410 — bazadan o'chirish
iPhone'da PushManager yo'q bosh ekranga qo'shilmagan o'rnatish yo'riqnomasi

10. Mashqlar

Mashqlar kurs/mashqlar/21/30-web-push/ da.

1-mashq (oson): bo'shliqlarni to'ldiring

  • Rad etilgan ruxsat holati —
  • Obunadagi push xizmati manzili —
  • Server imzosi uchun kalit standarti —
  • Bir xil bildirishnomalarni almashtiradigan parametr —
  • Obuna endi yo'qligini bildiruvchi HTTP holat kodi —
Yechim
  • denied — qayta so'rab bo'lmaydi, faqat sozlamalardan.
  • endpoint — har brauzer uchun alohida.
  • VAPID — RFC 8292; yopiq kalit serverda.
  • tag — oxirgisi qoladi.
  • 410 (Gone) — obunani bazadan o'chiring.

2-mashq (o'rta): taklifni qachon ko'rsatish

Funksiya taklifKorsatilsinmi(h) yozing. h — { ruxsat, buyurtmalar, rad, bugun }: ruxsat — "default" | "granted" | "denied", buyurtmalar — mehmonning buyurtmalari soni, rad — taklif oxirgi marta «Hozir emas» qilingan sana ("YYYY-MM-DD" yoki null), bugun — bugungi sana. Qoidalar:

  1. Ruxsat default bo'lmasa — false (berilgan yoki qaytarib bo'lmaydi).
  2. Kamida 1 ta buyurtma bo'lsin (foydali payt).
  3. Rad etilganidan beri kamida 7 kun o'tsin.

Ishora: sanalar orasidagi kun — (new Date(b) - new Date(a)) / 86_400_000 (Date).

Yechim
js
const KUN = 86_400_000;

function taklifKorsatilsinmi({ ruxsat, buyurtmalar, rad, bugun }) {
  if (ruxsat !== "default") return false;
  if (buyurtmalar < 1) return false;
  if (rad !== null && (new Date(bugun) - new Date(rad)) / KUN < 7) {
    return false;
  }
  return true;
}

const asos = {
  ruxsat: "default",
  buyurtmalar: 1,
  rad: null,
  bugun: "2026-11-02",
};
const sina = (o) =>
  console.log(taklifKorsatilsinmi({ ...asos, ...o }));
sina({});
sina({ ruxsat: "denied" });
sina({ buyurtmalar: 0 });
sina({ rad: "2026-10-30" });
sina({ rad: "2026-10-26" });

Konsolda:

text
true
false
false
false
true

Uchinchi holat — buyurtma yo'q (foydali payt kelmagan); to'rtinchisi — rad etilganiga 3 kun o'tgan, kutamiz; beshinchisi — 7 kun, yana taklif. denied uchun taklif ko'rsatish befoyda: «Ha» bosilsa ham brauzer oynasi chiqmaydi.

3-mashq (qiyin): oyna tanlash — eng aniq moslik

«notificationclick» bo'limidagi oynaTanla ni yaxshilang:

  1. Bir nechta oyna ochiq bo'lsa, fokusdagi (focused: true) oynani afzal ko'rsin (aniq moslik bo'lmaganda).
  2. Boshqa origin'dagi manzil (https:// bilan boshlanib, ilova origin'iga teng bo'lmagan) — { amal: "rad" }.
  3. Konsolda to'rt holatni tekshiring.

Ishora: origin — new URL(manzil).origin (URL).

Yechim
js
function oynaTanla(oynalar, manzil, origin) {
  if (new URL(manzil).origin !== origin) return { amal: "rad" };
  const aniq = oynalar.find((o) => o.url === manzil);
  if (aniq) return { amal: "fokus", url: aniq.url };
  const oyna = oynalar.find((o) => o.focused) ?? oynalar[0];
  if (oyna) return { amal: "o'tish", dan: oyna.url, url: manzil };
  return { amal: "ochish", url: manzil };
}

const o = "https://example.com";
const oynalar = [
  { url: `${o}/vazifalar/statistika`, focused: false },
  { url: `${o}/vazifalar/sozlamalar`, focused: true },
];
console.log(oynaTanla(oynalar, `${o}/vazifalar/statistika`, o).amal);
console.log(oynaTanla(oynalar, `${o}/vazifalar/`, o).dan);
console.log(oynaTanla([], `${o}/vazifalar/`, o).amal);
console.log(oynaTanla(oynalar, "https://boshqa.example/", o).amal);

Konsolda:

text
fokus
https://example.com/vazifalar/sozlamalar
ochish
rad

Ikkinchi holatda aniq moslik yo'q — fokusdagi sozlamalar oynasi tanlandi (statistika birinchi tursa ham). To'rtinchisi — boshqa origin: bildirishnoma hujumchi havolasi bilan kelgan bo'lsa ham ochilmaydi.

4-mashq: tajriba qadami (ixtiyoriy)

O'z kompyuteringizda: kichik sahifa (index.html + sw.js) yarating, SW'ga «Service Worker'da qabul qilish» bo'limidagi push va yaxshilangan notificationclick ni qo'ying. npx serve kabi lokal server bilan oching, tugma orqali ruxsat bering. DevTools → Application → Service Workers → Push maydoniga {"sarlavha":"«Bahor»","matn":"Osh tayyor"} yozib yuboring. Hisob va kalit kerak emas.

Yechim

Kutiladigan natija:

  • Push maydonidan yuborilganda — tizim bildirishnomasi ««Bahor»» / «Osh tayyor».
  • Ikki marta yuborsangiz — bitta bildirishnoma (bir xil tag).
  • Sahifa ochiq turganda bossangiz — yangi tab ochilmaydi, mavjudiga fokus.
  • Matn JSON bo'lmasa (salom) — sukut «Vazifalar» / «Yangi eslatma».
  • Bu sinov subscribe, VAPID va push xizmatini chetlab o'tadi — u faqat SW ishlovchisini tekshiradi.

11. Real ishda

  • Qayerda: buyurtma va yetkazish holati, chat xabarlari, bank tranzaksiyalari, kalendar eslatmalari. Telegram Web, Gmail, YouTube — hammasi Web Push ishlatadi.
  • Mahsulot ko'rsatkichi — ruxsat berish foizi. Yumshoq so'rash naqshi uni bir necha baravar oshiradi va denied ni kamaytiradi. Bu ko'pincha dasturchiga beriladigan aniq vazifa.
  • Intervyu savollari: "push oqimi qanday?", "VAPID nima uchun kerak?", "ruxsatni qachon so'raysiz?", "iOS'da qanday cheklovlar bor?", "410 kelsa nima qilasiz?".

Xulosa

  • Web push: sahifa → ruxsat → subscribe → server saqlaydi → server push xizmatiga (VAPID imzo, shifrlangan) → SW push → showNotification.
  • Ruxsat — bitta imkoniyat: faqat bosishdan keyin, foydali paytda, avval o'z UI'ingiz bilan ("yumshoq so'rash"). denied — faqat sozlamalardan.
  • VAPID: ochiq kalit (65 bayt) — brauzerga, yopiq — faqat serverda; har xabar ES256 JWT bilan imzolanadi.
  • SW: waitUntil, tag, data; bosilganda — ochiq oynaga fokus, faqat o'z origin'ingiz.
  • iOS — faqat bosh ekrandagi PWA. 404/410 obunani o'chiring.
  • Kanon: kalit va imzo lokal yaratildi; lokal yetkazish ishlamadi — vazifalar ga push o'z server bilan keladi.

Keyingi dars: Yuklanishni optimallashtirish: JS, shriftlar va resurslar — 21-qismda 225 KB gacha o'sgan birinchi yuklashni sahifalarni kech yuklash bilan qisqartiramiz va Lighthouse bilan o'lchaymiz.

Manbalar

  • MDN: Push API, PushManager.subscribe(), PushSubscription, ServiceWorkerRegistration.showNotification(), notificationclick, Clients.matchAll(), WindowClient.focus()
  • RFC 8030 (Generic Event Delivery Using HTTP Push), RFC 8291 (Message Encryption for Web Push), RFC 8292 (VAPID)
  • web-push npm paketi: generateVAPIDKeys, setVapidDetails, sendNotification
  • Firebase hujjati: Cloud Messaging — Web (getToken, vapidKey)
  • WebKit blogi: "Web Push for Web Apps on iOS and iPadOS" (iOS 16.4)
  • developer.chrome.com: permission UX va jimroq bildirishnoma so'rovlari
  • Kanon: tajriba/push/ (sw.js, sinov.mjs), natijalar/tajriba/30-web-push.txt (2026-10-17)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Web push bildirishnomalar: ruxsat, obuna, VAPID va Service Worker'da qabul qilish — IlmHamroh