新版本上的 Workbox 更新缓存
Workbox update cache on new version
我已经实现了 Workbox 来使用 webpack 生成我的 service worker。
这工作得很好 - 我可以确认 revision
在 运行 yarn run generate-sw
(package.json: "generate-sw": "workbox inject:manifest"
).[=16= 时在生成的服务工作者中更新]
问题是 - 我注意到我的客户在新版本发布后没有更新缓存。
即使在更新 service worker 几天后,我的客户仍在缓存旧代码,新代码只会在几次刷新后缓存 and/or 注销 service worker。
对于每个版本,const CACHE_DYNAMIC_NAME = 'dynamic-v1.1.0'
都会更新。
如何确保客户端在新版本发布后立即更新缓存?
serviceWorker-base.js
importScripts('workbox-sw.prod.v2.1.3.js')
const CACHE_DYNAMIC_NAME = 'dynamic-v1.1.0'
const workboxSW = new self.WorkboxSW()
// Cache then network for fonts
workboxSW.router.registerRoute(
/.*(?:googleapis)\.com.*$/,
workboxSW.strategies.staleWhileRevalidate({
cacheName: 'google-font',
cacheExpiration: {
maxEntries: 1,
maxAgeSeconds: 60 * 60 * 24 * 28
}
})
)
// Cache then network for css
workboxSW.router.registerRoute(
'/dist/main.css',
workboxSW.strategies.staleWhileRevalidate({
cacheName: 'css'
})
)
// Cache then network for avatars
workboxSW.router.registerRoute(
'/img/avatars/:avatar-image',
workboxSW.strategies.staleWhileRevalidate({
cacheName: 'images-avatars'
})
)
// Cache then network for images
workboxSW.router.registerRoute(
'/img/:image',
workboxSW.strategies.staleWhileRevalidate({
cacheName: 'images'
})
)
// Cache then network for icons
workboxSW.router.registerRoute(
'/img/icons/:image',
workboxSW.strategies.staleWhileRevalidate({
cacheName: 'images-icons'
})
)
// Fallback page for html files
workboxSW.router.registerRoute(
(routeData)=>{
// routeData.url
return (routeData.event.request.headers.get('accept').includes('text/html'))
},
(args) => {
return caches.match(args.event.request)
.then((response) => {
if (response) {
return response
}else{
return fetch(args.event.request)
.then((res) => {
return caches.open(CACHE_DYNAMIC_NAME)
.then((cache) => {
cache.put(args.event.request.url, res.clone())
return res
})
})
.catch((err) => {
return caches.match('/offline.html')
.then((res) => { return res })
})
}
})
}
)
workboxSW.precache([])
// Own vanilla service worker code
self.addEventListener('notificationclick', function (event){
let notification = event.notification
let action = event.action
console.log(notification)
if (action === 'confirm') {
console.log('Confirm was chosen')
notification.close()
} else {
const urlToOpen = new URL(notification.data.url, self.location.origin).href;
const promiseChain = clients.matchAll({ type: 'window', includeUncontrolled: true })
.then((windowClients) => {
let matchingClient = null;
let matchingUrl = false;
for (let i=0; i < windowClients.length; i++){
const windowClient = windowClients[i];
if (windowClient.visibilityState === 'visible'){
matchingClient = windowClient;
matchingUrl = (windowClient.url === urlToOpen);
break;
}
}
if (matchingClient){
if(!matchingUrl){ matchingClient.navigate(urlToOpen); }
matchingClient.focus();
} else {
clients.openWindow(urlToOpen);
}
notification.close();
});
event.waitUntil(promiseChain);
}
})
self.addEventListener('notificationclose', (event) => {
// Great place to send back statistical data to figure out why user did not interact
console.log('Notification was closed', event)
})
self.addEventListener('push', function (event){
console.log('Push Notification received', event)
// Default values
const defaultData = {title: 'New!', content: 'Something new happened!', openUrl: '/'}
const data = (event.data) ? JSON.parse(event.data.text()) : defaultData
var options = {
body: data.content,
icon: '/images/icons/manifest-icon-512.png',
badge: '/images/icons/badge128.png',
data: {
url: data.openUrl
}
}
console.log('options', options)
event.waitUntil(
self.registration.showNotification(data.title, options)
)
})
我应该手动删除缓存还是应该由 Workbox 帮我删除?
caches.keys().then(cacheNames => {
cacheNames.forEach(cacheName => {
caches.delete(cacheName);
});
});
亲切的问候/K
我认为您的问题与以下事实有关:当您对应用程序进行更新和部署时,新的 service worker 已安装,但未激活。这解释了为什么会发生这种情况。
原因是 registerRoute
函数还注册了 fetch
侦听器,但是这些获取侦听器只有在新的服务工作线程激活后才会被调用。另外,你的问题的答案:不,你不需要自己删除缓存。 Workbox 负责这些。
让我知道更多详情。当您部署新代码时,如果用户关闭您网站的所有选项卡并在之后打开一个新选项卡,它会在 2 次刷新后开始工作吗?如果是这样,那就是它应该如何工作。在您提供更多详细信息后,我将更新我的答案。
我建议您阅读以下内容:https://redfin.engineering/how-to-fix-the-refresh-button-when-using-service-workers-a8e27af6df68 并遵循第三种方法。
当您在本地而不是 CDN 上拥有文件时,让 WorkBox 更新的一种方法如下:
在您的 serviceworker.js 文件中添加一个事件侦听器,以便 WorkBox 在有更新时跳过等待,我的代码如下所示:
importScripts('Scripts/workbox/workbox-sw.js');
if (workbox) {
console.log('Workbox is loaded :)');
// Add a message listener to the waiting service worker
// instructing it to skip waiting on when updates are done.
addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
skipWaiting();
}
});
// Since I am using Local Workbox Files Instead of CDN I need to set the modulePathPrefix as follows
workbox.setConfig({ modulePathPrefix: 'Scripts/workbox/' });
// other workbox settings ...
}
如果服务工作者在导航器中,则在您的客户端页面中添加一个事件侦听器以加载。请注意,我在 MVC 中执行此操作,因此我将我的代码放在 _Layout.cshtml 中,以便它可以从我网站上的任何页面更新。
<script type="text/javascript">
if ('serviceWorker' in navigator) {
// Use the window load event to keep the page load performant
window.addEventListener('load', () => {
navigator.serviceWorker
// register WorkBox, our ServiceWorker.
.register("<PATH_TO_YOUR_SERVICE_WORKER/serviceworker.js"), { scope: '/<SOME_SCOPE>/' })
.then(function (registration) {
/**
* Whether WorkBox cached files are being updated.
* @type {boolean}
* */
let updating;
// Function handler for the ServiceWorker updates.
registration.onupdatefound = () => {
const serviceWorker = registration.installing;
if (serviceWorker == null) { // service worker is not available return.
return;
}
// Listen to the browser's service worker state changes
serviceWorker.onstatechange = () => {
// IF ServiceWorker has been installed
// AND we have a controller, meaning that the old chached files got deleted and new files cached
// AND ServiceWorkerRegistration is waiting
// THEN let ServieWorker know that it can skip waiting.
if (serviceWorker.state === 'installed' && navigator.serviceWorker.controller && registration && registration.waiting) {
updating = true;
// In my "~/serviceworker.js" file there is an event listener that got added to listen to the post message.
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
}
// IF we had an update of the cache files and we are done activating the ServiceWorker service
// THEN let the user know that we updated the files and we are reloading the website.
if (updating && serviceWorker.state === 'activated') {
// I am using an alert as an example, in my code I use a custom dialog that has an overlay so that the user can't do anything besides clicking okay.
alert('The cached files have been updated, the browser will re-load.');
window.location.reload();
}
};
};
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(function (err) {
//registration failed :(
console.log('ServiceWorker registration failed: ', err);
});
});
} else {
console.log('No service-worker on this browser');
}
</script>
注意:我使用浏览器的 service worker 来更新我的 WorkBox 缓存文件,另外,我只在 Chrome 中测试过,我没有在其他浏览器中尝试过。
我已经实现了 Workbox 来使用 webpack 生成我的 service worker。
这工作得很好 - 我可以确认 revision
在 运行 yarn run generate-sw
(package.json: "generate-sw": "workbox inject:manifest"
).[=16= 时在生成的服务工作者中更新]
问题是 - 我注意到我的客户在新版本发布后没有更新缓存。
即使在更新 service worker 几天后,我的客户仍在缓存旧代码,新代码只会在几次刷新后缓存 and/or 注销 service worker。
对于每个版本,const CACHE_DYNAMIC_NAME = 'dynamic-v1.1.0'
都会更新。
如何确保客户端在新版本发布后立即更新缓存?
serviceWorker-base.js
importScripts('workbox-sw.prod.v2.1.3.js')
const CACHE_DYNAMIC_NAME = 'dynamic-v1.1.0'
const workboxSW = new self.WorkboxSW()
// Cache then network for fonts
workboxSW.router.registerRoute(
/.*(?:googleapis)\.com.*$/,
workboxSW.strategies.staleWhileRevalidate({
cacheName: 'google-font',
cacheExpiration: {
maxEntries: 1,
maxAgeSeconds: 60 * 60 * 24 * 28
}
})
)
// Cache then network for css
workboxSW.router.registerRoute(
'/dist/main.css',
workboxSW.strategies.staleWhileRevalidate({
cacheName: 'css'
})
)
// Cache then network for avatars
workboxSW.router.registerRoute(
'/img/avatars/:avatar-image',
workboxSW.strategies.staleWhileRevalidate({
cacheName: 'images-avatars'
})
)
// Cache then network for images
workboxSW.router.registerRoute(
'/img/:image',
workboxSW.strategies.staleWhileRevalidate({
cacheName: 'images'
})
)
// Cache then network for icons
workboxSW.router.registerRoute(
'/img/icons/:image',
workboxSW.strategies.staleWhileRevalidate({
cacheName: 'images-icons'
})
)
// Fallback page for html files
workboxSW.router.registerRoute(
(routeData)=>{
// routeData.url
return (routeData.event.request.headers.get('accept').includes('text/html'))
},
(args) => {
return caches.match(args.event.request)
.then((response) => {
if (response) {
return response
}else{
return fetch(args.event.request)
.then((res) => {
return caches.open(CACHE_DYNAMIC_NAME)
.then((cache) => {
cache.put(args.event.request.url, res.clone())
return res
})
})
.catch((err) => {
return caches.match('/offline.html')
.then((res) => { return res })
})
}
})
}
)
workboxSW.precache([])
// Own vanilla service worker code
self.addEventListener('notificationclick', function (event){
let notification = event.notification
let action = event.action
console.log(notification)
if (action === 'confirm') {
console.log('Confirm was chosen')
notification.close()
} else {
const urlToOpen = new URL(notification.data.url, self.location.origin).href;
const promiseChain = clients.matchAll({ type: 'window', includeUncontrolled: true })
.then((windowClients) => {
let matchingClient = null;
let matchingUrl = false;
for (let i=0; i < windowClients.length; i++){
const windowClient = windowClients[i];
if (windowClient.visibilityState === 'visible'){
matchingClient = windowClient;
matchingUrl = (windowClient.url === urlToOpen);
break;
}
}
if (matchingClient){
if(!matchingUrl){ matchingClient.navigate(urlToOpen); }
matchingClient.focus();
} else {
clients.openWindow(urlToOpen);
}
notification.close();
});
event.waitUntil(promiseChain);
}
})
self.addEventListener('notificationclose', (event) => {
// Great place to send back statistical data to figure out why user did not interact
console.log('Notification was closed', event)
})
self.addEventListener('push', function (event){
console.log('Push Notification received', event)
// Default values
const defaultData = {title: 'New!', content: 'Something new happened!', openUrl: '/'}
const data = (event.data) ? JSON.parse(event.data.text()) : defaultData
var options = {
body: data.content,
icon: '/images/icons/manifest-icon-512.png',
badge: '/images/icons/badge128.png',
data: {
url: data.openUrl
}
}
console.log('options', options)
event.waitUntil(
self.registration.showNotification(data.title, options)
)
})
我应该手动删除缓存还是应该由 Workbox 帮我删除?
caches.keys().then(cacheNames => {
cacheNames.forEach(cacheName => {
caches.delete(cacheName);
});
});
亲切的问候/K
我认为您的问题与以下事实有关:当您对应用程序进行更新和部署时,新的 service worker 已安装,但未激活。这解释了为什么会发生这种情况。
原因是 registerRoute
函数还注册了 fetch
侦听器,但是这些获取侦听器只有在新的服务工作线程激活后才会被调用。另外,你的问题的答案:不,你不需要自己删除缓存。 Workbox 负责这些。
让我知道更多详情。当您部署新代码时,如果用户关闭您网站的所有选项卡并在之后打开一个新选项卡,它会在 2 次刷新后开始工作吗?如果是这样,那就是它应该如何工作。在您提供更多详细信息后,我将更新我的答案。
我建议您阅读以下内容:https://redfin.engineering/how-to-fix-the-refresh-button-when-using-service-workers-a8e27af6df68 并遵循第三种方法。
当您在本地而不是 CDN 上拥有文件时,让 WorkBox 更新的一种方法如下:
在您的 serviceworker.js 文件中添加一个事件侦听器,以便 WorkBox 在有更新时跳过等待,我的代码如下所示:
importScripts('Scripts/workbox/workbox-sw.js'); if (workbox) { console.log('Workbox is loaded :)'); // Add a message listener to the waiting service worker // instructing it to skip waiting on when updates are done. addEventListener('message', (event) => { if (event.data && event.data.type === 'SKIP_WAITING') { skipWaiting(); } }); // Since I am using Local Workbox Files Instead of CDN I need to set the modulePathPrefix as follows workbox.setConfig({ modulePathPrefix: 'Scripts/workbox/' }); // other workbox settings ... }
如果服务工作者在导航器中,则在您的客户端页面中添加一个事件侦听器以加载。请注意,我在 MVC 中执行此操作,因此我将我的代码放在 _Layout.cshtml 中,以便它可以从我网站上的任何页面更新。
<script type="text/javascript"> if ('serviceWorker' in navigator) { // Use the window load event to keep the page load performant window.addEventListener('load', () => { navigator.serviceWorker // register WorkBox, our ServiceWorker. .register("<PATH_TO_YOUR_SERVICE_WORKER/serviceworker.js"), { scope: '/<SOME_SCOPE>/' }) .then(function (registration) { /** * Whether WorkBox cached files are being updated. * @type {boolean} * */ let updating; // Function handler for the ServiceWorker updates. registration.onupdatefound = () => { const serviceWorker = registration.installing; if (serviceWorker == null) { // service worker is not available return. return; } // Listen to the browser's service worker state changes serviceWorker.onstatechange = () => { // IF ServiceWorker has been installed // AND we have a controller, meaning that the old chached files got deleted and new files cached // AND ServiceWorkerRegistration is waiting // THEN let ServieWorker know that it can skip waiting. if (serviceWorker.state === 'installed' && navigator.serviceWorker.controller && registration && registration.waiting) { updating = true; // In my "~/serviceworker.js" file there is an event listener that got added to listen to the post message. registration.waiting.postMessage({ type: 'SKIP_WAITING' }); } // IF we had an update of the cache files and we are done activating the ServiceWorker service // THEN let the user know that we updated the files and we are reloading the website. if (updating && serviceWorker.state === 'activated') { // I am using an alert as an example, in my code I use a custom dialog that has an overlay so that the user can't do anything besides clicking okay. alert('The cached files have been updated, the browser will re-load.'); window.location.reload(); } }; }; console.log('ServiceWorker registration successful with scope: ', registration.scope); }).catch(function (err) { //registration failed :( console.log('ServiceWorker registration failed: ', err); }); }); } else { console.log('No service-worker on this browser'); } </script>
注意:我使用浏览器的 service worker 来更新我的 WorkBox 缓存文件,另外,我只在 Chrome 中测试过,我没有在其他浏览器中尝试过。