بازگشت به وبلاگ

راه‌اندازی کامل PWA در Next.js: از Service Worker تا Push Notification و Background Sync

۲ مرداد ۱۴۰۵4 دقیقه مطالعه
راه‌اندازی کامل PWA در Next.js: از Service Worker تا Push Notification و Background Sync

اپلیکیشن‌های وب پیشرو (PWA) مرز بین «سایت» و «اپلیکیشن نصب‌شده» رو کمرنگ می‌کنن. توی Next.js، رسیدن به یه PWA واقعاً آماده‌ی production یعنی چهارتا بخش با هم درست کار کنن: یه manifest، یه Service Worker با استراتژی کش هدفمند، Push Notification، و Background Sync برای لحظه‌هایی که کاربر آفلاینه. این‌جا می‌بینیم این چهارتا چطور کنار هم جفت‌وجور می‌شن.

۱. Manifest — قابل‌نصب کردن اپ

فایل manifest.json به مرورگر (و پرامپت نصب سیستم‌عامل) می‌گه اپ چه اسمی داره، چه شکلیه، و بعد از نصب چطور رفتار می‌کنه.

// public/manifest.json
{
  "name": "Ali Rahimi — Frontend Developer",
  "short_name": "Ali Rahimi",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#4338CA",
  "icons": [
    { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
  ]
}

توی root layout بهش لینک بده، همراه با theme-color:

// src/app/layout.tsx
export const metadata = {
  manifest: "/manifest.json",
  themeColor: "#4338CA",
};

۲. Service Worker — موتور اصلی ماجرا

Service Worker یه اسکریپته که توی یه ترد جدا اجرا می‌شه، درخواست‌های شبکه رو قطع می‌کنه، و تصمیم می‌گیره باهاشون چیکار کنه. یه‌بار، بعد از hydrate شدن صفحه، ثبتش کن:

// src/lib/register-sw.ts
if (typeof window !== "undefined" && "serviceWorker" in navigator) {
  window.addEventListener("load", () => {
    navigator.serviceWorker.register("/sw.js").catch(console.error);
  });
}

۳. استراتژی‌های کش — جایی که اکثراً اشتباه می‌کنن

همه‌ی درخواست‌ها نباید یه‌جور کش بشن. انتخاب استراتژی درست برای هر نوع درخواست، فرقیه بین اپی که حس آنی داره و اپی که داده‌ی بیات تحویل می‌ده.

استراتژی

رفتار

مناسب برای

Cache First

اول از کش سرو کن؛ فقط موقع miss شدن کش برو سراغ شبکه

فونت‌ها، آیکون‌ها، فایل‌های استاتیک نسخه‌دار

Network First

اول شبکه؛ اگه fail شد برگرد سراغ کش

صفحات HTML، داده‌ی APIای که مدام تغییر می‌کنه

Stale-While-Revalidate

نسخه‌ی کش‌شده رو فوری سرو کن، پشت‌صحنه کش رو آپدیت کن

آواتارها، پاسخ‌های API غیرحیاتی

Network Only

هیچ‌وقت کش نکن — همیشه برو سراغ شبکه

پرداخت، endpoint های احرازهویت، هرچی حساسه

Cache Only

هیچ‌وقت سراغ شبکه نرو — کش باید از قبل پر شده باشه

اسکلت اپ بعد از نصب

یه Service Worker ساده که دوتا از این‌ها رو پیاده می‌کنه:

// public/sw.js
const STATIC_CACHE = "static-v1";
const APP_SHELL = ["/", "/offline", "/icons/icon-192.png"];

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(STATIC_CACHE).then((cache) => cache.addAll(APP_SHELL))
  );
  self.skipWaiting();
});

self.addEventListener("fetch", (event) => {
  const { request } = event;

  // Cache-first برای فایل‌های استاتیک
  if (request.destination === "image" || request.destination === "font") {
    event.respondWith(
      caches.match(request).then((cached) => cached || fetch(request))
    );
    return;
  }

  // Network-first برای navigation (صفحات HTML)، با fallback به صفحه‌ی آفلاین
  if (request.mode === "navigate") {
    event.respondWith(
      fetch(request).catch(() => caches.match("/offline"))
    );
  }
});

توی عمل، به‌جای دست‌ساز نوشتنش، از next-pwa یا Serwist استفاده کن — این‌ها از روی یه config یه Service Worker تنظیم‌شده می‌سازن و edge caseهایی (مثل cache-busting بر اساس hash بیلد) رو خودشون مدیریت می‌کنن. فهمیدن استراتژی‌های بالا دقیقاً همون چیزیه که کمک می‌کنه درست پیکربندیشون کنی.

۴. Push Notification

سه بخش متحرک داره: یه subscription که توی مرورگر ساخته می‌شه، کلیدهای VAPID برای احراز هویت سرورت، و یه push handler توی Service Worker.

// کلاینت: اجازه بگیر و subscribe کن
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: urlBase64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!),
});

// subscription رو بفرست سمت سرور تا برای این کاربر ذخیره بشه
await fetch("/api/push/subscribe", {
  method: "POST",
  body: JSON.stringify(subscription),
});
// public/sw.js — گرفتن رویداد push
self.addEventListener("push", (event) => {
  const data = event.data?.json() ?? {};
  event.waitUntil(
    self.registration.showNotification(data.title ?? "به‌روزرسانی جدید", {
      body: data.body,
      icon: "/icons/icon-192.png",
      data: { url: data.url ?? "/" },
    })
  );
});

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

سمت سرور، با کتابخونه‌ای مثل web-push و جفت‌کلید VAPID خودت، پیام واقعی رو به subscription ذخیره‌شده می‌فرستی — همون چیزی که رویداد push بالا رو trigger می‌کنه.

۵. Background Sync — دووم آوردن جلوی اتصال ناپایدار

Background Sync بهت اجازه می‌ده یه درخواست fail‌شده (مثل ارسال فرم موقع آفلاین بودن) رو به تعویق بندازی، و مرورگر خودش دوباره امتحانش می‌کنه به‌محض برگشتن اتصال — حتی اگه کاربر تب رو بسته باشه.

// کلاینت: موقع submit، اگه آفلاین بودیم به‌جای fail کردن یه sync tag ثبت کن
async function submitMessage(data: FormData) {
  try {
    await fetch("/api/contact", { method: "POST", body: data });
  } catch {
    const registration = await navigator.serviceWorker.ready;
    await saveToIndexedDB(data); // اول توی صف محلی ذخیره‌ش کن
    await registration.sync.register("sync-contact-form");
  }
}
// public/sw.js
self.addEventListener("sync", (event) => {
  if (event.tag === "sync-contact-form") {
    event.waitUntil(flushQueuedFormsFromIndexedDB());
  }
});

Background Sync (نوع one-off بالا) تا زمان نوشتن این متن فقط روی مرورگرهای Chromium کار می‌کنه — باهاش مثل یه بهبود تدریجی رفتار کن، نه تنها استراتژی آفلاینت. کاربرهای Safari/Firefox رتری خودکار نمی‌گیرن؛ مطمئن شو مسیر رتری دستی هم براشون کار می‌کنه.

چک‌لیست

  1. manifest.json لینک و validate شده (تست installability توی Lighthouse پاس می‌شه)

  2. Service Worker بعد از hydrate ثبت شده، نه قبلش که جلوی اولین رندر رو بگیره

  3. استراتژی کش هدفمند برای هر نوع درخواست — نه یه قانون کلی برای همه

  4. کلیدهای VAPID ساخته شده، subscription سمت سرور ذخیره شده، push handler روی یه دستگاه واقعی تست شده

  5. Background sync به‌عنوان بهبود تدریجی، با یه مسیر رتری دستی برای مرورگرهای پشتیبانی‌نشده

این چهارتا رو درست انجام بده، فرقش فوری معلومه: بارگذاری آنیِ بازدید‌های بعدی، نوتیفیکیشن‌هایی که کاربر رو برمی‌گردونن، و فرم‌هایی که با یه اتصال ناپایدار داده از دست نمی‌دن.