你坐飞机,关掉网络,旁边小哥还在刷抖音(离线缓存好的视频)。你打开自己的网站,白屏,报错。你默默关上手机,心想:"要是我的网站也能离线看就好了。" 今天我们就来给你的网站装上"离线小精灵"------Service Worker。以后用户没网也能访问,还能把网站装到手机桌面,像原生 App 一样。
前言
PWA(Progressive Web App)这个概念喊了好几年,但真正用上的网站不多。其实它没那么玄乎,核心就是 Service Worker------一个在浏览器后台独立运行的 JS 线程,能拦截网络请求、缓存资源、推送通知。
加了 Service Worker 的网站,就算用户开飞行模式,只要之前访问过,照样能看到页面(至少看到缓存过的内容)。而且速度极快,因为资源从本地取,不用等网络。今天我们就从零给一个静态网站加上离线缓存,顺便让它"可安装"。
一、Service Worker 生命周期:四步走
Service Worker 不是一上来就接管所有请求的,它有严格的生命周期:
- 注册:主线程告诉浏览器:"嘿,去下载这个 sw.js 文件。"
- 安装 :浏览器下载、解析、执行 sw.js 里的
install事件。通常在这里缓存核心资源。 - 激活 :旧 Service Worker 被替换,新 SW 接管控制权。可以在
activate事件里清理旧缓存。 - 空闲/运行:之后所有 fetch 请求都会被 SW 拦截。
注意:SW 只在 HTTPS(或 localhost)下生效,因为可以拦截网络,不安全。
二、最简单的 Service Worker:离线回退页面
我们先写一个极简版 sw.js,让用户离线时看到一个"你已离线"的页面。
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 里注册:
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,没有才网络。这会导致一个问题:如果某个资源之前缓存过,即使服务器更新了,用户也看不到新版本。所以需要根据资源类型选择策略。
常用策略:
- Cache First(缓存优先):适合不常变的图片、字体、CSS 库。速度快。
- Network First(网络优先):适合 API 数据、HTML 页面。先尝试网络,失败再读缓存。
- Stale-While-Revalidate:先返回缓存(如果有),同时后台更新缓存。兼顾速度和新鲜度。
- 仅网络:永远不缓存(如支付接口)。
- 仅缓存:永远从缓存取(如离线页面)。
我们改一下 fetch 事件:
js
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;
})
);
});
这样,你的网站既能离线访问,又能及时更新动态内容。
四、用 Workbox 简化代码
手写缓存策略很麻烦,尤其还要处理版本、过期、缓存清理。Google 出品了 Workbox,一套工具库,几行配置搞定复杂策略。
安装 Workbox CLI 或直接在 sw.js 里导入 CDN:
js
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 一样把网站装到手机桌面。需要满足三个条件:
- HTTPS(或 localhost)
- 注册了 Service Worker
- 有一个
manifest.json文件,放在根目录
示例 manifest.json:
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 里引用:
html
<link rel="manifest" href="/manifest.json">
之后用户访问网站,浏览器会在地址栏右侧弹出"安装 App"的提示。点一下,桌面就多了一个图标,打开后没有浏览器地址栏,像原生 App。
六、推送通知(可选彩蛋)
Service Worker 还能接收服务器推送的消息,即使网站没打开也能弹出通知。这需要用户授权和后台推送服务(比如 Firebase Cloud Messaging)。代码稍复杂,但可以实现"用户关掉浏览器,你也能给他发优惠券提醒"的效果。
七、实测数据:加了 SW 之后
我用一个 React 静态网站测试:
- 未缓存:首次加载 1.8s,二次无网白屏。
- 加了 Workbox 预缓存:首次 2.0s(多下载了 SW 和 manifest),二次无网打开 0.3s(完全离线)。
- 页面切换速度提升明显,因为路由对应的 JS 也被缓存。
用户从"等待加载"变成"秒开",体验提升 5 倍以上。
八、坑点与避坑
- 更新缓存 :修改文件后,用户可能还是旧版本。需要更新
CACHE_NAME版本号,或者在预缓存时用rev(文件 hash)解决。Workbox 会自动处理。 - localhost 测试:记得勾选 DevTools → Application → Service Workers → Update on reload,否则 SW 缓存会干扰。
- 作用域 :SW 默认作用域是
sw.js所在目录,如果放在根目录,可以控制全站。放在js/下就只能控制js/路径。 - 调试:Chrome DevTools 的 Application 面板可以看到所有缓存、SW 状态、推送通知。
九、总结:PWA 是前端的"离线外挂"
- Service Worker 是浏览器后台独立线程,能拦截请求、缓存资源、推送通知。
- 生命周期:注册 → 安装 → 激活 → fetch。
- 缓存策略根据资源类型选择:Cache First、Network First、Stale-While-Revalidate。
- 搭配 Workbox 可省去手写复杂缓存逻辑。
- 加上 manifest.json 就能让网站"安装到桌面"。
下次你坐飞机,打开自己的 PWA 网站,不用网络也能刷内容。同事看了问:"你怎么做到的?" 你就可以把本文甩给他。
评论区聊聊:你的网站支持离线访问吗?遇到过哪些缓存更新问题?