Workbox precache putting items inside -temp cache, and not using it later [duplicate] - service-worker

To enable my app running offline. During installation the service worker should:
fetch a list of URLs from an async API
reformat the response
add all URLs in the response to the precache
For this task I use Googles Workbox in combination with Webpack.
The problem: While the service worker successfully caches all the Webpack assets (which tells me that the workbox basically does what it should), it does not wait for the async API call to cache the additional remote assets. They are simply ignored and neither cached nor ever fetched in the network.
Here is my service worker code:
importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.1.0/workbox-sw.js');
workbox.skipWaiting();
workbox.clientsClaim();
self.addEventListener('install', (event) => {
const precacheController = new
workbox.precaching.PrecacheController();
const preInstallUrl = 'https://www.myapiurl/Assets';
event.waitUntil(fetch(preInstallUrl)
.then(response => response.json()
.then((Assets) => {
Object.keys(Assets.data.Assets).forEach((key) => {
precacheController.addToCacheList([Assets.data.Assets[key]]);
});
})
);
});
self.__precacheManifest = [].concat(self.__precacheManifest || []);
workbox.precaching.suppressWarnings();
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});
workbox.routing.registerRoute(/^.*\.(jpg|JPG|gif|GIF|png|PNG|eot|woff(2)?|ttf|svg)$/, workbox.strategies.cacheFirst({ cacheName: 'image-cache', plugins: [new workbox.cacheableResponse.Plugin({ statuses: [0, 200] }), new workbox.expiration.Plugin({ maxEntries: 600 })] }), 'GET');
And this is my webpack configuration for the workbox:
new InjectManifest({
swDest: 'sw.js',
swSrc: './src/sw.js',
globPatterns: ['dist/*.{js,png,html,css,gif,GIF,PNG,JPG,jpeg,woff,woff2,ttf,svg,eot}'],
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
})

It looks like you're creating your own PrecacheController instance and also using the precacheAndRoute(), which aren't actually intended to be used together (not super well explained in the docs, it's only mentioned in this one place).
The problem is the helper methods on workbox.precaching.* actually create their own PrecacheController instance under the hood. Since you're creating your own PrecacheController instance and also calling workbox.precaching.precacheAndRoute([...]), you'll end up with two PrecacheController instances that aren't working together.
From your code sample, it looks like you're creating a PrecacheController instance because you want to load your list of files to precache at runtime. That's fine, but if you're going to do that, there are a few things to be aware of:
Your SW might not update
Service worker updates are usually triggered when you call navigator.serviceWorker.register() and the browser detects that the service worker file has changed. That means if you change what /Assets returns but the contents of your service worker files haven't change, your service worker won't update. This is why most people hard-code their precache list in their service worker (since any changes to those files will trigger a new service worker installation).
You'll have to manually add your own routes
I mentioned before that workbox.precaching.precacheAndRoute([...]) creates its own PrecacheController instance under the hood. It also adds its own fetch listener manually to respond to requests. That means if you're not using precacheAndRoute(), you'll have to create your own router and define your own routes. Here are the docs on how to create routes: https://developers.google.com/web/tools/workbox/modules/workbox-routing.

I realised my mistake. I hope this helps others as well. The problem was that I did not call precacheController.install() manually. While this function will be executed automatically it will not wait for additional precache files that are inserted asynchronously. This is why the function needs to be called after all the precaching happened. Here is the working code:
importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.1.0/workbox-sw.js');
workbox.skipWaiting();
workbox.clientsClaim();
const precacheController = new workbox.precaching.PrecacheController();
// Hook into install event
self.addEventListener('install', (event) => {
// Get API URL passed as query parameter to service worker
const preInstallUrl = new URL(location).searchParams.get('preInstallUrl');
// Fetch precaching URLs and attach them to the cache list
const assetsLoaded = fetch(preInstallUrl)
.then(response => response.json())
.then((values) => {
Object.keys(values.data.Assets).forEach((key) => {
precacheController.addToCacheList([values.data.Assets[key]]);
});
})
.then(() => {
// After all assets are added install them
precacheController.install();
});
event.waitUntil(assetsLoaded);
});
self.__precacheManifest = [].concat(self.__precacheManifest || []);
workbox.precaching.suppressWarnings();
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});
workbox.routing.registerRoute(/^.*\.(jpg|JPG|gif|GIF|png|PNG|eot|woff(2)?|ttf|svg)$/, workbox.strategies.cacheFirst({ cacheName: 'image-cache', plugins: [new workbox.cacheableResponse.Plugin({ statuses: [0, 200] }), new workbox.expiration.Plugin({ maxEntries: 600 })] }), 'GET');

Related

Workbox Warming Cache Questions

I have api's that I am caching in my app. I would like to cache the api while the service worker is installing. I came across warming the cache:
import {cacheNames} from 'workbox-core';
self.addEventListener('install', (event) => {
const urls = [/* ... */];
const cacheName = cacheNames.runtime;
event.waitUntil(caches.open(cacheName).then((cache) => cache.addAll(urls)));
});
If you use strategies configured with a custom cache name you can do the same thing; just assign your custom value to cacheName.
1) I am using custom cache names. Would I use an array for multiple cache names? ie const cacheName = [ 'foo-api', 'bar'api']?
2) The url's I use are regexp /foo/. Will those rexexp urls work here?
3) Will I be able to cache the api while the service worker is installing before the browser consumes the api?
You can add as many items to as many caches as you'd like inside of your install handler.
Workbox can use RegExps for routing incoming fetch requests to an appropriate response handler, and I assume that's what you're referring to here. The answer is no, you can't just provide a RegExp if you want to cache URLs in advance—you need to provide a complete list of URLs.
Any caching that you perform inside of an install handler is guaranteed to happen before the service worker activates, and therefore before your fetch handlers start intercepting requests. So yes, this is a way of ensuring that your caches are pre-populated.
A modification of your code could look like:
self.addEventListener('install', (event) => {
const cacheURLs = async () => {
const cache1 = await caches.open('my-first-cache');
await cache1.addAll([
'/url1',
'/url2',
]);
const cache2 = await caches.open('my-second-cache');
await cache2.addAll([
'/url3',
'/url4',
]);
};
event.waitUntil(cacheURLs());
});

Service worker installs via localhost, but fails when deployed to GitHub pages

I am working on a PWA and I have installed and activated a service worker on my site. It works perfectly well while testing on local server but when I ship my code live, it fails.
This is my SW:
const cacheName = 'v1';
const cacheFiles = [
'/',
'/css/styles.css',
'/images/test1.png',
'/images/test2.png',
'/js/app.js',
'/js/sw-registration.js'
]
// Install event
self.addEventListener('install', function(event) {
console.log("SW installed");
event.waitUntil(
caches.open(cacheName)
.then(function(cache){
console.log('SW caching cachefiles');
return cache.addAll(cacheFiles);
})
)
});
// Activate event
self.addEventListener('activate', function(event) {
console.log("SW activated");
event.waitUntil(
caches.keys()
.then(function(cacheNames){
return Promise.all(cacheNames.map(function(thisCacheName){
if(thisCacheName !== cacheName){
console.log('SW Removing cached files from', thisCacheName);
return caches.delete(thisCacheName);
}
}))
})
)
});
// Fetch event
self.addEventListener('fetch', function(event) {
console.log("SW fetching", event.request.url);
event.respondWith(
caches.match(event.request)
.then(function(response){
console.log('Fetching new files');
return response || fetch(event.request);
})
);
});
This is the error I'm getting:
Uncaught (in promise) TypeError: Request failed (sw.js:1)
I don't understand why it fails to cache my files online (github pages) when it works locally. Can someone help me understand?
Thank you.
EDIT: I tried to deploy the site via Netlify and it works there. So it has to be something to do with Github pages. I would still like to know what it is, if anyone can shed any light.
As mentioned in Service Worker caches locally but fails online, when deploying to gh-pages, your web app's content will normally be accessed from a subpath, not in the top-level path, for the domain.
For instance, if your files are in the gh-pages branch of https://github.com/<user>/<repo>, then your web content can be accessed from https://<user>.github.io/<repo>/.
All of the URLs in your cacheFiles array are prefixed with /, which isn't what you want, given that all of your content is accessible under /<repo>/. For instance, / is interpreted as https://<user>.github.io/, which is different from https://<user>.github.io/<repo>/.
The solution to your problem, which will lead to a configuration that works regardless of what the base URL is for your hosting environment, is to prepend each of your URLs with ./ rather than /. For instance:
const cacheFiles = [
'./',
'./css/styles.css',
// etc.
];
The ./ means that the URL is relative, with the location of the service worker file used as the base. Your service worker file will be deployed under https://<user>.github.io/<repo>/, so that will end up being the correct base URL to use for the rest of your content as well.

Setting service worker to exclude certain urls only

I built an app using create react which by default includes a service worker. I want the app to be run anytime someone enters the given url except when they go to /blog/, which is serving a set of static content. I use react router in the app to catch different urls.
I have nginx setup to serve /blog/ and it works fine if someone visits /blog/ without visiting the react app first. However because the service worker has a scope of ./, anytime someone visits any url other than /blog/, the app loads the service worker. From that point on, the service worker bypasses a connection to the server and /blog/ loads the react app instead of the static contents.
Is there a way to have the service worker load on all urls except /blog/?
So, considering, you have not posted any code relevant to the service worker, you might consider adding a simple if conditional inside the code block for fetch
This code block should already be there inside your service worker.Just add the conditionals
self.addEventListener( 'fetch', function ( event ) {
if ( event.request.url.match( '^.*(\/blog\/).*$' ) ) {
return false;
}
// OR
if ( event.request.url.indexOf( '/blog/' ) !== -1 ) {
return false;
}
// **** rest of your service worker code ****
note you can either use the regex or the prototype method indexOf.
per your whim.
the above would direct your service worker, to just do nothing when the url matches /blog/
Another way to blacklist URLs, i.e., exclude them from being served from cache, when you're using Workbox can be achieved with workbox.routing.registerNavigationRoute:
workbox.routing.registerNavigationRoute("/index.html", {
blacklist: [/^\/api/,/^\/admin/],
});
The example above demonstrates this for a SPA where all routes are cached and mapped into index.html except for any URL starting with /api or /admin.
here's whats working for us in the latest CRA version:
// serviceWorker.js
window.addEventListener('load', () => {
if (isAdminRoute()) {
console.info('unregistering service worker for admin route')
unregister()
console.info('reloading')
window.location.reload()
return false
}
we exclude all routes under /admin from the server worker, since we are using a different app for our admin area. you can change it of course for anything you like, here's our function in the bottom of the file:
function isAdminRoute() {
return window.location.pathname.startsWith('/admin')
}
Here's how you do it in 2021:
import {NavigationRoute, registerRoute} from 'workbox-routing';
const navigationRoute = new NavigationRoute(handler, {
allowlist: [
new RegExp('/blog/'),
],
denylist: [
new RegExp('/blog/restricted/'),
],
});
registerRoute(navigationRoute);
How to Register a Navigation Route
If you are using or willing to use customize-cra, the solution is quite straight-forward.
Put this in your config-overrides.js:
const { adjustWorkbox, override } = require("customize-cra");
module.exports = override(
adjustWorkbox(wb =>
Object.assign(wb, {
navigateFallbackWhitelist: [
...(wb.navigateFallbackWhitelist || []),
/^\/blog(\/.*)?/,
],
})
)
);
Note that in the newest workbox documentation, the option is called navigateFallbackAllowlist instead of navigateFallbackWhitelist. So, depending on the version of CRA/workbox you use, you might need to change the option name.
The regexp /^/blog(/.*)?/ matches /blog, /blog/, /blog/abc123 etc.
Try using the sw-precache library to overwrite the current service-worker.js file that is running the cache strategy. The most important part is setting up the config file (i will paste the one I used with create-react-app below).
Install yarn sw-precache
Create and specify the config file which indicates which URLs to not cache
modify the build script command to make sure sw-precache runs and overwrites the default service-worker.js file in the build output directory
I named my config file sw-precache-config.js is and specified it in build script command in package.json. Contents of the file are below. The part to pay particular attention to is the runtimeCaching key/option.
"build": "NODE_ENV=development react-scripts build && sw-precache --config=sw-precache-config.js"
CONFIG FILE: sw-precache-config.js
module.exports = {
staticFileGlobs: [
'build/*.html',
'build/manifest.json',
'build/static/**/!(*map*)',
],
staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/],
swFilePath: './build/service-worker.js',
stripPrefix: 'build/',
runtimeCaching: [
{
urlPattern: /dont_cache_me1/,
handler: 'networkOnly'
}, {
urlPattern: /dont_cache_me2/,
handler: 'networkOnly'
}
]
}
Update (new working solution)
In the last major release of Create React App (version 4.x.x), you can easily implement your custom worker-service.js without bleeding. customize worker-service
Starting with Create React App 4, you have full control over customizing the logic in this service worker, by creating your own src/service-worker.js file, or customizing the one added by the cra-template-pwa (or cra-template-pwa-typescript) template. You can use additional modules from the Workbox project, add in a push notification library, or remove some of the default caching logic.
You have to upgrade your react script to version 4 if you are currently using older versions.
Working solution for CRA v4
Add the following code to the file service-worker.js inside the anonymous function in registerRoute-method.
// If this is a backend URL, skip
if (url.pathname.startsWith("/backend")) {
return false;
}
To simplify things, we can add an array list of items to exclude, and add a search into the fetch event listener.
Include and Exclude methods below for completeness.
var offlineInclude = [
'', // index.html
'sitecss.css',
'js/sitejs.js'
];
var offlineExclude = [
'/networkimages/bigimg.png', //exclude a file
'/networkimages/smallimg.png',
'/admin/' //exclude a directory
];
self.addEventListener("install", function(event) {
console.log('WORKER: install event in progress.');
event.waitUntil(
caches
.open(version + 'fundamentals')
.then(function(cache) {
return cache.addAll(offlineInclude);
})
.then(function() {
console.log('WORKER: install completed');
})
);
});
self.addEventListener("fetch", function(event) {
console.log('WORKER: fetch event in progress.');
if (event.request.method !== 'GET') {
console.log('WORKER: fetch event ignored.', event.request.method, event.request.url);
return;
}
for (let i = 0; i < offlineExclude.length; i++)
{
if (event.request.url.indexOf(offlineExclude[i]) !== -1)
{
console.log('WORKER: fetch event ignored. URL in exclude list.', event.request.url);
return false;
}
}

How to queue post request using workbox?

Using following js in my service worker from workboxjs sample for my testing:
importScripts('https://unpkg.com/workbox-sw#0.0.2/build/importScripts/workbox-sw.dev.v0.0.2.js');
I want to try out how to queue the post requests in offline mode using workbox-sw, so once the network is available it process the request from the queue!
Q 1:
I think I need to import additional libraries to define my routes for post methods as shown here on github issue #634
How can use import on browser? I tried using importScripts but it doesn't work.
import * as worker from 'workbox-sw';
import 'workbox-routing';
Q 2:
Do I need any additional libraries for background sync, so post methods are queued?
I'd recommend using it as part of the larger workbox-sw framework, as a plugin. Here's a version of the sample code, modified to use importScripts() to pull in the Workbox code from a CDN. Alternatively, rather than using the pre-packaged bundles over a CDN, you could use the ES2015 module syntax and then a bundler like Rollup or Webpack to include the relevant code from your local node_modules into the final service worker file.
importScripts('https://unpkg.com/workbox-runtime-caching#1.3.0');
importScripts('https://unpkg.com/workbox-routing#1.3.0');
importScripts('https://unpkg.com/workbox-background-sync#1.3.0');
let bgQueue = new workbox.backgroundSync.QueuePlugin({
callbacks: {
replayDidSucceed: async(hash, res) => {
self.registration.showNotification('Background sync demo', {
body: 'Product has been purchased.',
icon: '/images/shop-icon-384.png',
});
},
replayDidFail: (hash) => {},
requestWillEnqueue: (reqData) => {},
requestWillDequeue: (reqData) => {},
},
});
const requestWrapper = new workbox.runtimeCaching.RequestWrapper({
plugins: [bgQueue],
});
const route = new workbox.routing.RegExpRoute({
regExp: new RegExp('^https://jsonplaceholder.typicode.com'),
handler: new workbox.runtimeCaching.NetworkOnly({requestWrapper}),
});
const router = new workbox.routing.Router();
router.registerRoute({route});

Accessing webContents from within preload script in BrowserWindow

I want to create a "Worker" BrowserWindow from within a render process. The worker process will do some long running calculations and periodically report back.
Instead of creating the worker BrowserWindow from within the main process directly, it looks like I can use the remote module to do so from within the renderer process:
import remote from 'electron'
const workerWindow = new remote.BrowserWindow({
show: false,
webPreferences: {
preload: path.join(__dirname, 'preloadWorker.js')
}
});
workerWindow.loadURL('file://' + __dirname + '/worker.html');
I figured I can use the webContents object of the workerWindow to listen for updates like this:
workerWindow.webContents.on('worker-result', (event, msg) => console.log(msg));
But what I can't figure out how to do is emit events from the 'preloadWorker.js' file. I want to do something this in the preloadWorker.js,
setInterval(() =>{
this.webContents.send('worker-result', 'hello from worker');
}, 1000);
Now I know I could do all of this by sending messages using ipcMain and ipcRemote, but I'm trying to save myself some work...it seems like I should be able to access the webContents from within the preload.js script somehow.
This is a really old question, and you might not need it anymore. However, you can use ipcRenderer.sendToHost to communicate with the webview that your preload script is bound to.
const { ipcRenderer } = require('electron');
setInterval(() => {
ipcRenderer.sendToHost('worker-result', 'hello from worker');
}, 1000);

Resources