在项目根目录的 public 文件夹下新建 sw.js(Next.js 会将public目录文件原样复制到打包后的根目录):
// public/sw.js
// 打包时会自动替换 __BUILD_VERSION__ 为 Git Commit + 构建时间
const CACHE_VERSION = '__BUILD_VERSION__';
const CACHE_NAME = `next-static-cache-${CACHE_VERSION}`;
// 缓存过期时间:7天(单位:秒)
const CACHE_EXPIRE_SECONDS = 7 * 24 * 60 * 60;
// 只缓存非HTML静态资源(含CDN)
const STATIC_ASSET_REG = /\.(js|css|png|webp|jpg|jpeg|woff2|gif|svg)$/;
// 排除HTML(Next.js的路由如/、/dashboard等)
const HTML_REG = /(\/$)|(\.html$)/;
// 你的CDN域名(替换为实际值)
const CDN_DOMAINS = ['cdn.your-domain.com', 'static.your-domain.com'];
// 1. SW安装:跳过等待,直接激活
self.addEventListener('install', (e) => {
console.log(`[SW] 安装中,版本:${CACHE_VERSION}`);
e.waitUntil(self.skipWaiting());
});
// 2. SW激活:清理旧缓存 + 接管所有页面
self.addEventListener('activate', async (e) => {
e.waitUntil(
Promise.all([
self.clients.claim(), // 立即接管所有页面
// 清理逻辑:删除所有非当前版本的缓存
(async () => {
const allCacheKeys = await caches.keys();
const oldCaches = allCacheKeys.filter(key => !key.includes(CACHE_VERSION));
await Promise.all(oldCaches.map(key => {
console.log(`[SW] 清理旧缓存:${key}`);
return caches.delete(key);
}));
})()
])
);
console.log(`[SW] 激活成功,当前版本:${CACHE_VERSION}`);
});
// 3. 核心:拦截请求,只缓存非HTML静态资源
self.addEventListener('fetch', (e) => {
const req = e.request;
const url = new URL(req.url);
// 过滤规则:
// - 只处理GET请求
// - 排除HTML/API(Next.js的API路由以/api/开头)
// - 只匹配静态资源(本地+CDN)
const isGet = req.method === 'GET';
const isHTML = HTML_REG.test(url.pathname);
const isApi = url.pathname.startsWith('/api/');
const isLocalStatic = STATIC_ASSET_REG.test(url.pathname) && url.hostname === location.hostname;
const isCdnStatic = STATIC_ASSET_REG.test(url.pathname) && CDN_DOMAINS.includes(url.hostname);
const isNeedCache = isGet && !isHTML && !isApi && (isLocalStatic || isCdnStatic);
// 非目标资源 → 直接走网络
if (!isNeedCache) {
e.respondWith(fetch(req));
return;
}
// 静态资源:缓存优先 + 过期校验
e.respondWith(
(async () => {
const cache = await caches.open(CACHE_NAME);
const cachedRes = await cache.match(req);
// 有缓存 → 校验过期时间
if (cachedRes) {
const cacheTime = new Date(cachedRes.headers.get('x-cache-time')).getTime();
// 未过期:直接返回SW缓存(第二次请求命中)
if (Date.now() - cacheTime < CACHE_EXPIRE_SECONDS * 1000) {
console.log(`[SW] ✅ 缓存命中:${req.url}`);
return cachedRes;
}
// 已过期:删除旧缓存
await cache.delete(req);
console.log(`[SW] ⚠️ 缓存过期:${req.url}`);
}
// 无缓存/过期 → 走网络 + 存新缓存
try {
const networkRes = await fetch(req.clone());
if (networkRes.ok) {
// 克隆响应并添加缓存时间戳
const resToCache = new Response(networkRes.clone().body, {
status: networkRes.status,
statusText: networkRes.statusText,
headers: {
...Object.fromEntries(networkRes.headers.entries()),
'x-cache-time': new Date().toUTCString()
}
});
await cache.put(req, resToCache);
console.log(`[SW] 📥 缓存新资源:${req.url}`);
}
return networkRes;
} catch (err) {
console.error(`[SW] 网络失败,兜底返回缓存:${req.url}`, err);
// 网络失败时,即使缓存过期也返回(降级策略)
if (cachedRes) return cachedRes;
throw err;
}
})()
);
});
// 4. 监听更新(新版本部署后通知)
self.addEventListener('message', (e) => {
if (e.data?.type === 'SW_UPDATED') {
self.skipWaiting(); // 立即激活新版本
}
});
修改项目根目录的 next.config.js,实现打包时自动替换SW中的版本号:
npm install git-rev-sync copy-webpack-plugin --save-dev
# 或 yarn add git-rev-sync copy-webpack-plugin -D
// next.config.js
const nextConfig = {
reactStrictMode: true,
webpack: (config, { buildId, dev, isServer }) => {
// 开发环境不处理SW(避免缓存干扰)
if (dev || isServer) return config;
const gitRev = require('git-rev-sync');
const CopyPlugin = require('copy-webpack-plugin');
// 生成唯一版本号:Git Commit短哈希 + 构建ID
const gitCommit = gitRev.short() || 'dev';
const buildVersion = `${gitCommit}-${buildId}`;
// 1. 替换SW中的__BUILD_VERSION__占位符
config.plugins.push(
new CopyPlugin({
patterns: [
{
from: 'public/sw.js',
to: 'sw.js',
transform: (content) => {
return content.toString().replace('__BUILD_VERSION__', buildVersion);
}
}
]
})
);
return config;
}
};
module.exports = nextConfig;
Next.js 14+ App Router 中,需在客户端组件中注册SW(Pages Router 同理):
// app/sw-register.js
'use client';
import { useEffect } from 'react';
export default function SWRegister() {
useEffect(() => {
// 仅生产环境注册SW(开发环境禁用)
if (process.env.NODE_ENV !== 'production' || !('serviceWorker' in navigator)) {
return;
}
// 注册SW
const registerSW = async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/' // 全局拦截
});
console.log('SW注册成功:', registration.scope);
// 监听SW更新(新版本部署后)
registration.addEventListener('updatefound', () => {
const newSW = registration.installing;
newSW.addEventListener('statechange', () => {
if (newSW.state === 'installed' && navigator.serviceWorker.controller) {
// 新版本已安装,提示用户刷新
console.log('SW有新版本,刷新页面生效');
// 可选:手动触发SW激活
// registration.waiting.postMessage({ type: 'SW_UPDATED' });
// window.location.reload();
}
});
});
} catch (err) {
console.error('SW注册失败:', err);
}
};
// 页面加载完成后注册
window.addEventListener('load', registerSW);
// 卸载时取消监听
return () => {
window.removeEventListener('load', registerSW);
};
}, []);
return null; // 无UI渲染
}
// app/layout.js
import SWRegister from './sw-register';
export default function RootLayout({ children }) {
return (
<html lang="zh-CN">
<body>
<SWRegister /> {/* 注册SW */}
{children}
</body>
</html>
);
}
npm run build
# 或 yarn build
打包后查看 .next/static/sw.js,确认 __BUILD_VERSION__ 已被替换为实际版本号(如 a8b7c9d-123456789)。
npm run start
# 或 yarn start
Application → Service Workers:
Activated + Running[SW] 📥 缓存新资源Cache Storage 中出现 next-static-cache-xxx 缓存库,里面是JS/CSS/图片/CDN资源(无HTML)[SW] ✅ 缓存命中Network 面板中,静态资源的 Size 列显示 (from ServiceWorker)[SW] 清理旧缓存 → 新资源重新缓存git-rev-sync 获取Git Commit,结合 buildId 生成唯一版本号;/dashboard)、API路由(/api/*)直接走网络,不缓存。contenthash,更新后URL变化 → 自动缓存新资源;这套方案是Next.js生产环境专用,无需额外配置,复制即可用,完美解决「静态资源缓存+更新+CDN适配」问题。