Next.js
قراءة في 8 دقائقبناء تطبيق PWA في Next.js: عامل الخدمة يدويًا أم Serwist
قارن بين كتابة Service Worker يدويًا واستخدام Serwist، من استراتيجيات التخزين والعمل دون اتصال إلى التحديث والنشر ومفاضلات الإنتاج.

يتطلب PWA الإنتاجي قرارات واضحة: ما الذي يمكن تثبيته، وما الذي يبقى متاحًا دون اتصال، وما الاستجابات التي يمكن تخزينها، وكيف ينتقل المستخدم بأمان بين الإصدارات. لا ينشئ manifest وحده سلوكًا دون اتصال. يقارن هذا الدليل عاملًا يدويًا صغيرًا مع Serwist ويشرح الأجزاء.
١. ابدأ بملف 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" },
],
};
}٢. عامل الخدمة اليدوي: افهم دورة الحياة
عامل الخدمة سكربت يعمل بالأحداث وبشكل منفصل عن الصفحة. يتيح 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 أدوات للتخزين المسبق لملفات البناء وتطبيق قواعد التخزين أثناء التشغيل دون إدارة كل حدث منخفض المستوى يدويًا. يغلّف التكامل إعدادات Next.js كي ينتج البناء ملف عامل الخدمة ويحقن قائمة التخزين المسبق. قد تتغير توافقية الحزم؛ طابق الإصدارات مع توثيق 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);٤. اقرأ عامل Serwist سطرًا بسطر
يشير swSrc إلى مصدر العامل وswDest إلى الملف العام الناتج. يحقن البناء __SW_MANIFEST الذي يسرد الملفات المؤهلة للتخزين المسبق. توفر defaultCache قواعد أساسية للتخزين أثناء التشغيل؛ راجعها ولا تفترض أنها تلائم متطلبات الخصوصية وحداثة البيانات. يجعل skipWaiting وclientsClaim العامل الجديد يتولى التحكم بسرعة، لكن قد يخلطان تبويبًا قديمًا بأصول جديدة. عند الحاجة، نفذ تدفقًا صريحًا لإشعار المستخدم بوجود تحديث.
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();٥. سجّل العامل في المتصفح فقط
يجب أن يكون مكوّن التسجيل Client Component لأن navigator موجود في المتصفح فقط. التسجيل في الإنتاج فقط يتجنب سلوك التطوير المربك والتخزين القديم أثناء التحديث الساخن. اعرض المكوّن مرة واحدة في layout الجذر. نجاح التسجيل لا يعني أن التطبيق يعمل بالكامل دون اتصال؛ فلا بد من قواعد صحيحة واختبار إنتاجي.
"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 الأصول الثابتة ذات الإصدارات، بينما يناسب Network First المحتوى الذي تتطلب حداثته أولوية مع قبول بديل مخزن، ويعيد Stale While Revalidate الاستجابة المخزنة بسرعة مع تحديثها في الخلفية. لا تخزن عشوائيًا استجابات API المصادق عليها أو نتائج الدفع أو HTML المخصص للمستخدم. مفاتيح التخزين والانتهاء والإبطال جزء من تصميم المنتج.
٧. التحديثات والملفات القديمة والبديل دون اتصال
قد يزيل إصدار Next.js المنشور ملفات JavaScript القديمة بينما لا يزال تب مفتوح يشير إليها. قد يؤدي التفعيل السريع إلى هذا التعارض. أدرج صفحة عدم الاتصال في قائمة التخزين المسبق، وأصدر نسخًا للتخزين الذي تديره، ولا تحذف إلا التخزين الذي تملكه، واختبر الترقية من إصدار سابق. تحتاج الكتابات دون اتصال إلى طابور وإعادة محاولة منفصلين؛ تخزين الصفحة لا يجعل الكتابة تعمل دون اتصال.
٨. قائمة اختبار الإنتاج
- ابنِ التطبيق وشغّل نسخة الإنتاج عبر HTTPS أو localhost.
- افحص Service Workers وCache Storage في أدوات المطور.
- اختبر التنقل دون اتصال والصور المفقودة والنشر الجديد والاستعادة بعد مسح بيانات الموقع.
- تأكد من عدم مشاركة استجابات API الخاصة بين الحسابات.
عامل يدوي أم Serwist؟
اختر الكود اليدوي عندما تكون القواعد صغيرة وخاصة وسهلة الاختبار. اختر Serwist عند الحاجة إلى التخزين المسبق واستراتيجيات متعددة وتكامل مع البناء. لا يغني أي منهما عن تصميم التخزين بعناية ومراجعة الأمان واختبار الترقية.