I am trying to have a service worker respond to fetch events depending on the type of request made. For static resources I use cache:
// TODO: make cache update when item found
const _fetchOrCache = (cache, request) => {
return cache.match(request).then(cacheResponse => {
// found in cache
if (cacheResponse) {
return cacheResponse
}
// has to add to cache
return fetch(request)
.then(fetchResponse => {
// needs cloning since a response works only once
cache.put(request, fetchResponse.clone())
return fetchResponse
});
}).catch(e => { console.error(e) })
}
for api responses I have already wired up IndexedDB with Jake Archibald's IndexedDB Promised to return content like this:
const fetchAllItems = () => {
return self.idbPromise
.then(conn => conn.transaction(self.itemDB, 'readonly'))
.then(tx => tx.objectStore(self.itemDB))
.then(store => store.getAll())
.then(storeContents => JSON.stringify(storeContents));
}
when I call everything in the service worker the cache part works, but the indexedDB fails miserably throwing an error that it cannot get at the api url:
self.addEventListener("fetch", event => {
// analyzes request url and constructs a resource object
const resource = getResourceInfo(event.request.url);
// handle all cachable requests
if (resource.type == "other") {
event.respondWith(
caches.open(self.cache)
.then(cache => _fetchOrCache(cache, event.request))
);
}
// handle api requests
if (resource.type == "api") {
event.respondWith(
new Response(fetchAllItems());
);
}
});
My questions would be as follows:
1.) Is there any point in separating storing fetch requests like this?
2.) How do I make the indexedDB part work?
good catch on using Jake Archibalds promise based idb. There are many ways to install his idb. The quickest - download the idb.js file somewhere(this is the library). Then import it on the first line in the service worker likeso:
importScripts('./js/idb.js');
.....
//SW installation event
self.addEventListener('install', function (event) {
console.log("[ServiceWorker] Installed");
});
//SW Actication event (where we create the idb)
self.addEventListener('activate', function(event) {
console.log("[ServiceWorker] Activating");
createIndexedDB();
});
.....
//Intercept fetch events and save data in IDB
.....
//IndexedDB
function createIndexedDB() {
self.indexedDB = self.indexedDB || self.mozIndexedDB || self.webkitIndexedDB || self.msIndexedDB;
if (!(self.indexedDB)) { console.console.log('IDB not supported'); return null;}
return idb.open('mydb', 1, function(upgradeDb) {
if (!upgradeDb.objectStoreNames.contains('items')) {
upgradeDb.createObjectStore('items', {keyPath: 'id'});
}
});
}
Judging by the code you pasted above to retrieve IDB data, it is unclear to me what exactly is idbPromise... Are you sure you declared this variable?
You should have something like this
importScripts('./js/idb.js');
//...
//createIdb and store
//...
var idbPromise = idb.open('mydb');
//and after that you have your code like idbPromise.then().then()...
So you create the IDB and the tables during the SW activation. After that you intercept the fetch events and start using the indexeddb like in the tutorials you've seen.
Good luck
Related
I have a PWA with static content that must be 100% available offline. There are 400 HTML pages, and 450 PNG content images and the usual site assets. The total cache size is 21mb and content rarely changes, if ever. My service worker looks like this:
const all_assets = [...nnc, ...nc, ...checklist_content, ...info, ...fonts, ...other]
var cacheName = "ecl-cache-005"
self.addEventListener('install', function (event) {
event.waitUntil((async () => {
const cache = await caches.open(cacheName);
await cache.addAll(all_assets);
})());
});
self.addEventListener('activate', (e) => {
e.waitUntil(caches.keys().then((keyList) => {
return Promise.all(keyList.map((key) => {
if (key === cacheName) { return; }
return caches.delete(key);
}))
}));
});
self.addEventListener('fetch', function (event) {
event.respondWith(
// Try the cache
caches.match(event.request).then(function (response) {
// return it if there is a response,or else fetch again
return response || fetch(event.request);
})
);
});
The app works as intended; however, the install event is very slow, I assume due to the enourmous URL request size in the service worker or the 19mb of images, or both. Is there a better way? Can I cache the images in a separate event after the install?
I've looked into Firestore and IndexedDB options, but they seem not to be compatible with image contents.
I'm trying to implement a basic service worker to assure that users of my simple web app have the latest code. So when I update html, js, or css files I can increment the cachename in the service worker file and prompt users to refresh, clear their cache, and get the latest code.
Until now I've relied on hacky ways to update javascript files (including a parameter in the referring URL: /javascript-file.js?v=1).
The with the service worker code below seem unpredictable: sometimes small changes to JS or CSS are reflected after I increment the cachename (code below). Sometimes the changes are reflected without incrementing the cachename, which suggests the code is ALWAYS pulling from the network (wasting resources).
How can you troubleshoot which version of files the code is using and whether the service worker is using cached or network versions? Am I not understanding the basic model for using service workers to achieve this goal?
Any help appreciated.
serv-worker.js (in root):
console.log('Start serv-worker.js');
const cacheName = '3.2121';
var urlsToCache = [
'home.html',
'home-js.js',
'web-bg.js',
'css/main.css',
'css/edit-menus.css'
];
self.addEventListener('install', event => {
console.log('Install event...', urlsToCache);
event.waitUntil(
caches.open(cacheName)
.then(function(cache) {
console.log('Opened cache', cacheName);
return cache.addAll(urlsToCache);
})
);
});
// Network first.
self.addEventListener('fetch', (event) => {
// Check the cache first
// If it's not found, send the request to the network
// event.respondWith(
// caches.match(event.request).then(function (response) {
// return response || fetch(event.request).then(function (response) {
// return response;
// });
// })
// );
event.respondWith(async function() {
try {
console.log('aPull from network...', event.request);
return await fetch(event.request);
} catch (err) {
console.log('aPull from cache...', event.request);
return caches.match(event.request);
}
}());
});
self.addEventListener('message', function (event) {
console.log('ServiceWorker cache version: ', cacheName, event);
console.log('Received msg1: ', event.data);
if (event.data.action === 'skipWaiting') {
console.log('ccClearing cache: ', cacheName);
// caches.delete('1.9rt1'); // hardcode old one
// caches.delete(cacheName); // actually removes cached versions
caches.keys().then(function(names) {
for (let name of names)
caches.delete(name);
});
self.skipWaiting();
}
});
Code in web-bg.js, which home.html references:
function servWorker(){
let newWorker;
function showUpdateBar() {
console.log('Show the update mssgg...ddddd');
$('#flexModalHeader').html('AP just got better!');
$('#flexModalMsg').html("<p>AP just got better. Learn about <a href='https://11trees.com/support/release-notes-annotate-pro-web-editor/'>what changed</a>.<br><br>Hit Continue to refresh.</p>");
$('#flexModalBtn').html("<span id='updateAPbtn'>Continue</span>");
$('#flexModal').modal('show');
}
// The click event on the pop up notification
$(document).on('click', '#updateAPbtn', function (e) {
console.log('Clicked btn to refresh...');
newWorker.postMessage({ action: 'skipWaiting' });
});
if ('serviceWorker' in navigator) {
console.log('ServiceWORKER 1234');
navigator.serviceWorker.register(baseDomain + 'serv-worker.js').then(reg => {
console.log('In serviceWorker check...', reg);
reg.addEventListener('updatefound', () => {
console.log('A wild service worker has appeared in reg.installing!');
newWorker = reg.installing;
newWorker.addEventListener('statechange', () => {
// Has network.state changed?
console.log('SSState is now: ', newWorker.state);
switch (newWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
// new update available
console.log('Detected service worker update...show update...');
showUpdateBar();
}
// No update available
break;
}
});
});
});
let refreshing;
navigator.serviceWorker.addEventListener('controllerchange', function (e) {
console.log('a1111xxxListen for controllerchange...', e);''
if (refreshing) return;
console.log('Refresh the page...');
window.location.reload();
refreshing = true;
});
} // End serviceworker registration logic
return;
} // END serv-worker
You've commented out the section for /// Check the cache first and then below that the try/catch statement again pulls from the network and falls back to the cache.
Uncomment this section of code and see if you're loading from the cache first.
// event.respondWith(
// caches.match(event.request).then(function (response) {
// return response || fetch(event.request).then(function (response) {
// return response;
// });
// })
// );
Don't forget that even if you request from the network from the service worker the browser will still use it's own internal cache to serve data. How long the data stays in the browser's cache depends on the expiration headers being sent by the server.
When using expires, it's still a fairly common solution to do something like:
index.html - expires after an hour. Has script/css tags that call out file names with ?v=x.y.z
/resources - folder that holds js and css. This folder has a very long expiration time. But that long expiration is short circuited by changing the ?v=x.y.z in index.html
I've used the above successfully in Progressive Web Apps (PWAs). But it is a little painful when debugging. The best option here is to manually clear out the cache and service worker from Dev Tools \ Application, if you're in Chrome.
I need some help here. I'm currently using Chrome 86, trying out a tutorial with JS Service Workers. (This tutorial) to be specific. However, it doesn't seem to work. Upon looking at chrome://serviceworker-internals, I found that my service worker doesn't appear to have a fetch handler. However, this shouldn't be the case since I have added a fetch event listener on my service worker script.
Below is the script.
// Service worker code
'use strict';
var cacheVersion = 1;
var currentCache = {
offline: 'offline-cache' + cacheVersion
};
const offlineURL = '/offline.html';
self.addEventListener('install', event => {
event.waitUntil(
caches.open(currentCache.offline).then(c => {
return c.addAll([
/* Additional resources for offline page, */
/* Don't touch this */
offlineURL
]);
})
);
});
self.addEventListener('fetch', event => { // This seems to not be detected
// request.mode = navigate isn't supported in all browsers
// so include a check for Accept: text/html header.
if (event.request.mode === 'navigate' || (event.request.method === 'GET' && event.request.headers.get('accept').includes('text/html'))) {
event.respondWith(
fetch(event.request.url).catch(error => {
// Return the offline page
return caches.match(offlineUrl);
})
);
}
else{
// Respond with everything else if we can
event.respondWith(caches.match(event.request)
.then(function (response) {
return response || fetch(event.request);
})
);
}
});
I've commented the portion that seems to be buggy.
If anyone could help, that would be appreciated!
Thanks.
I have implemented Workbox to generate my service worker using webpack.
This works pretty well - I can confirm that revision is updated in the generated service worker when running yarn run generate-sw (package.json: "generate-sw": "workbox inject:manifest").
The problem is - I have noticed my clients are not updating the cache after a new release.
Even days after updating the service worker my clients are still caching the old code and new code will only cache after several refreshes and/or unregister the service worker.
For each release the const CACHE_DYNAMIC_NAME = 'dynamic-v1.1.0' is updated.
How can I ensure that clients updates the cache immediately after a new release?
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)
)
})
Should I delete the cache manually or should Workbox do that for me?
caches.keys().then(cacheNames => {
cacheNames.forEach(cacheName => {
caches.delete(cacheName);
});
});
Kind regards /K
I think your problem is related to the fact that when you make an update to the app and deploy, new service worker gets installed, but not activated. Which explains the behaviour why this is happening.
The reason for this is registerRoute function also registers fetch listeners , but those fetch listeners won't be called until new service worker kicks in as activated. Also, the answer to your question: No, you don't need to remove the cache by yourself. Workbox takes care of those.
Let me know more details. When you deploy new code, and if users close all the tabs of your website and open a new one after that, does it start working after 2 refreshes? If so , that's how it should be working. I will update my answer after you provide more details.
I'd suggest you read the following: https://redfin.engineering/how-to-fix-the-refresh-button-when-using-service-workers-a8e27af6df68 and follow the 3rd approach.
One way to get WorkBox to update when you have the files locally, not on a CDN, is the following way:
In your serviceworker.js file add an event listener so that WorkBox skips waiting when there is an update, my code looks like this:
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 ...
}
In your client side page add an event listener for loads if service worker is in the navigator. As a note I am doing this in MVC so I put my code in the _Layout.cshtml so that it can update from any page on my website.
<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>
Note: I used the browser's service worker to update my WorkBox cached files, also, I've only tested this in Chrome, I have not tried it in other browsers.
I implemented the service worker from pwabuilder.com and it works just fine
The problem is that the service worker runs even if the browser is online, so every js functions runs twice, one from the service worker and one from my other js files
Should I look if it's an active service worker before I run my js functions or should I somehow make sure that the service worker is not running when the browser is online?
This is the code I run in my main index file
if (navigator.serviceWorker.controller) {
//console.log('[PWA Builder] active service worker found, no need to register')
} else {
//Register the ServiceWorker
navigator.serviceWorker.register('pwabuilder-sw.js', {
scope: './'
}).then(function (reg) {
//console.log('Service worker has been registered for scope:' + reg.scope);
});
}
The pwabuilder-sw.js looks like this:
self.addEventListener('install', function (event) {
var indexPage = new Request('');
event.waitUntil(
fetch(indexPage).then(function (response) {
return caches.open('pwabuilder-offline').then(function (cache) {
//console.log('[PWA Builder] Cached index page during Install' + response.url);
return cache.put(indexPage, response);
});
}));
});
//If any fetch fails, it will look for the request in the cache and serve it from there first
self.addEventListener('fetch', function (event) {
var updateCache = function (request) {
return caches.open('pwabuilder-offline').then(function (cache) {
return fetch(request).then(function (response) {
//console.log('[PWA Builder] add page to offline' + response.url);
return cache.put(request, response);
});
});
};
event.waitUntil(updateCache(event.request));
event.respondWith(
fetch(event.request).catch(function (error) {
//Check to see if you have it in the cache
//Return response
//If not in the cache, then return error page
return caches.open('pwabuilder-offline').then(function (cache) {
return cache.match(event.request).then(function (matching) {
var report = !matching || matching.status === 404 ? Promise.reject('no-match') : matching;
return report;
});
});
})
);
});
Service Workers are meant to work all the time once registered, installed and activated
Service workers are event driven and their primary use is to act as a caching agent, to handle network requests and to store content for offline use. Secondly to handle push messaging.
I trust you understand that in order to act as a caching agent the service worker will run regardless if the application is online or offline. You have various caching scenarios to consider.
It is hard to provide exact solution for the mentioned: 'every js functions runs twice'.I doubt that all JS functions would always run twice. It seems this is implementation dependant.
Service workers cannot have a scope above their own path, by default it will control all resources below the scope of the service worker, this scope can also be restricted.
navigation.serviceWorker.register(
'/pwabuilder-sw.js', { //SW located at the root level here
scope: '/app/' //to control all resource accessed form within path /app/
}
);
I believe that the script from pwabuilder.com does attempt to cache all resources even resources that should not be cached such as POST requests. You may need to modify the caching policy depending on what type of resources your are using.
There is no simple solution here and no easy answer can be provided.
In general you can use the service worker to cache resources in one of the following ways:
Cache falling back to network
self.addEventListener('fetch', (event) => {
event.responseWith(
caches.match(event.request)
.then((response) => {
return response || fetch(event.request);
})
);
});
Network falling back to cache
//Good for resources that update frequently
//Bad for Intermittend and slow connections
self.addEventListener('fetch', (event) => {
event.responseWith(
fetch(event.request).catch(() => {
return caches.match(event.request);
})
);
});
Cache then network
//Cache then network(1) (send request to cache and network simultaneousely)
//show cached version first, then update the page when the network data arrives
var networkDataReceived = false;
var networkUpdate = fetch('/data.json')
.then((response) => {
return response.json();
}).then((data) => {
networkDataReceived = true;
updatePage(data);
});
//Cache then network(2)
caches.match('/data.json')
.then ((response) => {
return response.json();
}).then((data) => {
if (!networkDataReceived) {
updatePage(data);
}
}).catch(() => {
return networkUpdate;
});
Generic fallback
self.addEventListener('fetch', (event) => {
event.responseWith(
caches.match(event.request)
.then((response) => {
return response || fetch(event.request);
}).catch(() => {
return caches.match('offline.html');
})
)
});
I hope the above helps at least a little find the exact issue you are facing. Cheers and happy codding!
React added this React.strictMode HOC that runs twice certain parts of the application, like class component constructor, render, and shouldComponentUpdate methods.
Check the docs:
https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects.
See if you have <React.StrictMode> at the top of your index.js file and if so, you might want to run a test without it.