refactor(pwa): migrate from @ducanh2912/next-pwa to @serwist/turbopack
Next.js 16 ships Turbopack as the default bundler, but
@ducanh2912/next-pwa is webpack-only — the previous PWA setup
required `--webpack` on dev/build/"dev:proxy" scripts, accepting
a perf regression in exchange for working PWA install.
@serwist/turbopack (paired with the official 'serwist' package
and 'esbuild') is the Turbopack-native successor: it works with
the default bundler, no opt-out flag, and gives us a real TS
service worker source file we can read and edit.
What this commit does:
1. Swap deps in package.json:
- Remove @ducanh2912/next-pwa
- Add @serwist/turbopack, serwist, esbuild (all devDeps)
- Drop --webpack flag from dev / dev:proxy / build scripts
(Turbopack is restored as the bundler)
2. next.config.ts:
- Replace withPWAInit wrapping with withSerwist named import
- Update comment block to describe new architecture
3. New src/app/sw.ts: Serwist service worker source.
skipWaiting + clientsClaim + navigationPreload + defaultCache.
This file is compiled to a real SW at build time by Serwist.
4. New src/app/serwist/[[...slug]]/route.ts: catch-all Route
Handler that serves the compiled SW at /serwist/sw.js plus
Serwist's chunked runtime assets. Catch-all is required
because Serwist serves multiple files under /serwist/.
5. src/app/layout.tsx: replace <SwRegister /> with
<SerwistProvider swUrl="/serwist/sw.js"> from
@serwist/turbopack/react. The provider handles registration
+ lifecycle internally.
6. Delete src/app/_components/core/sw-register.tsx: replaced
by SerwistProvider.
7. .gitignore: add public/sw.js and public/workbox-*.js
(defensive — these are build artifacts that change hash
every build and shouldn't be committed).
8. Untrack public/workbox-3c9d0171.js: stale workbox runtime
from the previous next-pwa build, hash is build-specific
so it can never be regenerated correctly.
Not modified (intentionally):
- src/utils/pwa.ts: pure beforeinstallprompt browser-API wrapper,
library-agnostic, stays as-is.
- pwa-install-overlay / pwa-install-dialog: install UI flow
unchanged.
- public/manifest.json: still works; layout's metadata.manifest
points at it.
Verification:
- pnpm install resolved cleanly (added 24 pkgs, removed 209)
- npx tsc --noEmit passes (EXIT=0)
- Serwist provides named exports, not default — withSerwist and
createSerwistRoute are both named imports
This commit is contained in:
@@ -1,34 +0,0 @@
|
||||
"use client";
|
||||
/**
|
||||
* Service Worker 注册器
|
||||
*
|
||||
* 背景:`@ducanh2912/next-pwa` 配置 `register: false`,不自动注入注册脚本。
|
||||
* App Router 下手动注册更可控 —— 不污染 SSR / hydration,可显式做生产 / 开发分流。
|
||||
*
|
||||
* 挂载位置:layout body 末尾,全站生效。
|
||||
*
|
||||
* 行为:
|
||||
* - dev (`NODE_ENV === "development"`):跳过注册
|
||||
* 1. next-pwa `disable: true` 时 dev 不生成 sw.js,注册会 404
|
||||
* 2. dev HMR + 老 SW cache 互相干扰,开发体验差
|
||||
* - 生产:调 `navigator.serviceWorker.register("/sw.js", { scope: "/" })`
|
||||
* - `scope: "/"` 与 manifest.scope 一致
|
||||
* - `updateViaCache: "none"` 强制每次都拉新版 SW(next-pwa 默认就是这行为,显式更清晰)
|
||||
*/
|
||||
import { useEffect } from "react";
|
||||
|
||||
export function SwRegister() {
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
if (!("serviceWorker" in navigator)) return;
|
||||
if (process.env.NODE_ENV !== "production") return;
|
||||
|
||||
void navigator.serviceWorker.register("/sw.js", {
|
||||
scope: "/",
|
||||
updateViaCache: "none",
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 无 UI —— 纯副作用组件
|
||||
return null;
|
||||
}
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { SerwistProvider } from "@serwist/turbopack/react";
|
||||
import { SessionProvider } from "@/providers/session-provider";
|
||||
import { geistSans, geistMono, athelas } from "@/core/fonts";
|
||||
import "./globals.css";
|
||||
|
||||
import { SwRegister } from "@/app/_components/core/sw-register";
|
||||
import { RootProviders } from "@/providers/root-providers";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -51,9 +51,9 @@ export default function RootLayout({
|
||||
{/* NextAuth SessionProvider:为所有客户端组件提供 useSession() 上下文 */}
|
||||
<SessionProvider>
|
||||
<RootProviders>
|
||||
{children}
|
||||
{/* PWA Service Worker 注册器 —— 生产构建挂载 /sw.js(@ducanh2912/next-pwa 生成) */}
|
||||
<SwRegister />
|
||||
{/* PWA Service Worker 注册器 —— Serwist Turbopack 在 build 时编译 app/sw.ts,
|
||||
通过 /serwist/sw.js 路由 handler 服务。SerwistProvider 内部处理注册 + 生命周期。 */}
|
||||
<SerwistProvider swUrl="/serwist/sw.js">{children}</SerwistProvider>
|
||||
</RootProviders>
|
||||
</SessionProvider>
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createSerwistRoute } from "@serwist/turbopack";
|
||||
|
||||
/**
|
||||
* Cozsweet Serwist Service Worker Route Handler
|
||||
*
|
||||
* 路径:捕获 `/serwist/*` 所有子路径(典型是 `/serwist/sw.js` 主 SW + 一些 Serwist 内部 chunk)
|
||||
* 因此 `[[...slug]]` 是必须的 —— 仅 `app/serwist/sw.js` 一条不够,Serwist 还会
|
||||
* 拉取 `/serwist/<chunk>.js` 等辅助资源。
|
||||
*
|
||||
* `createSerwistRoute` 在 build 阶段用 esbuild 把 `src/app/sw.ts` 编译成真实 SW,
|
||||
* 并在 runtime 把编译产物通过 GET handler 吐出去。
|
||||
*
|
||||
* Revision 用 `git rev-parse HEAD` —— 每次 commit 变化让 precache 失效,强制客户端
|
||||
* 拉新 SW。fallback 到 randomUUID(罕见的非 git 环境兜底)。
|
||||
*/
|
||||
const revision =
|
||||
spawnSync("git", ["rev-parse", "HEAD"], { encoding: "utf-8" }).stdout ??
|
||||
crypto.randomUUID();
|
||||
|
||||
export const { dynamic, dynamicParams, revalidate, generateStaticParams, GET } =
|
||||
createSerwistRoute({
|
||||
additionalPrecacheEntries: [{ url: "/~offline", revision }],
|
||||
swSrc: "src/app/sw.ts",
|
||||
useNativeEsbuild: true,
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
/// <reference lib="esnext" />
|
||||
/// <reference lib="webworker" />
|
||||
/**
|
||||
* Cozsweet PWA Service Worker (Serwist source)
|
||||
*
|
||||
* 编译链:
|
||||
* src/app/sw.ts (此文件)
|
||||
* → esbuild (Serwist Turbopack 在 build 时跑)
|
||||
* → /serwist/sw.js (App Router Route Handler 服务)
|
||||
*
|
||||
* precache 列表由 `withSerwist` 在 build 时注入 `self.__SW_MANIFEST`。
|
||||
* Runtime cache 用 `defaultCache`(Serwist 推荐的 Google Fonts / Static / Image
|
||||
* 缓存策略)—— 对聊天类应用够用。
|
||||
*
|
||||
* 配置:
|
||||
* - skipWaiting: true 新 SW 立即 activate,避免旧 SW 卡住页面
|
||||
* - clientsClaim: true 新 SW 立即接管所有打开的 client
|
||||
* - navigationPreload: 用 preload API 加速文档请求(资源请求 SW 完成前先发)
|
||||
*
|
||||
* Offline fallback:`/~offline` 路由(next.config.ts Serwist 配置里也登记了
|
||||
* `additionalPrecacheEntries`,双保险)。注意本项目目前**没实现**这个路由,
|
||||
* 真要落地需要新增 src/app/~/offline/page.tsx —— 未做。
|
||||
*/
|
||||
import { defaultCache } from "@serwist/turbopack/worker";
|
||||
import type { PrecacheEntry, SerwistGlobalConfig } from "serwist";
|
||||
import { Serwist } from "serwist";
|
||||
|
||||
declare global {
|
||||
interface WorkerGlobalScope extends SerwistGlobalConfig {
|
||||
__SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
declare const self: ServiceWorkerGlobalScope;
|
||||
|
||||
const serwist = new Serwist({
|
||||
precacheEntries: self.__SW_MANIFEST,
|
||||
skipWaiting: true,
|
||||
clientsClaim: true,
|
||||
navigationPreload: true,
|
||||
runtimeCaching: defaultCache,
|
||||
fallbacks: {
|
||||
entries: [
|
||||
{
|
||||
url: "/~offline",
|
||||
matcher({ request }) {
|
||||
return request.destination === "document";
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
serwist.addEventListeners();
|
||||
Reference in New Issue
Block a user