2-1.文档及链接
jake archibald
youyuxi
调试
chrome://serviceworker-internals/
chrome://inspect/#service-workers
2-2.缓存策略
1.CacheOnly
仅缓存是指当 Service Worker
控制着页面时,匹配的请求将只会进入缓存。
这意味着,任何缓存的资源都需要进行预缓存,以使模式正常工作,并且在 Service Worker
更新之前,绝不会在缓存中更新这些资源。
// Establish a cache name
const cacheName = "MyFancyCacheName_v1"
// Assets to precache
const precachedAssets = [
"/possum1.jpg",
"/possum2.jpg",
"/possum3.jpg",
"/possum4.jpg",
]
self.addEventListener("install", (event) => {
// Precache assets on install
event.waitUntil(
caches.open(cacheName).then((cache) => {
return cache.addAll(precachedAssets)
})
)
})
self.addEventListener("fetch", (event) => {
// Is this one of our precached assets?
const url = new URL(event.request.url)
const isPrecachedRequest = precachedAssets.includes(url.pathname)
if (isPrecachedRequest) {
// Grab the precached asset from the cache
event.respondWith(
caches.open(cacheName).then((cache) => {
return cache.match(event.request.url)
})
)
} else {
// Go to the network
return
}
})
2.NetworkOnly
仅网络是指请求通过 Service Worker
传递到网络,而无需与 Service Worker
缓存进行任何交互。
这是确保内容新鲜度的好策略(比如使用标记),但需要权衡的是,用户离线时自定义设置将永远不会起作用。
3.CacheFirst
缓存优先是指:
- 请求到达缓存。如果资源位于缓存中,请从缓存中提供。
- 如果请求不在缓存中,请转到网络。
- 网络请求完成后,将其添加到缓存中,然后从网络返回响应。
// Establish a cache name
const cacheName = 'MyFancyCacheName_v1'
self.addEventListener('fetch', (event) => {
// Check if this is a request for an image
if (event.request.destination === 'image') {
event.respondWith(caches.open(cacheName).then((cache) => {
// Go to the cache first
return cache.match(event.request.url).then((cachedResponse) => {
// Return a cached response if we have one
if (cachedResponse) {
return cachedResponse
}
// Otherwise, hit the network
return fetch(event.request).then((fetchedResponse) => {
// Add the network response to the cache for later visits
cache.put(event.request, fetchedResponse.clone())
// Return the network response
return fetchedResponse
})
})
}))
} else {
return
}
})
TIP
这是一项适用于所有静态资源(例如 CSS
、JavaScript
、图片和字体)的绝佳策略,尤其是经过哈希处理的资源。
它可以绕过 HTTP
缓存可能启动的服务器执行任何内容新鲜度检查,从而加快不可变资源的速度。更重要的是,所有缓存的资源都将离线可用
4.NetworkFirst
网络优先是指:
- 优先直接向网络请求资源,同时在缓存中保留一份。
- 当网络不可用,资源请求失败时,取缓存中的资源。
// Establish a cache name
const cacheName = 'MyFancyCacheName_v1'
self.addEventListener('fetch', (event) => {
// Check if this is a navigation request
if (event.request.mode === 'navigate') {
// Open the cache
event.respondWith(caches.open(cacheName).then((cache) => {
// Go to the network first
return fetch(event.request.url).then((fetchedResponse) => {
cache.put(event.request, fetchedResponse.clone())
return fetchedResponse
}).catch(() => {
// If the network is unavailable, get
return cache.match(event.request.url)
})
}))
} else {
return
}
})
5.StaleWhileRevalidate
重新验证时过时是指:
- 在第一次请求获取资源时,从网络中提取资源,将其放入缓存中并返回网络响应;
- 对于后续请求,首先从缓存提供资源,然后在后台从网络重新请求该资源,并更新资源的缓存条目;
- 对于此后的请求,您将收到在上一步中从缓存中放置的最后一个网络提取的版本。
// Establish a cache name
const cacheName = 'MyFancyCacheName_v1'
self.addEventListener('fetch', (event) => {
if (event.request.destination === 'image') {
event.respondWith(caches.open(cacheName).then((cache) => {
return cache.match(event.request).then((cachedResponse) => {
const fetchedResponse = fetch(event.request).then((networkResponse) => {
cache.put(event.request, networkResponse.clone())
return networkResponse
})
return cachedResponse || fetchedResponse
})
}))
} else {
return
}
})
更新策略
Give Users Control Over App Updates in Vue CLI 3 PWAs
How to Fix the Refresh Button When Using Service Workers
1.直接使用skipWaiting
这种方式简单粗暴。
self.addEventListener('install', (event) => {
self.skipWaiting()
// 预缓存其他内容
})
✅ 优点:
逻辑直接,操作简单。
❌ 缺点:
- 较好的情况是,代码版本不一致,重新请求资源,浪费带宽。
- 最差的情况是,资源
404
,造成解析错误。
譬如,假设我们的 serviceWorker
有两个新旧版本。
较旧的 v1
版本缓存了 /cdn/ac4e.jpg
和 /route/about.v1.js
,而更新的 v2
版本只缓存了 /route/about.v2.js
,没有缓存 /cdn/ac4e.jpg
。
那么当内置了 skipWaiting
的 v2
版本控制客户端后,此时 v1
版本中的 /cdn/ac4e.jpg
缓存会失效,需要重新向 CDN
上发起请求获取。
而 v1
版本中的懒加载路由 /route/about.v1.js
已经不存在了,会导致 404
,造成解析错误。
TIP
简而言之,该策略适合始终存在的静态资源(譬如 CDN
上托管的资源),而不适合可能动态变化的资源(懒加载路由、hash
文件名资源)。
2024-05-22更新,发现上述优缺点的话述跟实际加载效果有出入(不确定是不是浏览器版本迭代过的原因):
当页面刷新时,无论何种方式,都会预取 serviceWorker
资源。如果 serviceWorker
设置了 skipWaiting
,那么后续资源的加载就会在新 serviceWorker
中获取,否则是在旧 serviceWorker
中获取。
但如果是,页面打开新 tab
时,此时页面也不会及时更新,会同时存在 v1
和 v2
两个版本:
2.使用 skipWaiting
+ window.reload()
在上一节中,提到了单独直接调用 skipWaiting
时,虽然 serviceWorker
会立即更新,但由于页面已经加载(譬如 index.html
已加载),因此整个页面逻辑是旧链接,会有可能导致带宽浪费或者资源 404
。
因此顺着这个思路来看,我们可以在 skipWaiting
后,再调用 window.reload()
,强制刷新页面,从而实现整个页面的重新更新。
我们可以在 Main thread
中监听 controllerchange
事件:
navigator.serviceWorker.addEventListener('controllerchange', () => {
window.location.reload()
})
当上述逻辑涉及到 Chrome Dev Tools
的 Update on Reload
功能时,为了防止无限刷新,可以额外添加一个 flag
变量:
let refreshing = false
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (refreshing) {
return
}
refreshing = true
window.location.reload()
})
✅ 优点:
相对后续方式来说,这种方式依然比较清晰简单。
❌ 缺点:
无缘无故的刷新客户端页面,不利于用户体验。
TIP
不推荐使用这种方式。生产环境应用性需谨慎。
3.用户自主控制更新
本节以 vue-cli@4
为例,完整版 demo
可参考pwa-app-updates
- 在
service-worker.js
文件中,预留message
事件监听,用于监听来自Main thread
的消息:
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting()
}
})
- 在
registerServiceWorker.js
中,注册回调函数:
{
// e.g. hourly checks
registered (registration) {
setInterval(() => {
registration.update()
}, 1000 * 60 * 60)
},
updated (registration) {
document.dispatchEvent(
new CustomEvent('swUpdated', { detail: registration })
)
}
}
- 添加通知组件
DOM
:
<template>
<button v-if="updateExists" @click="refreshApp">
New version available! Click to update
</button>
</template>
- 在组件中监听
swUpdated
事件:
export default {
data() {
return {
refreshing: false,
registration: null,
updateExists: false,
}
},
created () {
document.addEventListener(
'swUpdated', this.showRefreshUI, { once: true }
)
if (navigator.serviceWorker) {
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (this.refreshing) return
this.refreshing = true
window.location.reload()
}
)
}
},
methods: {
showRefreshUI (e) {
this.registration = e.detail
this.updateExists = true
},
refreshApp () {
this.updateExists = false
if (!this.registration || !this.registration.waiting) { return }
this.registration.waiting.postMessage({
type: 'SKIP_WAITING'
})
}
}
}
✅ 优点:
该方案相对成熟,也是目前大部分 PWA
应用采用的更新策略。
- 当
serviceWorker
检测到资源更新时,在客户端安装完毕后,会等待激活。那么此时会弹出更新按钮供用户点击; - 当用户点击后,会尝试激活新的
serviceWorker
; - 当新的
serviceWorker
激活后,会利用window.location.reload()
刷新页面,从而实现当前页面的更新。
❌ 缺点:
- 很繁琐,涉及到的文件众多,需要聚合逻辑。
- 页面上添加了额外组件,对于生产项目来说,会影响
UI
样式。而且需要用户点击,增加了部分心智负担。
4.单个导航刷新
当客户端只存在单个 tab
时,此时刷新该 tab
,即刻更新 serviceWorker
,并且为了保证资源的更新,重载页面。
核心代码如下:
// service-worker.js
self.addEventListener('fetch', event => {
event.respondWith((async () => {
if (event.request.mode === 'navigate' &&
event.request.method === 'GET' &&
self.registration.waiting &&
(await clients.matchAll()).length < 2
) {
self.registration.waiting.postMessage({
type: 'SKIP_WAITING'
})
return new Response('', {
headers: {
'Refresh': '0'
}
})
}
return await caches.match(event.request) ||
fetch(event.request)
})())
})
其中:
event.request.mode === 'navigate'
表示当前请求是navigate
请求,即浏览器导航;(await clients.matchAll()).length < 2
表示单个tab
,(仅限单个tab
导航刷新时);new Response('', { headers: { 'Refresh': '0' } })
表示页面导航后,重新定向刷新(延迟0
秒)。
TIP
该条更新策略,可根据实际业务场景,选择性与第 3
条更新策略结合使用。
✅ 优点:
- 对用户来说,侵入性较弱,体验较好。
❌ 缺点:
- 出现短暂白屏,
Refresh: '0'
重定向导致; - 浏览器兼容性较差。(截止到
2024/05/22
,firefox@125.0.3
版本执行依然失败)。