Migrate Google Workbox setDefaultHandler // setCatchHandler from v2 to v3 - service-worker

I'm trying to migrate my old code from google workbox v2 to workbox v3, and i can't use workbox.routing.registerNavigationRoute because my default route '/' (which is where my appshell is) is a runtime cache (because it's for a multilingual website https://www.autovisual.com with languages put in subfolder '/fr', '/es' ... with a unique Service-Worker scoped at '/').
This is the v2 code :
workboxSW.router.setDefaultHandler({
handler: ({
event
}) => {
return fetch(event.request);
}
});
workboxSW.router.setCatchHandler({
handler: ({
event
}) => {
if (event.request.mode === 'navigate') {
return caches.match('/');
}
return new Response();
}
});
It seems pretty basic : the goal is to catch all request 'navigate' that didn't match any other route and send the cached version, network first, of the url '/'.
For the info in the client js i use :
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
caches.open('rootCacheNetworkFirst').then(function(cache) {
cache.match('/').then(function(response) {
if (!response) {
cache.add('/');
}
});
});
navigator.serviceWorker.register('/sw.js', {
scope: "/"
});
});
}
I can't find any example with the new v3 workbox.routing.setDefaultHandler and workbox.routing.setCatchHandler and i'm stuck :(

I don't think that using either setDefaultHandler or setCatchHandler is relevant for that described use case.
To accomplish what you describe, add the following code to your service worker file after all other routes are registered. (In Workbox v3, the first-registered-route takes precedence.) You just need to configure a NavigationRoute and register it:
const networkFirst = workbox.strategies.networkFirst({
cacheName: 'your-cache-name',
});
const navigationRoute = new workbox.routing.NavigationRoute(networkFirst, {
// Set blacklist/whitelist if you need more control
// over which navigations are picked up.
blacklist: [],
whitelist: [],
});
workbox.router.registerRoute(navigationRoute);

Related

Workbox precache doesn't precache

I'm attempting to implement workbox to precache image and video assets on a website.
I've created a service worker file. It appears to be successfully referenced and used in my application. The service worker:
import { clientsClaim, setCacheNameDetails } from 'workbox-core';
import { precacheAndRoute, addRoute } from 'workbox-precaching';
const context = self; // eslint-disable-line no-restricted-globals
setCacheNameDetails({ precache: 'app' });
// eslint-disable-next-line no-restricted-globals, no-underscore-dangle
precacheAndRoute(self.__WB_MANIFEST);
context.addEventListener('install', (event) => {
event.waitUntil(
caches.open('dive').then((cache) => {
console.log(cache);
}),
);
});
context.addEventListener('activate', (event) => {
console.log('sw active');
});
context.addEventListener('fetch', async (event) => {
console.log(event.request.url);
});
context.addEventListener('message', ({ data }) => {
const { type, payload } = data;
if (type === 'cache') {
payload.forEach((url) => {
addRoute(url);
});
const manifest = payload.map((url) => (
{
url,
revision: null,
}
));
console.log('attempting to precache and route manifest', JSON.stringify(manifest));
precacheAndRoute(manifest);
}
});
context.skipWaiting();
clientsClaim();
The application uses workbox-window to load, reference and message the service worker. The app looks like:
import { Workbox } from 'workbox-window';
workbox = new Workbox('/sw.js');
workbox.register();
workbox.messageSW({
type: 'cache',
payload: [
{ url: 'https://media0.giphy.com/media/Ju7l5y9osyymQ/giphy.gif' }
],
});
This project is using vue with vue-cli. It has a webpack config which allows plugins to be sent added to webpack. The config looks like:
const { InjectManifest } = require('workbox-webpack-plugin');
const path = require('path');
module.exports = {
configureWebpack: {
plugins: [
new InjectManifest({
swSrc: path.join(__dirname, 'lib/services/Asset-Cache.serviceworker.js'),
swDest: 'Asset-Cache.serviceworker.js',
}),
],
},
};
I can see messages are successfully sent to the service worker and contain the correct payload. BUT, the assets never show up in Chrome dev tools cache storage. I also never see any workbox logging related to the assets sent via messageSW. I've also tested by disabling my internet, the assets don't appear to be loading into the cache. What am I doing wrong here?
I found the workbox docs to be a little unclear and have also tried to delete the message event handler from the service worker. Then, send messages to the service worker like this:
workbox.messageSW({
type: 'CACHE_URLS',
payload: { urlsToCache: [ 'https://media0.giphy.com/media/Ju7l5y9osyymQ/giphy.gif'] },
});
This also appears to have no effect on the cache.
The precache portion of precacheAndRoute() works by adding install and activate listeners to the current service worker, taking advantage of the service worker lifecycle. This will effectively be a no-op if the service worker has already finished installing and activating by the time it's called, which may be the case if you trigger it conditionally via a message handler.
We should probably warn folks about this ineffective usage of precacheAndRoute() in our Workbox development builds; I've filed an issue to track that.

Request method 'POST' is unsupported

The following config throws an error of 'Request method 'POST' is unsupported'. I already read that the storage api does not request objects with a method of POST as keys in a cache, but I have no clue how to add a route, which manifests a networkOnly strategy for those requests.
Specs (setup taken from https://github.com/nystudio107/annotated-webpack-4-config)
Using GenerateSW
webpack.settings.js (remember the importScripts statement)
workboxConfig: {
swDest: "../sw.js",
precacheManifestFilename: "js/precache-manifest.[manifestHash].js",
importScripts: [
"/dist/workbox-catch-handler.js"
],
exclude: [
/\.(png|jpe?g|gif|svg|webp)$/i,
/\.map$/,
/^manifest.*\\.js(?:on)?$/,
],
globDirectory: "./web/",
globPatterns: [
"offline.html",
"offline.svg"
],
offlineGoogleAnalytics: true,
runtimeCaching: [
{
urlPattern: /\.(?:png|jpg|jpeg|svg|webp)$/,
handler: "cacheFirst",
options: {
cacheName: "images",
expiration: {
maxEntries: 20
}
}
}
]
}
wepack.prod.js
// webpack.prod.js - production builds
const LEGACY_CONFIG = 'legacy';
const MODERN_CONFIG = 'modern';
const WorkboxPlugin = require('workbox-webpack-plugin');
// config files
const settings = require('./webpack.settings.js');
const common = require('./webpack.common.js');
...
// Configure Workbox service worker
const configureWorkbox = () => {
let config = settings.workboxConfig;
return config;
};
// Module Exports – simplified for clarity - see github repro for more details
module.exports = [
...
...,
merge(
common.modernConfig,
{
...
...
plugins: [
...
new WorkboxPlugin.GenerateSW(
configureWorkbox()
),
]
}
]
workbox-catch-handler.js
// fallback URLs
const FALLBACK_HTML_URL = '/offline.html';
const FALLBACK_IMAGE_URL = '/offline.svg';
// This "catch" handler is triggered when any of the other routes fail to
// generate a response.
workbox.routing.setCatchHandler(({event, request, url}) => {
// Use event, request, and url to figure out how to respond.
// One approach would be to use request.destination, see
// https://medium.com/dev-channel/service-worker-caching-strategies-based-on-request-types-57411dd7652c
switch (request.destination) {
case 'document':
return caches.match(FALLBACK_HTML_URL);
break;
case 'image':
return caches.match(FALLBACK_IMAGE_URL);
break;
default:
// If we don't have a fallback, just return an error response.
return Response.error();
}
});
// Use a stale-while-revalidate strategy for all other requests.
workbox.routing.setDefaultHandler(
workbox.strategies.staleWhileRevalidate()
);
The error is caused by the strategy of the DefaultHandler, so I tried to add another route for those requests right below the DefaultHandler with no success. Eg:
workbox.routing.registerRoute(
new RegExp('*/admin/*'),
workbox.strategies.networkOnly()
);
I also tried the bgSyncPlugin with no success. Any help is appreciated. I'd like to implement a side wide networkOnly strategy for POST requests (not only for admin URLS).
You can't cache POST requests with the Cache API, meaning you can't use a network first strategy.
See: Can service workers cache POST requests?
You might be able to do something with a network request (i.e. change the request type in the service worker by reading a POST response and generating a new response to put in the Cache API). This will require a custom strategy.
To access POST requests with the Workbox router, see: https://developers.google.com/web/tools/workbox/modules/workbox-routing#defining_a_route_for_non-get_requests
To write your own function to handle a network request see: https://developers.google.com/web/tools/workbox/modules/workbox-routing#matching_and_handling_in_routes
You might be able to re-use some of the workbox strategies, check here for details no how that might work: https://developers.google.com/web/tools/workbox/guides/advanced-recipes#make-requests

What's the right way to implement offline fallback with workbox

I am implementing PWA into my project, I have setted up the serviceworker.js, and I am using workbox.js for cache routing and strategies.
1- I add the offline page to cache on install event, when a user first visit the site:
/**
* Add on install
*/
self.addEventListener('install', (event) => {
const urls = ['/offline/'];
const cacheName = workbox.core.cacheNames.runtime;
event.waitUntil(caches.open(cacheName).then((cache) => cache.addAll(urls)))
});
2- Catch & cache pages with a specific regex, like these:
https://website.com/posts/the-first-post
https://website.com/posts/
https://website.com/articles/
workbox.routing.registerRoute(
new RegExp('/posts|/articles'),
workbox.strategies.staleWhileRevalidate({
cacheName: 'pages-cache'
})
);
3- Catch errors and display the offline page, when there's no internet connection.
/**
* Handling Offline Page fallback
*/
this.addEventListener('fetch', event => {
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('/offline/');
})
);
}
else{
// Respond with everything else if we can
event.respondWith(caches.match(event.request)
.then(function (response) {
return response || fetch(event.request);
})
);
}
});
Now this is working for me so far if I visit for example: https://website.com/contact-us/ but if I visit any url within the scope I defined earlier for "pages-cache" like https://website.com/articles/231/ this would not return the /offline page since it's not in the user cache, and I would get a regular browser error.
There's an issue in how errors are handled, when there's a specific caching route by workbox.
Is this the best method to apply for offline fallback? how can I catch errors from these paths: '/articles' & '/posts' and display an offline page?
Please refer as well to this answer where there's a different
approach to applying the fallack with workbox, I tried it as well same
results. Not sure which is the accurate approach for this.
I found a way to do it right with workbox.
For each route I would add a fallback method like this:
const offlinePage = '/offline/';
/**
* Pages to cache
*/
workbox.routing.registerRoute(/\/posts.|\/articles/,
async ({event}) => {
try {
return await workbox.strategies.staleWhileRevalidate({
cacheName: 'cache-pages'
}).handle({event});
} catch (error) {
return caches.match(offlinePage);
}
}
);
In case of using network first strategy this is the method:
/**
* Pages to cache (networkFirst)
*/
var networkFirst = workbox.strategies.networkFirst({
cacheName: 'cache-pages'
});
const customHandler = async (args) => {
try {
const response = await networkFirst.handle(args);
return response || await caches.match(offlinePage);
} catch (error) {
return await caches.match(offlinePage);
}
};
workbox.routing.registerRoute(
/\/posts.|\/articles/,
customHandler
);
More details at workbox documentation here: Provide a fallback response to a route

Service worker spreading

I have a service worker for caching images, this service worker is only registered within the frontend template but it still keeps spreading into my admin template.
This causes my forms to behave unpredictably as the validation tokens get impacted with it.
With some console.log I figured the install event is triggered before getting to the requested page but I'm unable to determine the current/next URL there.
How can I prevent the service worker to spreading to the admin panel and interfere with the pages? I just want only assets to be cached.
This is my service worker as far as that is relevant:
const PRECACHE = 'precache-v1.0.0';
const RUNTIME = 'runtime';
// A list of local resources we always want to be cached.
const PRECACHE_URLS = [
"public",
"media",
"unify",
];
importScripts('./cache-polyfill.js');
// The install handler takes care of precaching the resources we always need.
self.addEventListener('install', function(event) {
console.log('installing resources');
event.waitUntil(
caches.open(PRECACHE)
//.then(cache => cache.addAll(PRECACHE_URLS))
.then(self.skipWaiting())
);
});
// The activate handler takes care of cleaning up old caches.
self.addEventListener('activate', function(event) {
const currentCaches = [PRECACHE, RUNTIME];
event.waitUntil(
caches.keys().then(cacheNames => {
return cacheNames.filter(cacheName => !currentCaches.includes(cacheName));
}).then(cachesToDelete => {
return Promise.all(cachesToDelete.map(cacheToDelete => {
return caches.delete(cacheToDelete);
}));
}).then(() => self.clients.claim())
);
});
// The fetch handler serves responses for same-origin resources from a cache.
// If no response is found, it populates the runtime cache with the response
// from the network before returning it to the page.
self.addEventListener('fetch', event => {
// Skip cross-origin requests, like those for Google Analytics.
if (event.request.method === "GET") {
if (event.request.url.indexOf(PRECACHE_URLS) > -1) {
console.log("fetching " + event.request.url + " by the service worker");
event.respondWith(
caches.match(event.request).then(cachedResponse => {
if (cachedResponse) {
return cachedResponse;
}
return caches.open(RUNTIME).then(cache => {
return fetch(event.request).then(response => {
// Put a copy of the response in the runtime cache.
return cache.put(event.request, response.clone()).then(() => {
console.log('cached: ' + event.request.url);
return response;
});
});
});
})
);
}
else {
console.log("fetching " + event.request.url + " by service worker blocked, it's not a resource");
}
}
return fetch(event.request);
});
The problem is most likely that your admin pages lie inside the SW scope. This means that your SW controls eg. everything in / and your admin pages are located in /admin/ or something.
You can prevent the behaviour by checking the fetch requests your SW is intercepting. Something like:
if (event.request.url.match('^.*(\/admin\/).*$')) {
return false;
}
This should be the first thing in the SW's fetch listener. It checks whether it received a request for something from the admin pages and then cancels out if it did. Otherwise, it continues normally.

Falcor Router should return the value from external API

I am new to JavaScript frameworks and currently trying to setup a falcor router calling an external api (for now consider it as an express api app + mango db, hosted at 3000 port).
Now, I am able to use the request package (commented out lines) and successfully call the Express Api app (which returns obj.rating = 4). But I am unable to send this value from the falcor router instead of the hard-coded value "5".
Below is the falcor-router's server.js code:
app.use('/rating.json', falcorExpress.dataSourceRoute(function (req, res) {
return new Router([
{
route: "rating",
get: function() {
var obj;
// request('http://localhost:3000/rating/101', function (error, response, body) {
// obj = JSON.parse(body);
// console.log('rating:', obj.rating); // obj.rating = 4
// });
return {path:["rating"], value:"5"};
}
}
]);
}));
The below is the code for index.html:
<script>
function showRating() {
var model = new falcor.Model({source: new falcor.HttpDataSource('http://localhost/rating.json') });
model.
get("rating").
then(function(response) {
document.getElementById('filmRating').innerText = JSON.stringify(response.json,null, 4);
});
}
</script>
I also tried to look at the global variable declaration, synchronize http request calls, promises, then statements etc. But nothing seemed to work, clearly I am missing out something here - not sure what.
The router's get handler expects the return value to be a promise or an observable that resolves to a pathValue. To get your request against the db to work, simply return a promise that resolves to a pathValue, e.g.
return new Router([
{
route: "rating",
get: function() {
return request('http://localhost:3000/rating/101', function (error, response, body) {
return { path: ["rating", value: JSON.parse(body).rating };
});
}
}
]);

Resources