你坐飞机,关掉网络,旁边小哥还在刷抖音(离线缓存好的视频)。你打开自己的网站,白屏,报错。你默默关上手机,心想:“要是我的网站也能离线看就好了。” 今天我们就来给你的网站装上“离线小精灵”——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,让用户离线时看到一个“你已离线”的页面。
// sw.jsconstCACHE_NAME='my-pwa-cache-v1';constOFFLINE_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)=>{returnPromise.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)=>{returnresponse||fetch(event.request);}));}});然后在index.html里注册:
<script>if('serviceWorker'innavigator){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事件:
self.addEventListener('fetch',(event)=>{consturl=newURL(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-revalidateevent.respondWith(caches.open(CACHE_NAME).then(async(cache)=>{constcached=awaitcache.match(event.request);constfetchPromise=fetch(event.request).then((response)=>{cache.put(event.request,response.clone());returnresponse;}).catch(()=>cached);returncached||fetchPromise;}));});这样,你的网站既能离线访问,又能及时更新动态内容。
四、用 Workbox 简化代码
手写缓存策略很麻烦,尤其还要处理版本、过期、缓存清理。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',newstrategies.CacheFirst({cacheName:'images',plugins:[newcacheableResponse.CacheableResponsePlugin({statuses:[0,200]}),newworkbox.expiration.ExpirationPlugin({maxEntries:50,maxAgeSeconds:30*24*60*60})]}));// API 网络优先registerRoute(({url})=>url.pathname.startsWith('/api/'),newstrategies.NetworkFirst());配合 webpack/vite 插件,可以自动生成预缓存清单,连install里的cache.add都不用手动写。
五、让网站可安装(添加到主屏幕)
PWA 另一大特性:用户可以像装 App 一样把网站装到手机桌面。需要满足三个条件:
- HTTPS(或 localhost)
- 注册了 Service Worker
- 有一个
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里引用:
<linkrel="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 网站,不用网络也能刷内容。同事看了问:“你怎么做到的?” 你就可以把本文甩给他。
评论区聊聊:你的网站支持离线访问吗?遇到过哪些缓存更新问题?