PWA离线缓存策略让你的应用在无网络环境下也能运行前言各位前端小伙伴不知道你们有没有遇到过这种情况在地铁里或者信号不好的地方打开一个网页结果显示无法访问那种心情简直想摔手机我曾经开发过一个新闻应用用户反馈说在地铁里看不了新闻体验很差。后来我引入了PWA离线缓存用户即使在没有网络的情况下也能浏览之前看过的新闻体验大大提升什么是PWA离线缓存PWAProgressive Web App离线缓存是指通过Service Worker技术将应用的静态资源和数据缓存到用户的设备上使得应用在没有网络连接的情况下也能正常运行。Service Worker基础注册Service Worker// main.js if (serviceWorker in navigator) { window.addEventListener(load, async () { try { const registration await navigator.serviceWorker.register(/sw.js) console.log(Service Worker registered:, registration) } catch (error) { console.error(Service Worker registration failed:, error) } }) }Service Worker生命周期// sw.js self.addEventListener(install, (event) { console.log(Service Worker installing...) event.waitUntil( caches.open(my-app-cache).then((cache) { return cache.addAll([ /, /index.html, /styles.css, /app.js ]) }) ) }) self.addEventListener(activate, (event) { console.log(Service Worker activated) }) self.addEventListener(fetch, (event) { event.respondWith( caches.match(event.request).then((response) { return response || fetch(event.request) }) ) })缓存策略1. Cache First缓存优先self.addEventListener(fetch, (event) { event.respondWith( caches.match(event.request).then((cachedResponse) { if (cachedResponse) { return cachedResponse } return fetch(event.request).then((networkResponse) { caches.open(my-app-cache).then((cache) { cache.put(event.request, networkResponse.clone()) }) return networkResponse }) }) ) })2. Network First网络优先self.addEventListener(fetch, (event) { event.respondWith( fetch(event.request).then((networkResponse) { caches.open(my-app-cache).then((cache) { cache.put(event.request, networkResponse.clone()) }) return networkResponse }).catch(() { return caches.match(event.request) }) ) })3. Stale-while-revalidate先展示缓存后台更新self.addEventListener(fetch, (event) { event.respondWith( caches.match(event.request).then((cachedResponse) { const fetchPromise fetch(event.request).then((networkResponse) { caches.open(my-app-cache).then((cache) { cache.put(event.request, networkResponse.clone()) }) return networkResponse }) return cachedResponse || fetchPromise }) ) })4. Cache Only仅缓存self.addEventListener(fetch, (event) { if (event.request.url.includes(/static/)) { event.respondWith(caches.match(event.request)) } })5. Network Only仅网络self.addEventListener(fetch, (event) { if (event.request.url.includes(/api/)) { event.respondWith(fetch(event.request)) } })缓存策略选择指南资源类型推荐策略说明静态资源Cache First变化不频繁优先使用缓存API数据Network First需要最新数据优先使用网络用户数据Stale-while-revalidate先展示缓存后台更新关键资源Cache Only必须离线可用实时数据Network Only必须最新数据高级缓存策略版本化缓存const CACHE_VERSION v1 const CACHE_NAME my-app-${CACHE_VERSION} self.addEventListener(install, (event) { event.waitUntil( caches.open(CACHE_NAME).then((cache) { return cache.addAll([ /, /index.html, /styles.css, /app.js ]) }) ) }) self.addEventListener(activate, (event) { event.waitUntil( caches.keys().then((cacheNames) { return Promise.all( cacheNames.filter((name) { return name ! CACHE_NAME }).map((name) { return caches.delete(name) }) ) }) ) })动态缓存self.addEventListener(fetch, (event) { const request event.request if (request.method ! GET) { return } event.respondWith( caches.match(request).then((cachedResponse) { const fetchPromise fetch(request).then((networkResponse) { caches.open(dynamic-cache).then((cache) { cache.put(request, networkResponse.clone()) }) return networkResponse }).catch(() cachedResponse) return cachedResponse || fetchPromise }) ) })缓存大小管理const MAX_CACHE_SIZE 50 * 1024 * 1024 // 50MB async function trimCache(cacheName, maxSize) { const cache await caches.open(cacheName) const keys await cache.keys() let totalSize 0 const entries [] for (const key of keys) { const response await cache.match(key) const blob await response.blob() totalSize blob.size entries.push({ key, size: blob.size }) } if (totalSize maxSize) { entries.sort((a, b) b.size - a.size) for (const entry of entries) { await cache.delete(entry.key) totalSize - entry.size if (totalSize maxSize) { break } } } }缓存API数据使用Workboximport { registerRoute } from workbox-routing import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from workbox-strategies import { CacheableResponsePlugin } from workbox-cacheable-response import { ExpirationPlugin } from workbox-expiration // 缓存静态资源 registerRoute( ({ request }) request.destination style || request.destination script || request.destination image, new CacheFirst({ cacheName: static-resources, plugins: [ new CacheableResponsePlugin({ statuses: [0, 200] }), new ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60 // 30天 }) ] }) ) // 缓存API数据 registerRoute( ({ url }) url.pathname.startsWith(/api/), new NetworkFirst({ cacheName: api-data, plugins: [ new CacheableResponsePlugin({ statuses: [0, 200] }), new ExpirationPlugin({ maxEntries: 20, maxAgeSeconds: 5 * 60 // 5分钟 }) ] }) )处理缓存更新通知用户更新// sw.js self.addEventListener(install, (event) { self.skipWaiting() }) self.addEventListener(activate, (event) { event.waitUntil( self.clients.claim() ) }) // main.js let newWorker null navigator.serviceWorker.addEventListener(controllerchange, () { if (newWorker) { if (window.confirm(有新版本可用是否刷新)) { window.location.reload() } } }) navigator.serviceWorker.register(/sw.js).then((registration) { registration.addEventListener(updatefound, () { newWorker registration.installing newWorker.addEventListener(statechange, () { if (newWorker.state installed) { if (navigator.serviceWorker.controller) { // 显示更新提示 } } }) }) })常见问题问题1缓存不更新解决方案使用版本化缓存并在activate事件中清理旧缓存问题2缓存过大解决方案使用ExpirationPlugin限制缓存大小和数量问题3首次加载慢解决方案使用preload和prefetch优化首屏加载问题4缓存数据过期解决方案使用Stale-while-revalidate策略总结离线缓存是PWA的核心特性之一。通过合理的缓存策略我们可以提升用户体验无网络环境也能使用减少网络请求缓存静态资源加快加载速度优先使用缓存保证数据新鲜后台更新缓存现在开始为你的应用添加离线缓存功能吧你的用户会感谢你的最后一句忠告不要过度缓存定期清理过期缓存