你坐飞机,关掉网络,旁边小哥还在刷抖音(离线缓存好的视频)。你打开自己的网站,白屏,报错。你默默关上手机,心想:“要是我的网站也能离线看就好了。” 今天我们就来给你的网站装上 “离线小精灵”——Service Worker。以后用户没网也能访问,还能把网站装到手机桌面,像原生 App 一样。

PWA(Progressive Web App)这个概念喊了好几年,但真正用上的网站不多。其实它没那么玄乎,核心就是 Service Worker——一个在浏览器后台独立运行的 JS 线程,能拦截网络请求、缓存资源、推送通知。
加了 Service Worker 的网站,就算用户开飞行模式,只要之前访问过,照样能看到页面(至少看到缓存过的内容)。而且速度极快,因为资源从本地取,不用等网络。今天我们就从零给一个静态网站加上离线缓存,顺便让它 “可安装”。
Service Worker 不是一上来就接管所有请求的,它有严格的生命周期:
install 事件。通常在这里缓存核心资源。activate 事件里清理旧缓存。注意:SW 只在 HTTPS(或 localhost)下生效,因为可以拦截网络,不安全。
我们先写一个极简版 sw.js,让用户离线时看到一个 “你已离线” 的页面。
// sw.js
const CACHE_NAME = 'my-pwa-cache-v1';
const OFFLINE_URL = '/offline.html';
// 安装时缓存离线页面
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.add(OFFLINE_URL))
);
// 强制等待中的 SW 立即激活
self.skipWaiting();
});
// 激活时清理旧缓存
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) => {
return Promise.all(
keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key))
);
})
);
self.clients.claim();
});
// 拦截请求,离线时返回缓存
self.addEventListener('fetch', (event) => {
if (event.request.mode === 'navigate') {
// 页面导航请求
event.respondWith(
fetch(event.request).catch(() => caches.match(OFFLINE_URL))
);
} else {
// 其他资源走缓存优先策略(稍后优化)
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
}
});
然后在 index.html 里注册:
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').then(reg => {
console.log('SW 注册成功', reg);
}).catch(err => {
console.log('SW 注册失败', err);
});
});
}
</script>
现在你打开网站,开飞机模式(或 DevTools → Network 离线),刷新页面,应该会显示 offline.html。说明 SW 已经拦下了请求。
上面的代码对所有资源都用了 “缓存优先”——先查 cache,没有才网络。这会导致一个问题:如果某个资源之前缓存过,即使服务器更新了,用户也看不到新版本。所以需要根据资源类型选择策略。
常用策略:
我们改一下 fetch 事件:
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// 如果是 API 请求,走网络优先
if (url.pathname.startsWith('/api/')) {
event.respondWith(
fetch(event.request).catch(() => caches.match(event.request))
);
return;
}
// 如果是静态资源(js、css、图片),走缓存优先
if (/\.(js|css|png|jpg|webp)$/.test(url.pathname)) {
event.respondWith(
caches.match(event.request).then((cached) => cached || fetch(event.request))
);
return;
}
// 其他(如 HTML)走 stale-while-revalidate
event.respondWith(
caches.open(CACHE_NAME).then(async (cache) => {
const cached = await cache.match(event.request);
const fetchPromise = fetch(event.request).then((response) => {
cache.put(event.request, response.clone());
return response;
}).catch(() => cached);
return cached || fetchPromise;
})
);
});
这样,你的网站既能离线访问,又能及时更新动态内容。
手写缓存策略很麻烦,尤其还要处理版本、过期、缓存清理。Google 出品了 Workbox,一套工具库,几行配置搞定复杂策略。
安装 Workbox CLI 或直接在 sw.js 里导入 CDN:
importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.0.0/workbox-sw.js');
const { registerRoute, strategies, cacheableResponse } = workbox;
// 预缓存静态资源(构建时生成 manifest)
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST || []);
// 图片缓存策略
registerRoute(
({ request }) => request.destination === 'image',
new strategies.CacheFirst({
cacheName: 'images',
plugins: [
new cacheableResponse.CacheableResponsePlugin({ statuses: [0, 200] }),
new workbox.expiration.ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 30 * 24 * 60 * 60 })
]
})
);
// API 网络优先
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new strategies.NetworkFirst()
);
配合 webpack/vite 插件,可以自动生成预缓存清单,连 install 里的 cache.add 都不用手动写。
PWA 另一大特性:用户可以像装 App 一样把网站装到手机桌面。需要满足三个条件:
manifest.json 文件,放在根目录示例 manifest.json:
{
"name": "我的离线网站",
"short_name": "离线站",
"start_url": "/",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
在 index.html 里引用:
<link rel="manifest" href="/manifest.json">
之后用户访问网站,浏览器会在地址栏右侧弹出 “安装 App” 的提示。点一下,桌面就多了一个图标,打开后没有浏览器地址栏,像原生 App。
Service Worker 还能接收服务器推送的消息,即使网站没打开也能弹出通知。这需要用户授权和后台推送服务(比如 Firebase Cloud Messaging)。代码稍复杂,但可以实现 “用户关掉浏览器,你也能给他发优惠券提醒” 的效果。
我用一个 React 静态网站测试:
用户从 “等待加载” 变成 “秒开”,体验提升 5 倍以上。
CACHE_NAME 版本号,或者在预缓存时用 rev(文件 hash)解决。Workbox 会自动处理。sw.js 所在目录,如果放在根目录,可以控制全站。放在 js/ 下就只能控制 js/ 路径。下次你坐飞机,打开自己的 PWA 网站,不用网络也能刷内容。同事看了问:“你怎么做到的?” 你就可以把本文甩给他。
评论区聊聊:你的网站支持离线访问吗?遇到过哪些缓存更新问题?