Next.js
۸ دقیقه مطالعهساخت PWA در Next.js؛ پیادهسازی دستی در برابر Serwist
پیادهسازی دستی Service Worker را با Serwist مقایسه میکنیم؛ از استراتژی کش و حالت آفلاین تا بهروزرسانی، استقرار و محدودیتهای محیط واقعی.

یک PWA واقعی مجموعهای از تصمیمهای مشخص است: چه چیزی نصبپذیر باشد، در حالت آفلاین چه چیزی در دسترس بماند، کدام پاسخها کش شوند و کاربر چطور بدون ناسازگاری به نسخه جدید برود. Manifest بهتنهایی رفتار آفلاین ایجاد نمیکند. در این مقاله پیادهسازی دستی را با Serwist مقایسه میکنیم و اجزای هر روش را توضیح میدهیم.
۱. شروع با Web App Manifest
Manifest اطلاعاتی برای نصب و اجرای اپ مثل نام، آدرس شروع، حالت نمایش، رنگها و آیکنها را توصیف میکند؛ خودش صفحهای را کش نمیکند و درخواستها را رهگیری نمیکند. در App Router میتوان آن را با app/manifest.ts ساخت. وجود فایل آیکنها و رفتار نصب را در مرورگرهای پشتیبانیشده بررسی کن.
import type { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
id: "/",
name: "Arka Workspace",
short_name: "Arka",
description: "An installable workspace app",
start_url: "/",
scope: "/",
display: "standalone",
background_color: "#ffffff",
theme_color: "#0f766e",
icons: [
{ src: "/icons/icon-192.png", sizes: "192x192", type: "image/png" },
{ src: "/icons/icon-512.png", sizes: "512x512", type: "image/png" },
],
};
}۲. Service Worker دستی: چرخه عمر را بفهم
Service Worker اسکریپتی event-driven است که جدا از صفحه اجرا میشود. رویداد install برای پیشکشکردن پوسته کوچک برنامه، activate برای حذف کشهای منسوخ و fetch برای تعریف رفتار هر درخواست است. waitUntil اجازه میدهد کار ناهمگام پیش از پایان رویداد کامل شود.
const SHELL_CACHE = "shell-v1";
const IMAGE_CACHE = "images-v1";
const SHELL = ["/offline", "/icons/icon-192.png"];
self.addEventListener("install", (event) => {
event.waitUntil(caches.open(SHELL_CACHE).then((cache) => cache.addAll(SHELL)));
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil((async () => {
const keys = await caches.keys();
await Promise.all(keys.filter((key) => ![SHELL_CACHE, IMAGE_CACHE].includes(key)).map((key) => caches.delete(key)));
await self.clients.claim();
})());
});
self.addEventListener("fetch", (event) => {
const request = event.request;
const url = new URL(request.url);
if (request.method !== "GET" || url.origin !== self.location.origin) return;
if (request.mode === "navigate") {
event.respondWith(fetch(request).catch(async () => (await caches.match("/offline")) || Response.error()));
return;
}
if (request.destination === "image") {
event.respondWith((async () => {
const cached = await caches.match(request);
if (cached) return cached;
const response = await fetch(request);
if (response.ok) (await caches.open(IMAGE_CACHE)).put(request, response.clone());
return response;
})());
}
});۳. نصب Serwist و اتصال به تنظیمات Next.js
Serwist ابزارهایی برای precache فایلهای build و اجرای قوانین runtime caching فراهم میکند تا مجبور نباشی همه رویدادهای سطح پایین را دستی مدیریت کنی. wrapper تنظیمات Next.js را میپوشاند تا build خروجی Service Worker را بسازد و فهرست precache را تزریق کند. سازگاری پکیجها ممکن است تغییر کند؛ نسخهها را با مستندات فعلی Serwist و نسخه Next.js پروژه هماهنگ کن.
npm install @serwist/next serwistimport type { NextConfig } from "next";
import withSerwistInit from "@serwist/next";
const withSerwist = withSerwistInit({
swSrc: "src/app/sw.ts",
swDest: "public/sw.js",
disable: process.env.NODE_ENV === "development",
});
const nextConfig: NextConfig = {};
export default withSerwist(nextConfig);۴. کد Worker در Serwist را خطبهخط بخوان
swSrc مسیر سورس Worker و swDest مسیر فایل خروجی عمومی است. مقدار __SW_MANIFEST هنگام build تزریق میشود و فایلهای قابل precache را فهرست میکند. defaultCache مجموعهای پایه از قوانین runtime است؛ آن را بررسی کن و فرض نکن با نیازهای حریم خصوصی و تازگی داده پروژه تو سازگار است. skipWaiting و clientsClaim کنترل را زودتر به Worker جدید میدهند؛ این کار راحت است اما ممکن است تب قدیمی را با assetهای نسخه جدید ترکیب کند. اگر این ریسک مهم است، جریان «نسخه جدید آماده است» طراحی کن.
import { defaultCache } from "@serwist/next/worker";
import type { PrecacheEntry, SerwistGlobalConfig } from "serwist";
import { Serwist } from "serwist";
declare global {
interface WorkerGlobalScope extends SerwistGlobalConfig {
__SW_MANIFEST: (PrecacheEntry | string)[];
}
}
declare const self: ServiceWorkerGlobalScope;
const serwist = new Serwist({
precacheEntries: self.__SW_MANIFEST,
skipWaiting: true,
clientsClaim: true,
navigationPreload: true,
runtimeCaching: defaultCache,
});
serwist.addEventListeners();۵. ثبت Worker فقط در مرورگر
کامپوننت ثبت باید Client Component باشد، چون navigator فقط در مرورگر وجود دارد. ثبت فقط در production از رفتار گیجکننده در توسعه و کش قدیمی هنگام hot reload جلوگیری میکند. این کامپوننت را یکبار در layout اصلی رندر کن. موفقیت در ثبت به معنای آفلاینبودن کامل برنامه نیست؛ مسیرها، قوانین کش و آزمایش واقعی production هم لازماند.
"use client";
import { useEffect } from "react";
export function PwaRegister() {
useEffect(() => {
if (!("serviceWorker" in navigator)) return;
if (process.env.NODE_ENV !== "production") return;
navigator.serviceWorker.register("/sw.js").catch((error) => {
console.error("Service worker registration failed", error);
});
}, []);
return null;
}۶. برای هر منبع استراتژی کش مناسب انتخاب کن
Cache First برای assetهای نسخهدار و کمتغییر مناسب است؛ Network First وقتی تازگی محتوا مهم است اما fallback کششده قابل قبول است؛ و Stale While Revalidate پاسخ کششده را سریع برمیگرداند و در پسزمینه بهروزرسانی میکند. پاسخ API احراز هویتشده، نتیجه پرداخت یا HTML مخصوص کاربر را کورکورانه کش نکن. کلید کش، انقضا، سهمیه و invalidation بخشی از طراحی محصولاند.
۷. آپدیت، chunk قدیمی و fallback آفلاین
نسخه جدید Next.js ممکن است chunkهای JavaScript قدیمی را حذف کند، درحالیکه تب باز هنوز به آنها ارجاع میدهد. فعالسازی تهاجمی میتواند این ناسازگاری را ایجاد کند. صفحه آفلاین را در precache قرار بده، کشهای runtime خودت را نسخهبندی کن، فقط کشهایی را حذف کن که مالکشان هستی و ارتقا از نسخه قبلی را آزمایش کن. عملیات نوشتنی آفلاین به صف و retry جداگانه نیاز دارد؛ کششدن صفحه به معنای کارکرد نوشتن آفلاین نیست.
۸. چکلیست تست production
- برنامه را build کن و نسخه production را روی HTTPS یا localhost اجرا کن.
- در DevTools بخش Application → Service Workers و Cache Storage را بررسی کن.
- ناوبری آفلاین، تصویر ناموجود، انتشار نسخه جدید و بازیابی پس از پاککردن داده سایت را تست کن.
- مطمئن شو پاسخ API خصوصی بین حسابهای مختلف به اشتراک گذاشته نمیشود.
Worker دستی یا Serwist؟
وقتی قوانین کوچک، خاص و قابلآزمایشاند کد دستی انتخاب خوبی است. وقتی precache، چند استراتژی runtime و یکپارچگی با build لازم داری، Serwist نگهداری را آسانتر میکند. هیچکدام جای طراحی دقیق کش، بازبینی امنیت و تست ارتقا را نمیگیرند.