Reputation: 23
I'm working with NextJS and Workbox to create the PWAs and the offline support I need with this library: https://github.com/shadowwalker/next-pwa. There's an example of what I need in the repo above: an offline fallback. I don't need the app to work fully on offline mode, just a fallback page indicating that the connection is lost.
I read the workbox section about the comprehensive fallback:https://developers.google.com/web/tools/workbox/guides/advanced-recipes#comprehensive_fallbacks
There's a catchHandler which is triggered when any of the other routes fail to generate a response, but the problem is that I'm having huge trouble catching the XMLHttpRequests (XHR) errors.
When the request is sent by the client to an API for example, if there's no internet connection, I'd like to render a fallback page instead. The handler only servers the fallback page if the failing request is a "document", and since XHR requests are not documents, I just cannot handle them.
import { clientsClaim } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import {
NetworkOnly,
NetworkFirst,
StaleWhileRevalidate,
} from 'workbox-strategies';
import {
registerRoute,
setDefaultHandler,
setCatchHandler,
} from 'workbox-routing';
import {
precacheAndRoute,
cleanupOutdatedCaches,
matchPrecache,
} from 'workbox-precaching';
clientsClaim();
// must include following lines when using inject manifest module from workbox
// https://developers.google.com/web/tools/workbox/guides/precache-files/workbox-build#add_an_injection_point
const WB_MANIFEST = self.__WB_MANIFEST;
// Precache fallback route and image
WB_MANIFEST.push({
url: '/fallback',
revision: '1234567890',
});
cleanupOutdatedCaches();
precacheAndRoute(WB_MANIFEST);
registerRoute(
'/',
new NetworkFirst({
cacheName: 'start-url',
plugins: [
new ExpirationPlugin({
maxEntries: 1,
maxAgeSeconds: 86400,
purgeOnQuotaError: !0,
}),
],
}),
'GET'
);
// disable image cache, so we could observe the placeholder image when offline
registerRoute(
/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,
new NetworkOnly({
cacheName: 'static-image-assets',
plugins: [
new ExpirationPlugin({
maxEntries: 64,
maxAgeSeconds: 86400,
purgeOnQuotaError: !0,
}),
],
}),
'GET'
);
registerRoute(
/\.(?:js)$/i,
new StaleWhileRevalidate({
cacheName: 'static-js-assets',
plugins: [
new ExpirationPlugin({
maxEntries: 32,
maxAgeSeconds: 86400,
purgeOnQuotaError: !0,
}),
],
}),
'GET'
);
registerRoute(
/\.(?:css|less)$/i,
new StaleWhileRevalidate({
cacheName: 'static-style-assets',
plugins: [
new ExpirationPlugin({
maxEntries: 32,
maxAgeSeconds: 86400,
purgeOnQuotaError: !0,
}),
],
}),
'GET'
);
registerRoute(
/\.(?:json|xml|csv)$/i,
new NetworkFirst({
cacheName: 'static-data-assets',
plugins: [
new ExpirationPlugin({
maxEntries: 32,
maxAgeSeconds: 86400,
purgeOnQuotaError: !0,
}),
],
}),
'GET'
);
registerRoute(
/https:\/\/api[a-z-]*\.pling\.net\.br.*$/i,
new NetworkFirst({
cacheName: 'pling-api',
networkTimeoutSeconds: 10,
plugins: [
new ExpirationPlugin({
maxEntries: 16,
maxAgeSeconds: 86400,
purgeOnQuotaError: !0,
}),
],
}),
'GET'
);
registerRoute(
/https:\/\/[a-zA-Z0-9]+\.cloudfront.net\/.*$/i,
new NetworkFirst({
cacheName: 'cloudfront-assets',
networkTimeoutSeconds: 10,
plugins: [
new ExpirationPlugin({
maxEntries: 32,
maxAgeSeconds: 86400,
purgeOnQuotaError: !0,
}),
],
}),
'GET'
);
registerRoute(
/.*/i,
new NetworkFirst({
cacheName: 'others',
networkTimeoutSeconds: 10,
plugins: [
new ExpirationPlugin({
maxEntries: 32,
maxAgeSeconds: 86400,
purgeOnQuotaError: !0,
}),
],
}),
'GET'
);
setDefaultHandler(new NetworkOnly());
// This "catch" handler is triggered when any of the other routes fail to
// generate a response.
setCatchHandler(({ event }) => {
switch (event.request.destination) {
case 'document':
// If using precached URLs:
return matchPrecache('/fallback');
case 'image':
return matchPrecache('/static/images/fallback.png');
default:
// If we don't have a fallback, just return an error response.
// Switch statement for XHR Requests
return Response.error();
}
});
Upvotes: 1
Views: 1135
Reputation: 56104
The scenario you describe—where a failed XHR originating from a page that's already loaded should trigger an "error page"—is probably best addressed via client-side code in the window
context, rather than via service worker logic. I think that's more in keeping with how service workers are "meant" to be used, and would result in a better user experience.
The code to do this would look something like;
const xhrRequest = new XMLHttpRequest();
// Set request details and make the request.
xhrRequest.addEventListener('error', (event) => {
// Do something to display a "Sorry, an error occurred."
// message within your open page.
});
So instead of trying to load a completely different page when the XHR fails, you'd show an error message somewhere on the existing page. (The details of how to show this message depends on how you're handling your page's UI in general.)
If you really wanted to completely replace the current page with a dedicated error page when your XHR fails, then inside the error
event listener, you could do a window.location.href = '/offline.html'
.
And if you really, really wanted to use service workers for this (for some reason; I don't think you should), you could theoretically use the Clients API within your Workbox-based service worker to force a navigation:
setCatchHandler(async ({ event }) => {
switch (event.request.destination) {
case 'document':
// If using precached URLs:
return matchPrecache('/fallback');
case 'image':
return matchPrecache('/static/images/fallback.png');
default:
if (event.request.url === 'https://example.com/api') {
// See https://developer.mozilla.org/en-US/docs/Web/API/WindowClient/navigate
const client = await self.clients.get(event.clientId);
await client.navigate('/offline.html');
}
// If we don't have a fallback, just return an error response.
// Switch statement for XHR Requests
return Response.error();
}
});
Upvotes: 3