Workbox cache URLs from message event - javascript

I try to add some routes in cache using message event.
On every page, there are several on dynamic that I would like to keep in cache. For this, i send an array of URL to my Service Worker on document load :
window.addEventListener('load', () => {
if (serviceWorker.isServiceWorkerSupported()) {
serviceWorker.register();
if (typeof PRECACHE_ROUTES !== 'undefined') {
serviceWorker.sendPreCacheRoutesToSW(PRECACHE_ROUTES);
}
}
});
But, with this method, if user have no network, the StaleWhileRevalidate same not work, you can see an example :
registerRoute(
'/',
new StaleWhileRevalidate({
cacheName: 'routes', // Work on offline
plugins,
}),
);
self.addEventListener('message', event => {
if (event.data && event.data.type === 'PRECACHE_ROUTES') {
event.data.routes.forEach(route => {
registerRoute(
route,
new StaleWhileRevalidate({
cacheName: 'routes', // Not work on offline
}),
);
});
event.waitUntil(
caches.open('routes').then(cache => cache.addAll(event.data.routes)),
);
}
});
All urls are well cached, but do not seem to be taken into account offline.
Anyone can help me ?

I would suggest following this recipe using workbox-window and workbox-routing to accomplish that:
// From within your web page, using workbox-window:
const wb = new Workbox('/sw.js');
wb.addEventListener('activated', (event) => {
// Get the current page URL + all resources the page loaded.
// Replace with a list of URLs obtained elsewhere, as needed.
const urlsToCache = [
location.href,
...performance.getEntriesByType('resource').map((r) => r.name),
];
// Send that list of URLs to your router in the service worker.
wb.messageSW({
type: 'CACHE_URLS',
payload: {urlsToCache},
});
});
// Register the service worker after event listeners have been added.
wb.register();
This will automatically apply the routes defined in your service worker to the URLs you provide in the payload.
Dynamically setting up routes for those URLs inside of your message event isn't going to give you the behavior you're after, as you've found.

Related

Function onLoad in SPA Page

I have SPA page, all work very good but when user reload page beeing on winners or garage get info :
Cannot GET /Garage. Then have to pick default url. How to set reload function on current page.
https://darogawlik-async-race-api.netlify.app/ (my app)
const navigateTo = url => {
history.pushState(null, null, url)
router()
}
const router = async () => {
const routes = [
{ path: '/Garage', view: garage },
{ path: '/Winners', view: winners },
]
// Test each route for potential match
const potentialMatches = routes.map(route => ({
route,
isMatch: location.pathname === route.path,
}))
let match = potentialMatches.find(potentialMatches => potentialMatches.isMatch)
if (!match) {
match = {
route: routes[0],
isMatch: true,
}
}
const view = new match.route.view(document.querySelector('#main'))
}
window.addEventListener('popstate', router)
document.addEventListener('DOMContentLoaded', () => {
document.body.addEventListener('click', e => {
if (e.target.matches('[data-link]')) {
e.preventDefault()
navigateTo(e.target.href)
}
})
router()
})
window.addEventListener('load', router())
This will be a problem with default document handling in the web host - it is not a page load problem. Eg just click this link to get the problem:
https://darogawlik-async-race-api.netlify.app/Garage
Since you are using path based routing, your web host must serve the default document for all paths, including /Garage and /Winners. As an example, in Node.js Express you write code like this. For other web hosts you either write similar code or there is a configuration option that will do it for you.
// Serve static content for physical files, eg .js and .css files
expressApp.use('/', express.static());
// Serve the index.html for other paths
expressApp.get('*', (request, response) => {
response.sendFile('index.html');
}
According to this post on Netlify, you can add a file something like this. I'm no expert on this platform, but hopefully this gives you the info you need to resolve your issue:
[[redirects]]
from = "/*"
to = "/index.html"
status = 200

is there any way to find out which endpoints are triggered on a page?

I have a generic implementation to fire a page_view google analytics event in my react application every time there's a route change:
const usePageViewTracking = () => {
const { pathname, search, hash } = useLocation();
const pathnameWithTrailingSlash = addTrailingSlashToPathname(pathname) + search + hash;
useEffect(() => {
invokeGAPageView(pathnameWithTrailingSlash);
}, [pathname]);
};
export default usePageViewTracking;
This works fine, but I need to fire ga4 page_view events with custom dimensions and if the page doesn't have some data, I should not send it in page_view event.
I turned my previous code into this:
const usePageViewTracking = () => {
const { pathname, search, hash } = useLocation();
const subscriptionsData = useAppSelector(
(state) => state?.[REDUX_API.KEY]?.[REDUX_API.SUBSCRIPTIONS]?.successPayload?.data
);
useEffect(() => {
sendPageViewEvent({ subscriptionsData });
}, [pathname, subscriptionsData]);
};
export default usePageViewTracking;
sendPageViewEvent is where I collect most of the information I need to be sent, and currently is like this:
export const sendPageViewEvent = ({ subscriptionsData }: SendPageViewEventProps): void => {
const { locale, ga } = window.appData;
const { subscriptions, merchants, providers } =
prepareSubscriptionsData({ subscriptionsData }) || {};
const events = {
page_lang: locale || DEFAULT_LOCALE,
experiment: ga.experiment,
consent_status: Cookies.get(COOKIES.COOKIE_CONSENT) || 'ignore',
...(subscriptionsData && {
ucp_subscriptions: subscriptions,
ucp_payment_providers: providers,
ucp_merchants: merchants,
}),
};
sendGA4Event({ eventType: GA4_EVENT_TYPE.PAGE_VIEW, ...events });
};
So as you can see, I have some dimensions that are always sent, and some that are conditionally sent (subscriptionsData).
The problem
The problem with this implementation is that once the page renders, it waits for subscriptionData to be available to fire the event, which would be ok, if this data would be fetched in all pages. If a page doesn't have this data, I still need to send the event, just not attach subscriptions dimensions into it.
I tried different approaches in my application, like:
going to each page and firing it individually, but since it's a huge application, it would require a huge refactoring that turns out to not to be justified for analytics purposes.❌
Having some sort of config file to define which routes fire which endpoints, but this is a terrible and unmaintainable idea ❌
Now if there would be a way to know based on the redux store how figure out which endpoints are being triggered on a page, maybe I could then detect it and decide if I should wait for this property to be available or fire the event without it.
PS: there will be more fetched data from different endpoints that I'll have to fire on page_view experiment too... and it's an SPA aplication (so everything is using CSR).
Any ideas are welcome! :D

How to hard refresh Firefox after Service Worker update?

I have implemented the code below to clear service worker cache and reloads - after the user has accepted update of the service worker. The code works well in Chrome and Edge, but Firefox will not reload the page. Firefox will keep asking to install the same version until I hard refresh (shift reload) the page.
service-worker-base.js
// Imports
const CACHE_DYNAMIC_NAME = 'DEBUG-035'
setCacheNameDetails({ prefix: 'myApp', suffix: CACHE_DYNAMIC_NAME });
// Cache then network for css
registerRoute(
'/dist/main.css',
new StaleWhileRevalidate({
cacheName: `${CACHE_DYNAMIC_NAME}-css`,
plugins: [
new ExpirationPlugin({
maxEntries: 10, // Only cache 10 requests.
maxAgeSeconds: 60 * 60 * 24 * 7 // Only cache requests for 7 days
})
]
})
)
// Cache then network for images
//...
// Use a stale-while-revalidate strategy for all other requests.
setDefaultHandler(new StaleWhileRevalidate())
precacheAndRoute(self.__WB_MANIFEST)
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting()
}
})
// Clear cache before installing new service worker
self.addEventListener('activate', (event) => {
var cachesToKeep = ['none'];
event.waitUntil(
caches.keys().then((keyList) => {
return Promise.all(keyList.map((key) => {
if (cachesToKeep.indexOf(key) === -1) {
console.log('Delete cache', key)
return caches.delete(key);
}
}));
})
);
event.waitUntil(self.clients.claim());
});
//...
app.js
const enableServiceWorker = process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'qa'
const serviceWorkerAvailable = ('serviceWorker' in navigator) ? true : false
if (enableServiceWorker && serviceWorkerAvailable) {
const wb = new Workbox('/service-worker.js');
let registration;
const showSkipWaitingPrompt = (event) => {
if (window.confirm("New version available! Refresh?")) {
wb.addEventListener('controlling', (event) => {
window.location.reload();
});
console.log('registration', registration) //<-- LINE 13
// In Chrome and Edge this logs a service worker registration object
// In Firefox, this is undefined !!?
if (registration && registration.waiting) {
messageSW(registration.waiting, {type: 'SKIP_WAITING'});
}
}
}
// Add an event listener to detect when the registered service worker has installed but is waiting to activate.
wb.addEventListener('waiting', showSkipWaitingPrompt);
wb.addEventListener('externalwaiting', showSkipWaitingPrompt);
wb.register().then((r) => {
registration = r
console.log('Service worker registered', registration) //<-- LINE 23
}).catch(registrationError => {
console.error('Service worker error', registrationError )
})
}
// Install prompt event handler
export let deferredPrompt
window.addEventListener('beforeinstallprompt', (event) => {
event.preventDefault() // Prevent Chrome 76 and later from showing the mini-infobar
deferredPrompt = event // Stash the event so it can be triggered later.
// Update UI notify the user they can add to home screen
try{
showInstallPromotion()
}catch(e){
// console.log('showInstallPromotion()', e)
}
})
window.addEventListener('appinstalled', (event) => {
console.log('a2hs installed')
})
In Firefox dev-tools I can see the new service worker precache, but all other cache belongs to previous version. After shift-reload the new service worker gets "fully activated".
How can I get Firefox to hard reload the page automatically after new service worker install?
UPDATE: It seems like Firefox is missing a handle to the service worker on line 13 of app-js.
UPDATE: Console output indicates that the code sequence differs between browsers?
Chrome / Edge
registration > ServiceWorkerRegistration {installing: null, waiting: ServiceWorker, active: ServiceWorker, navigationPreload: NavigationPreloadManager, scope: "http://127.0.0.1:8080/", …} app.js:13
**PAGE RELOAD***
Service worker registered ServiceWorkerRegistration {installing: null, waiting: null, active: ServiceWorker, navigationPreload: NavigationPreloadManager, scope: "http://127.0.0.1:8080/", …} app.js:23
Firefox
registration undefined app.js:13:14
Service worker registered > ServiceWorkerRegistration { installing: null, waiting: ServiceWorker, active: ServiceWorker, scope: "http://127.0.0.1:8080/", updateViaCache: "imports", onupdatefound: null, pushManager: PushManager } app.js:23:12
Kind regards /K
This might help you , please check controllerchange of serviceworker.
As per this documentations:- The oncontrollerchange property of the ServiceWorkerContainer interface is an event handler fired whenever a "controllerchange event occurs" — when the document's associated ServiceWorkerRegistration acquires a new active worker.
To use it, you can attach an event handler and it will be triggered only when a new service worker activates. and If you want you can reload the page using reload function.
navigator.serviceWorker.addEventListener('controllerchange', function(){
window.location.reload();
});
I created a special case since Firefox seems to install the new service-worker differently from chromium (does not have a handle to the service-worker registration on line 13)
When the new service worker is waiting showSkipWaitingPrompt gets triggered and
in Chromium the service-worker registration is ready ---> we call SKIP_WAITING --> the browser reloads and replaces the service worker
in Firefox the service-worker registration handle is not accessible yet --> we cannot call SKIP_WAITING
The solution, for me, was to add the below line in the registration. This tells Firefox to skip waiting when the new service-worker is in waiting state and we have a registration handle.
wb.register().then((r) => {
registration = r
if(registration.waiting){ mySkipWaitingNow() } // Fix for firefox
...
The mySkipWaitingNow() tells the service-worker to SKIP_WAITING without prompting the user.
This will never trigger in Chrome/Edge since the browser reloads in showSkipWaitingPrompt() - see point 1 above.
To prevent a possible eternal loop I also created a global variable skipWaitingConfirmed that gets set in showSkipWaitingPrompt() and checked in mySkipWaitingNow().
/K

How do you retrieve a value from IndexedDB when using serviceworkers?

I am new to IndexedDB and serviceworkers and am having a very difficult time understanding how to turn these into a funcitonal application. I've done extensive reading on both, but even the "complete" examples don't incorporate the two.
I am tasked with creating an application that will allow users to work offline. The first time they connect to the site, I want to pull specific information from the database and store it in IndexedDB. When they go offline, I need to use that data to display information on the page. Certain interactions will cause the data to update, then to be synced later once an internet connection is reestablished. From a high-level, I udnerstand how this works.
It is my understanding that we cannot call functions from the serviceworker.js file due to the asynchronous nature of serviceworkers. Additionally, serviceworkers.js cannot directly update the DOM. However, the examples I have seen are creating and managing the IndexedDB data within the serviceworkers.js file.
So let's say I have a file:
<!-- index.html -->
<html>
<body>
Hello <span id="name"></span>
</body>
</html>
And a serviceworker.js:
var CACHE_NAME = 'my-cache-v1';
var urlsToCache = [
'/'
// More to be added later
];
self.addEventListener('install', function(event) {
// Perform install steps
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('activate', function(event) {
event.waitUntil(
createDB() //Use this function to create or open the database
);
});
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// Cache hit - return response
if (response) {
return response;
}
return fetch(event.request).then(
function(response) {
// Check if we received a valid response
if(!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
var responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});
function createDB() {
idb.open('mydata', 1, function(upgradeDB) {
var store = upgradeDB.createObjectStore('user', {
keyPath: 'id'
});
store.put({id: 1, name: 'John Doe'}); //This can be updated with an AJAX call to the database later
});
}
How do I now update the element "name" with the value for key = 1 from the "user" objectstore in the "mydata" database?
Depending on your use case, you've got several options :
You dont need the service worker. Just pull your data from iDB directly from the page. The DOM has access to iDB.
Set a template for your index.html. At the activate step in service worker, pre-render the page with the value from iDB and cache it.

Clear Workbox cache of all content

Using Workbox in a service worker in a javascript webapp.
Want to clear the entire workbox/application cache of all content... basically go back to a state as similar as possible to the state before first load of the app into a browser, potentially to be followed by refreshing via window.location.href = '/'.
Googling and looking on SO, I have found various recipes for clearing various things from the cache. I have not been able to figure out a simple way to just 'clear everything and start over'.
I tried this in server code in sw.js:
var logit = true;
if (logit) console.log('Service Worker Starting Up for Caching... Hello from sw.js');
importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.6.1/workbox-sw.js');
if (workbox) {
if (logit) console.log(`Yay! Workbox is loaded 🎉`);
} else {
if (logit) console.log(`Boo! Workbox didn't load 😬`);
}
workbox.routing.registerRoute(
// Cache image files
/.*\.(?:mp3|flac|png|gif|jpg|jpeg|svg|mp4)/,
// Use the cache if it's available
workbox.strategies.cacheFirst({
// Use a custom cache name
cacheName: 'asset-cache',
plugins: [
new workbox.expiration.Plugin({
// Cache only 20 images
maxEntries: 20,
// Cache for a maximum of x days
maxAgeSeconds: 3 * 24 * 60 * 60,
})
],
})
);
self.addEventListener('message', function(event){
msg = event.data;
console.log("SW Received Message: " + msg);
if (msg==='clearCache') {
console.log('Clearing Workbox Cache.');
WorkBoxCache = new workbox.expiration.Plugin;
WorkBoxCache.expirationPlugin.deleteCacheAndMetadata();
//WorkBoxCacheServer.clear();
}
});
paired with this on the client:
navigator.serviceWorker.controller.postMessage("clearCache");
This didn't work, though the message was apparently passed. Also, this seems an inelegant solution and I presume there is a simpler one.
How can this be done?
How can it be initiated from the client side in client side js on the browser? What does this require in server side code (eg in sw.js).
Thank you
CacheStorage is accessible in the client code (where you register the SW) so you can delete it from there.
caches.keys().then(cacheNames => {
cacheNames.forEach(cacheName => {
caches.delete(cacheName);
});
});
If we only delete the cache then it damage service worker ,service worker will not work properly, so we have to unregister service worker then have to delete cache and then reregister service worker.
refreshCacheAndReload = () => {
if ('serviceWorker' in navigator) {
serviceWorkerRegistration.unregister();
caches.keys().then(cacheNames => {
cacheNames.forEach(cacheName => {
caches.delete(cacheName);
});
}).then(() => {
serviceWorkerRegistration.register();
})
}
setTimeout(function () { window.location.replace(""); }, 300)
}

Categories