feat(pwa): wire up @ducanh2912/next-pwa + register SW in layout

PWA install flow was wired but unusable: pwaUtil.install() calls
deferred.prompt(), but Chrome only fires beforeinstallprompt when a
valid Service Worker is registered + a manifest is linked. The project
had both intent (manifest, usePwaInstall hook) but no live SW —
public/sw.js was a stale workbox build artifact from a previous
attempt with hardcoded chunk URLs that no longer matched any build.

What this commit does:

1. Add @ducanh2912/next-pwa@10.2.9 as devDependency.
2. Wrap nextConfig with withPWA({...}):
   - dest: "public" SW outputs to public/sw.js
   - disable: true skip SW generation in dev (HMR-friendly)
   - register: false don't auto-inject registration
   - workboxOptions.skipWaiting: true + clientsClaim: true — new SW
     takes over immediately
3. Add --webpack flag to dev/build/dev:proxy scripts.
   @ducanh2912/next-pwa is webpack-only; Turbopack (Next 16 default)
   doesn't load webpack plugins. Per user decision to accept the perf
   trade-off in exchange for a working PWA.
4. Delete stale public/sw.js (workbox will regenerate it at build
   time with the correct chunk URLs).
5. Add src/app/_components/core/sw-register.tsx: a no-UI client
   component that registers /sw.js in production. Mounted at the end
   of <body> in layout.tsx. disable: true means dev mode skips
   registration (no /sw.js to fetch anyway).

After this commit + pnpm build, public/sw.js is regenerated with
correct chunk URLs, registered on first prod load, and Chrome fires
beforeinstallprompt — usePwaInstall + pwaUtil.install() then trigger
the native install prompt end-to-end.
This commit is contained in:
2026-06-16 19:36:03 +08:00
parent e632fa7139
commit 2acc005809
6 changed files with 2333 additions and 21 deletions
+22 -7
View File
@@ -1,4 +1,5 @@
import type { NextConfig } from "next";
import withPWAInit from "@ducanh2912/next-pwa";
/**
* Next.js 16 临时绕过 `/_global-error` prerender 的 workStore bug
@@ -11,12 +12,15 @@ import type { NextConfig } from "next";
* - 服务器启动:`cd <path> && next start -H 0.0.0.0 -p 3000`
* 或用 PM2`pm2 start "next start" --name cozsweet`
*
* 已知 trade-off@ducanh2912/next-pwa 是 webpack-only 插件,与 Turbopack 不兼容
* 本轮按用户指令采用"默认构建"**移除了 next-pwa 插件**`public/manifest.json` +
* `public/favicon.ico` 仍由 Next.js metadata 加载;install prompt 仍由
* `usePwaInstall` hook 监听 `beforeinstallprompt` 浏览器 API 提供)。
* Service Worker 暂不再自动生成;如未来需要,请改用 Next 16 原生 PWA 方案
* `src/app/manifest.ts` + 手写 `public/sw.js` + client `useEffect` 注册)。
* PWA 集成:`@ducanh2912/next-pwa` 在 build 阶段用 workbox 生成 public/sw.js
* - `dest: "public"` SW 输出到 public/sw.js
* - `disable: true` + dev NODE_ENV 跳过 dev —— 避免 HMR + 老 SW cache 冲突
* - `register: false` 不自动注入注册脚本 —— App Router 下走 <SwRegister /> 客户端组件更可控
* - `skipWaiting: true` 新 SW 立即接管(避免旧 SW 卡住页面)
*
* 已知 trade-off`@ducanh2912/next-pwa` 是 webpack-only 插件,与 Turbopack 不兼容。
* scripts 中加 `--webpack` flag 取消 Turbopack。等未来 next-pwa 支持 Turbopack 时再切回。
* Serwist 同样 webpack-only —— 见 node_modules/next/dist/docs/.../progressive-web-apps.md
*/
const nextConfig: NextConfig = {
experimental: {
@@ -25,4 +29,15 @@ const nextConfig: NextConfig = {
},
};
export default nextConfig;
const withPWA = withPWAInit({
dest: "public",
disable: true, // 仅当 NODE_ENV === "development" 时跳过 SW 生成
register: false,
workboxOptions: {
// skipWaiting + clientsClaim 嵌在 workboxOptionsGenerateSWOptions 字段)
skipWaiting: true,
clientsClaim: true,
},
});
export default withPWA(nextConfig);
+4 -3
View File
@@ -3,9 +3,9 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"dev:proxy": "HTTPS_PROXY=http://127.0.0.1:7891 next dev",
"build": "next build",
"dev": "next dev --webpack",
"dev:proxy": "HTTPS_PROXY=http://127.0.0.1:7891 next dev --webpack",
"build": "next build --webpack",
"start": "next start -p 9135",
"build:start": "npm run build && npm run start",
"lint": "eslint",
@@ -35,6 +35,7 @@
"zod": "^4.4.3"
},
"devDependencies": {
"@ducanh2912/next-pwa": "^10.2.9",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
+2267 -9
View File
File diff suppressed because it is too large Load Diff
-1
View File
File diff suppressed because one or more lines are too long
+34
View File
@@ -0,0 +1,34 @@
"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;
}
+6 -1
View File
@@ -3,6 +3,7 @@ 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 = {
@@ -49,7 +50,11 @@ export default function RootLayout({
<body className="min-h-full flex flex-col">
{/* NextAuth SessionProvider:为所有客户端组件提供 useSession() 上下文 */}
<SessionProvider>
<RootProviders>{children}</RootProviders>
<RootProviders>
{children}
{/* PWA Service Worker 注册器 —— 生产构建挂载 /sw.js@ducanh2912/next-pwa 生成) */}
<SwRegister />
</RootProviders>
</SessionProvider>
</body>
</html>