离线与缓存策略
缓存策略
1. Cache First(缓存优先)
js
// 先查缓存,有则返回;没有则请求并缓存
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
if (cached) return cached
return fetch(event.request).then((response) => {
return caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, response.clone())
return response
})
})
})
)
})// 先查缓存,有则返回;没有则请求并缓存
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
if (cached) return cached
return fetch(event.request).then((response) => {
return caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, response.clone())
return response
})
})
})
)
})适用: 静态资源(图片、字体、CSS、JS) 特点: 速度快,离线可用
2. Network First(网络优先)
js
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then((response) => {
return caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, response.clone())
return response
})
})
.catch(() => caches.match(event.request))
)
})self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then((response) => {
return caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, response.clone())
return response
})
})
.catch(() => caches.match(event.request))
)
})适用: API 请求、需要实时数据 特点: 在线时取最新数据,离线时用缓存
3. Stale While Revalidate(stale 时重新验证)
js
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open(CACHE_NAME).then((cache) => {
return cache.match(event.request).then((cached) => {
const fetchPromise = fetch(event.request).then((response) => {
cache.put(event.request, response.clone())
return response
})
// 返回缓存数据,同时在后台更新
return cached || fetchPromise
})
})
)
})self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open(CACHE_NAME).then((cache) => {
return cache.match(event.request).then((cached) => {
const fetchPromise = fetch(event.request).then((response) => {
cache.put(event.request, response.clone())
return response
})
// 返回缓存数据,同时在后台更新
return cached || fetchPromise
})
})
)
})适用: 非关键数据,用户不期望即时更新(头像、列表) 特点: 极快响应,后台更新
4. Network Only(仅网络)
js
self.addEventListener('fetch', (event) => {
event.respondWith(fetch(event.request))
})self.addEventListener('fetch', (event) => {
event.respondWith(fetch(event.request))
})适用: 支付、表单提交等需要实时验证的操作
5. Cache Only(仅缓存)
js
self.addEventListener('fetch', (event) => {
event.respondWith(caches.match(event.request))
})self.addEventListener('fetch', (event) => {
event.respondWith(caches.match(event.request))
})适用: 预缓存资源(应用 Shell)
策略选择表
| 资源类型 | 推荐策略 | 说明 |
|---|---|---|
| 应用 Shell | Cache First | HTML/CSS/JS 骨架 |
| 图片/字体 | Cache First | 不常变化 |
| API 数据 | Network First | 需要最新数据 |
| 图片列表 | Stale While Revalidate | 快速展示,后台更新 |
| 支付 | Network Only | 不信任缓存 |
| 图标/Logo | Cache Only | 预缓存 |
Workbox(Google 官方库)
大幅简化 SW 编写。
安装
bash
npm install workbox-webpack-plugin
# 或 Vite
npm install vite-plugin-pwanpm install workbox-webpack-plugin
# 或 Vite
npm install vite-plugin-pwaWorkbox 配置
js
import { precacheAndRoute } from 'workbox-precaching'
import { registerRoute } from 'workbox-routing'
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies'
import { ExpirationPlugin } from 'workbox-expiration'
import { CacheableResponsePlugin } from 'workbox-cacheable-response'
// 预缓存(注入清单)
precacheAndRoute(self.__WB_MANIFEST)
// 图片 — 缓存优先
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [
new ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30天
}),
],
})
)
// API — 网络优先
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({
cacheName: 'api',
plugins: [
new ExpirationPlugin({ maxEntries: 50 }),
],
})
)
// 静态资源 — stale while revalidate
registerRoute(
({ request }) =>
request.destination === 'style' ||
request.destination === 'script' ||
request.destination === 'font',
new StaleWhileRevalidate({
cacheName: 'static',
})
)import { precacheAndRoute } from 'workbox-precaching'
import { registerRoute } from 'workbox-routing'
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies'
import { ExpirationPlugin } from 'workbox-expiration'
import { CacheableResponsePlugin } from 'workbox-cacheable-response'
// 预缓存(注入清单)
precacheAndRoute(self.__WB_MANIFEST)
// 图片 — 缓存优先
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [
new ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30天
}),
],
})
)
// API — 网络优先
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({
cacheName: 'api',
plugins: [
new ExpirationPlugin({ maxEntries: 50 }),
],
})
)
// 静态资源 — stale while revalidate
registerRoute(
({ request }) =>
request.destination === 'style' ||
request.destination === 'script' ||
request.destination === 'font',
new StaleWhileRevalidate({
cacheName: 'static',
})
)Vite PWA 插件
ts
// vite.config.ts
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['favicon.ico'],
manifest: {
name: 'My App',
short_name: 'App',
theme_color: '#ffffff',
icons: [
{ src: 'icon-192.png', sizes: '192x192', type: 'image/png' },
{ src: 'icon-512.png', sizes: '512x512', type: 'image/png' },
],
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: { maxEntries: 50, maxAgeSeconds: 86400 },
},
},
],
},
}),
],
})// vite.config.ts
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['favicon.ico'],
manifest: {
name: 'My App',
short_name: 'App',
theme_color: '#ffffff',
icons: [
{ src: 'icon-192.png', sizes: '192x192', type: 'image/png' },
{ src: 'icon-512.png', sizes: '512x512', type: 'image/png' },
],
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: { maxEntries: 50, maxAgeSeconds: 86400 },
},
},
],
},
}),
],
})IndexedDB + Service Worker
js
// 在 SW 中使用 IndexedDB 存储离线数据
import { openDB } from 'idb'
self.addEventListener('sync', async (event) => {
if (event.tag === 'sync-posts') {
const db = await openDB('offline-db', 1)
const tx = db.transaction('pending-posts', 'readonly')
const posts = await tx.store.getAll()
for (const post of posts) {
await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(post),
})
}
}
})// 在 SW 中使用 IndexedDB 存储离线数据
import { openDB } from 'idb'
self.addEventListener('sync', async (event) => {
if (event.tag === 'sync-posts') {
const db = await openDB('offline-db', 1)
const tx = db.transaction('pending-posts', 'readonly')
const posts = await tx.store.getAll()
for (const post of posts) {
await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(post),
})
}
}
})
Cao Note