Как подружить PWA-функционал и Next.js-приложение с помощью пакета next-pwa-pack


Мой путь к созданию пакета
Процесс создания пакета
Преимущества next-pwa-pack
- Автоматизация регистрации сервис-воркера – не нужно вручную писать код для регистрации и управления сервис-воркером;
- Важные элементы копируются в проект и их можно редактировать под специфические нужды;
- Управление кэшем – пакет предоставляет удобные функции для очистки, обновления и отключения кэша;
- Синхронизация между вкладками – автоматическое обновление кэша во всех открытых вкладках;
- Офлайн-режим – приложение работает без интернета;
- Удобные инструменты для разработки – встроенная панель разработчика для управления PWA-состоянием;
- Инструменты для обновления и ревалидации кэша с сервера – поддержка server actions, API routes и интеграция с внешними системами.
Что происходит при установке
- sw.js – сервис-воркер с готовой логикой кэширования;
- offline.html – страница для офлайн-режима;
- manifest.json – конфигурация PWA (требует настройки под ваш проект).
node node_modules/next-pwa-pack/scripts/copy-pwa-files.mjs
# или
npx next-pwa-pack/scripts/copy-pwa-files.mjs
"use server";
export async function revalidatePWA(urls: string[]) {
const baseUrl = process.env.NEXT_PUBLIC_HOST || "http://localhost:3000";
const res = await fetch(`${baseUrl}/api/pwa/revalidate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
urls,
secret: process.env.REVALIDATION_SECRET,
}),
});
return res.json();
}
node node_modules/next-pwa-pack/scripts/copy-pwa-server-actions.mjs
Настройка manifest.json
{
"name": "Моё приложение",
"short_name": "Моё приложение",
"description": "Описание моего приложения",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
Быстрый старт
import { PWAProvider } from "next-pwa-pack";
export default function layout({ children }) {
return <PWAProvider>{children}</PWAProvider>;
}
// /middleware.ts
import { withPWA } from "next-pwa-pack/hoc/withPWA";
function originalMiddleware(request) {
// ...ваша логика
return response;
}
export default withPWA(originalMiddleware, {
revalidationSecret: process.env.REVALIDATION_SECRET!,
sseEndpoint: "/api/pwa/cache-events",
webhookPath: "/api/pwa/revalidate",
});
export const config = {
matcher: ["/", "/(ru|en)/:path*", "/api/pwa/:path*"],
};
```
**Аргументы хока:**
- `originalMiddleware` — ваша функция middleware (например, для i18n, авторизации и т.д.)
- `revalidationSecret` — секрет для авторизации запросов на ревалидацию, нужен, чтобы никто кроме вас не мог в него стучаться
- `sseEndpoint` — endpoint для SSE-событий (по умолчанию `/api/pwa/cache-events`), измените его, если данный роут уже занят вашим приложением
- `webhookPath` — endpoint для webhook-ревалидации (по умолчанию `/api/pwa/revalidate`), по нему методом POST можно осуществлять ревалидацию кеша на сервере или извне, так же на нем завязана функция revalidatePWA
**Важно:**
В `config.matcher` обязательно укажите пути, которые должны обрабатываться этим middleware (например, корень сайта, локализованные маршруты и PWA endpoints).
Что внутри PWAProvider
RegisterSW
CacheCurrentPage
SWRevalidateListener
SSERevalidateListener
DevPWAStatus
import { PWAProvider } from "next-pwa-pack";
export default function layout({ children }) {
return <PWAProvider devMode>{children}</PWAProvider>;
}
- Очистка кэша;
- Перезагрузка сервис-воркера;
- Обновление кэша страницы;
- Удаление сервис-воркера и кэша;
- Включение/отключение кэша PWA.
Что делает сервис-воркер?
- Кэширует HTML с TTL (время жизни) по умолчанию 10 минут. Можно изменить в файле /sw.js;
import { PWAProvider } from "next-pwa-pack";
export default function layout({ children }) {
return <PWAProvider swPath="/some-path/sw.js">{children}</PWAProvider>
;
}
- При истечении TTL автоматически обновляет кэш;
- Показывает кэшированную версию при отсутствии интернета
- CSS, JavaScript, изображения кэшируются навсегда;
- Улучшает скорость загрузки при повторных посещениях;
- Работает только с GET-запросами (безопасность).
- CACHE_CURRENT_HTML — кэширование текущей страницы;
- REVALIDATE_URL – принудительное обновление кэша;
- DISABLE_CACHE / ENABLE_CACHE – включение/отключение кэша;
- SKIP_WAITING – активация новой версии;
- CLEAR_STATIC_CACHE — сброс кэша статических данных и API-ответов (например, после серверной ревалидации или обновления данных через SSE);
- Автоматически показывает offline.html при отсутствии интернета и кеша текущей страницы;
- Пытается загрузить свежую версию при восстановлении соединения.
HOC withPWA
export default withPWA(originalMiddleware, {
revalidationSecret: process.env.REVALIDATION_SECRET!,
sseEndpoint: "/api/pwa/cache-events",
webhookPath: "/api/pwa/revalidate",
});
- `originalMiddleware` — ваша функция middleware (например, для i18n, авторизации и т.д.)
- `revalidationSecret` — секрет для авторизации запросов на ревалидацию, нужен, чтобы никто кроме вас не мог в него стучаться
- `sseEndpoint` — endpoint для SSE-событий (по умолчанию `/api/pwa/cache-events`), измените его, если данный роут уже занят вашим приложением
- `webhookPath` — endpoint для webhook-ревалидации (по умолчанию `/api/pwa/revalidate`), по нему методом POST можно осуществлять ревалидацию кеша на сервере или извне, так же на нем завязана функция revalidatePWA
Примеры использования
Обновление кэша после изменения данных
import { updateSWCache } from "next-pwa-pack";
// После успешного создания поста
const handleCreatePost = async (data) => {
await createPost(data);
// Обновляем кэш вкладок с блогом и дашбордом
updateSWCache(["/blog", "/dashboard"]);
};
import { revalidatePWA } from "../actions";
await createPost(data);
await revalidatePWA(["/my-page"]);
Очистка кэша при выходе пользователя
import { clearAllCache } from "next-pwa-pack";
const handleLogout = async () => {
await logout();
await clearAllCache(); // Очищаем все кэши
router.push("/login");
};
Уведомление о новой версии
import { usePWAStatus } from "next-pwa-pack";
function UpdateNotification() {
const { hasUpdate, update } = usePWAStatus();
if (hasUpdate) {
return (
<div className="update-banner">
<p>Доступна новая версия приложения</p>
<button onClick={update}>Обновить</button>
</div>
);
}
return null;
}
Краткое описание всех экспортируемых экшенов
import {
clearAllCache,
reloadServiceWorker,
updatePageCache,
unregisterServiceWorkerAndClearCache,
updateSWCache,
disablePWACache,
enablePWACache,
clearStaticCache,
usePWAStatus,
} from "next-pwa-pack";
// Очищает все кэши, связанные с сервис-воркером.
await clearAllCache();
// Перезагружает сервис-воркер и обновляет страницу.
await reloadServiceWorker();
// Обновляет кэш для указанной страницы (или текущей, если не указано).
await updatePageCache("/about");
// Отключает сервис-воркер и очищает кэш.
await unregisterServiceWorkerAndClearCache();
// Запускает сигнал для всех вкладок и обновляет кэш в текущей вкладке.
// Может быть вызван после revalidateTag на клиенте.
// Сбрасывает кэш статических данных и API-ответов.
await clearStaticCache();
updateSWCache(["/page1", "/page2"]);
// Глобально отключает кэш PWA (до перезагрузки или вызова enablePWACache).
disablePWACache();
// Глобально включает кэш PWA (после вызова disablePWACache).
enablePWACache();
const { online, hasUpdate, swInstalled, update } = usePWAStatus();
// - `online` — онлайн/оффлайн статус
// - `hasUpdate` — доступно ли обновление
// - `swInstalled` — установлен ли сервис-воркер
// - `update()` — активировать новую версию приложения
Пример: API Route для внешней ревалидации
// app/api/webhook/revalidate/route.ts
import { NextRequest, NextResponse } from "next/server";
import { revalidatePWA } from "@/app/actions";
import { revalidateTag } from "next/cache";
import { FetchTags } from "@/app/api/endpoints/backend";
interface RevalidateRequest {
tags?: string[];
secret: string;
urls?: string[];
}
interface RevalidateResponse {
success: boolean;
message: string;
tagsRevalidated: boolean;
urlsRevalidated: boolean;
tags: string[];
urls: string[];
successful: number;
failed: number;
timestamp: string;
}
export async function POST(request: NextRequest) {
try {
const { tags, secret, urls }: RevalidateRequest = await request.json();
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
let successful = 0;
let failed = 0;
let tagsRevalidated = false;
let urlsRevalidated = false;
const validTags = Object.values(FetchTags);
const invalidTags =
tags?.filter((tag) => !validTags.includes(tag as any)) || [];
if (invalidTags.length > 0) {
return NextResponse.json(
{ error: `Invalid tags: ${invalidTags.join(", ")}` },
{ status: 400 }
);
}
if (tags && tags.length > 0) {
const tagResults = await Promise.allSettled(
tags.map((tag) => revalidateTag(tag as FetchTags))
);
successful = tagResults.filter((r) => r.status === "fulfilled").length;
failed = tagResults.filter((r) => r.status === "rejected").length;
tagsRevalidated = true;
}
if (urls && urls.length > 0) {
await revalidatePWA(urls);
urlsRevalidated = true;
}
const response: RevalidateResponse = {
success: true,
message: "Cache revalidation completed",
tagsRevalidated,
urlsRevalidated,
tags: tags || [],
urls: urls || [],
successful,
failed,
timestamp: new Date().toISOString(),
};
return NextResponse.json(response);
} catch (error) {
console.error("Webhook revalidation error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
POST: https://my-app(или localhost:3000)/api/webhook/revalidate
body: {
"tags": ["faq"],
"secret": "1234567890",
"urls": ["/ru/question-answer"]
}
response: {
"success": true,
"message": "Cache revalidation completed",
"tagsRevalidated": true,
"urlsRevalidated": true,
"tags": [
"faq"
],
"urls": [
"/ru/question-answer"
],
"successful": 1,
"failed": 0,
"timestamp": "2025-07-21T12:43:47.819Z"
}
Отладка и мониторинг
- Откройте DevTools → Application → Service Workers.
- Убедитесь, что сервис-воркер зарегистрирован.
- Перейдите в Cache Storage → html-cache-v2.
- Проверьте, что страницы кэшируются.
- Включите devMode в PWAProvider.
- Откройте панель разработчика (зеленая точка в левом нижнем углу).
- Отключите интернет в DevTools → Network → Offline.
- Обновите страницу — должна показаться offline.html.
- [PWA] Service Worker registered — успешная регистрация;
- [SW] Cached: /about — страница закэширована;
- [SW] Revalidated and updated cache for: /blog — кэш обновлен.
Ограничения и особенности пакета
- HTTPS обязателен для PWA в продакшене;
- Кэшируются только GET-запросы (API-вызовы не кэшируются);
- Чувствительные данные не сохраняются в кэше.
- Пакет не влияет на производительность в обычном режиме;
- Улучшает скорость загрузки при повторных посещениях.
- TTL кэша (10 минут) можно изменить только в sw.js;
- Исключения из кэширования настраиваются в CACHE_EXCLUDE;
- Manifest.json требует ручной настройки под проект.
- Серверный экшен revalidatePWA копируется в проект и его можно редактировать;
- HOC withPWA имеет свои пропсы для более гибкой настройки под проект;
- PWAProvider имеет свои пропсы для управления:
export default function PWAProvider({
children,
swPath,
devMode = false,
serverRevalidation = { enabled: true, sseEndpoint: "/api/pwa/cache-events" },
}: PWAProviderProps) {
- Настройка TTL через конфиг — без редактирования sw.js;
- Поддержка push-уведомлений — для уведомлений пользователей;
- Более гибкая настройка кэширования — по паттернам URL;
- Метрики производительности — мониторинг эффективности кэширования.