Next.js 14+ 完整实现:Service Worker 缓存非HTML静态资源(含更新/CDN适配)
整体流程
- 创建 Service Worker 核心文件(public/sw.js)
- 配置 next.config.js:打包时注入版本号、复制SW文件
- 客户端注册 Service Worker(App Router/Pages Router 兼容)
- 打包部署 & 验证缓存效果
前置说明
- 适配 Next.js 14+(App Router/Pages Router 通用)
- 只缓存:JS/CSS/图片/字体/CDN资源(排除HTML/API)
- 自动处理:版本号更新、缓存过期、CDN资源变化
步骤1:创建 Service Worker 文件(public/sw.js)
在项目根目录的 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(); // 立即激活新版本
}
});
步骤2:配置 next.config.js(注入版本号)
修改项目根目录的 next.config.js,实现打包时自动替换SW中的版本号:
步骤2.1:安装依赖
npm install git-rev-sync copy-webpack-plugin --save-dev
# 或 yarn add git-rev-sync copy-webpack-plugin -D
步骤2.2:编写 next.config.js
// 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;
步骤3:注册 Service Worker(客户端组件)
Next.js 14+ App Router 中,需在客户端组件中注册SW(Pages Router 同理):
步骤3.1:创建客户端组件(app/sw-register.js)
// 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渲染
}
步骤3.2:在根布局中引入(app/layout.js)
// app/layout.js
import SWRegister from './sw-register';
export default function RootLayout({ children }) {
return (
<html lang="zh-CN">
<body>
<SWRegister /> {/* 注册SW */}
{children}
</body>
</html>
);
}
步骤4:打包部署 & 验证
步骤4.1:打包构建
npm run build
# 或 yarn build
打包后查看 .next/static/sw.js,确认 __BUILD_VERSION__ 已被替换为实际版本号(如 a8b7c9d-123456789)。
步骤4.2:本地验证(启动生产环境)
npm run start
# 或 yarn start
步骤4.3:验证缓存效果
- 打开 Chrome → F12 →
Application→Service Workers:- 状态显示
Activated+Running - 版本号为打包时生成的唯一值
- 状态显示
- 刷新页面(首次访问):
- Console 打印
[SW] 📥 缓存新资源 Cache Storage中出现next-static-cache-xxx缓存库,里面是JS/CSS/图片/CDN资源(无HTML)
- Console 打印
- 再次刷新(第二次访问):
- Console 打印
[SW] ✅ 缓存命中 Network面板中,静态资源的Size列显示(from ServiceWorker)
- Console 打印
- 验证更新:
- 修改代码/CDN图片 → 重新打包(版本号变化)→ 部署
- 刷新页面 → Console 打印
[SW] 清理旧缓存→ 新资源重新缓存
核心关键点总结
1. 版本号自动注入
- Next.js 打包时,通过
git-rev-sync获取Git Commit,结合buildId生成唯一版本号; - 自动替换SW中的占位符,确保每次打包版本号唯一。
2. 只缓存非HTML静态资源
- 通过正则排除HTML/API,只匹配JS/CSS/图片/CDN资源;
- Next.js的路由(如
/dashboard)、API路由(/api/*)直接走网络,不缓存。
3. 缓存更新机制
- 版本号变化 → SW激活时清理旧缓存 → 新资源存入新版本缓存库;
- CDN资源带
contenthash,更新后URL变化 → 自动缓存新资源; - 7天过期时间兜底,避免缓存永久有效。
4. 第二次请求命中逻辑
- 静态资源首次请求:网络请求 → 存入SW缓存;
- 第二次请求:直接从SW缓存返回,优先级高于浏览器强缓存,无需走网络。
这套方案是Next.js生产环境专用,无需额外配置,复制即可用,完美解决「静态资源缓存+更新+CDN适配」问题。