What files should I cache in service-worker install event? - service-worker

I followed instructions from the Google PWA tutorial here to make my own app with offline functionality. When I ran Lighthouse check on my localhost:3000, I got a report that said everything is setup fine.
Note that I had only cached my index file and svg image assets only.
self.addEventListener('install', event => {
event.waitUntil(
caches
.open('word-cloud-v1')
.then(cache => {
return cache.addAll([
'/',
'/index.html',
'./images/paper-plane.svg',
'./images/idea.svg',
'./images/desk-lamp.svg',
'./images/stopwatch.svg',
'./images/pie-chart.svg',
])
})
)
})
But when I go offline and try to run my app, I get errors that some files have not loaded.
So I go back and add some more files to cache. Note that this file is not created by me.
.then(cache => {
return cache.addAll([
'/',
'/index.html',
'./static/js/bundle.js',
'./images/paper-plane.svg',
'./images/idea.svg',
'./images/desk-lamp.svg',
'./images/stopwatch.svg',
'./images/pie-chart.svg',
])
})
Although the offline feature works fine now, I'm also seeing a bunch of other randomly generated files that are created in the build folder that I have not explicitly cached yet. So what are the files I should cache inside a service worker so they show up in offline mode?
TLDR; what files should we cache apart from /, /index.html and images so we can have offline functionality?

You can cache anything that you would like to cache. Cache data can be of two types.
1) Static data - You can cache all your html/css/js/images which are not dynamic. When you plan to cache static data, it would be easy to wild card the files like below instead of choosing individual files. Below block (angular ng service worker. Your helper class might look different) will cache everything inside "assets" folder.
{
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**"
]
}
}
2) Dynamic data - You can cache dynamic data like API JSON response in IndexDB.

Related

How to conditionally cache bust an API in workbox

Details about my goal.
I have workbox webpack plugin configured to cache an API for a duration of 30 seconds. I would like to force cache bust it conditionally when a different API request is triggered.
Example, below config caches feature-flags. I am trying to cache bust it when the page sends a request to "updateTests".
workbox configuration to cache feature-flag
Workbox configuration updated to clear feature-flags
Cache clear makes it work
Things I have tried
Deleting IndexedDB manually does a fresh fetch of feature-flags
Just to make sure I understand:
You have API calls that include the feature-flags in their URL, and you want all of those calls to be served cache-first out of the cache named api, with a 30 seconds max lifetime.
If at any point the browser makes a request for a URL that contains updateFlags, that should serve as kind of a "kill switch" that automatically clears out the contents of the api cache, ensuring that the next feature-flags request will always go against the network.
Assuming that's an accurate summary, you could add a new runtimeCaching route to your configuration that does the following:
runtimeCaching: [{
// existing route
}, {
urlPattern: new RegExp('updateFlags'),
handler: async ({request, event}) => {
// Deleting the 'api' cache will ensure that the next API
// request goes against the network.
event.waitUntil(caches.delete('api'));
// Assuming that you want the actual request for the URL
// containing updateFlags to be made against the server:
return fetch(request);
},
}]

PWA Service Worker (Workbox) setting's '/' stand for?

I want to create PWA's service worker file by using workbox.
According to workbox document, precaching setting of workbox is something like this:
service-worker.js
workbox.precaching.precacheAndRoute([
'/styles/example.ac29.css',
{ url: '/index.html', revision: 'abcd1234' },
// ... other entries ...
]);
But what is the actual meaning of /index.html or /styles/example.ac29.css?
It is server root? or, the root of PWA's scope?
For example, if service-worker.js is served in https://example.com/hoge/fuga/service-worker.js, and manifest.json is also served in https://example.com/hoge/fuga/manifest.json with content:
{
"name": "Great PWA site",
"background_color": "#f6f0d3",
"icons": [...],
"start_url": "https://example.com/hoge/fuga/",
"scope":"/hoge/fuga/",
"display": "standalone"
}
In such case, /index.html in workbox setting means https://example.com/index.html? Or, https://example.com/hoge/fuga/index.html?
Within Workbox's precache manifest, /index.html is resolved to a full URL using the server root as the base. It does not user the service worker scope as the base. (After Googling, I guess it's technically called a "root-relative" URL, though I've never really used that phrase before.)
If you had a relative URL like ./index.html, it would be resolved to a full URL using the location of the service worker script as the base.
In general, if you're curious as to what a URL will resolve to, you can run the following from the ServiceWorkerGlobalScope to see:
(new URL('some-URL.html', self.location.href)).href
The easiest way to do this is to open up Chrome's DevTools while on a page you're curious about that has a service worker, go to the Console panel, and choose the service worker's scope in the popup menu, and then enter the code above.

Workbox redirect the clients page when resource is not cached and offline

Usually whenever I read a blog post about PWA's, the tutorial seems to just precache every single asset. But this seems to go against the app shell pattern a bit, which as I understand is: Cache the bare necessities (only the app shell), and runtime cache as you go. (Please correct me if I understood this incorrectly)
Imagine I have this single page application, it's a simple index.html with a web component: <my-app>. That <my-app> component sets up some routes which looks a little bit like this, I'm using Vaadin router and web components, but I imagine the problem would be the same using React with React Router or something similar.
router.setRoutes([
{
path: '/',
component: 'app-main', // statically loaded
},
{
path: '/posts',
component: 'app-posts',
action: () => { import('./app-posts.js');} // dynamically loaded
},
/* many, many, many more routes */
{
path: '/offline', // redirect here when a resource is not cached and failed to get from network
component: 'app-offline', // also statically loaded
}
]);
My app may have many many routes, and may get very large. I don't want to precache all those resources straight away, but only cache the stuff I absolutely need, so in this case: my index.html, my-app.js, app-main.js, and app-offline.js. I want to cache app-posts.js at runtime, when it's requested.
Setting up runtime caching is simple enough, but my problem arises when my user visits one of the potentially many many routes that is not cached yet (because maybe the user hasn't visited that route before, so the js file may not have loaded/cached yet), and the user has no internet connection.
What I want to happen, in that case (when a route is not cached yet and there is no network), is for the user to be redirected to the /offline route, which is handled by my client side router. I could easily do something like: import('./app-posts.js').catch(() => /* redirect user to /offline */), but I'm wondering if there is a way to achieve this from workbox itself.
So in a nutshell:
When a js file hasn't been cached yet, and the user has no network, and so the request for the file fails: let workbox redirect the page to the /offline route.
Option 1 (not always useful):
As far as I can see and according to this answer, you cannot open a new window or change the URL of the browser from within the service worker. However you can open a new window only if the clients.openWindow() function is called from within the notificationclick event.
Option 2 (hardest):
You could use the WindowClient.navigate method within the activate event of the service worker however is a bit trickier as you still need to check if the file requested exists in the cache or not.
Option 3 (easiest & hackiest):
Otherwise, you could respond with a new Request object to the offline page:
const cacheOnly = new workbox.strategies.CacheOnly();
const networkFirst = new workbox.strategies.NetworkFirst();
workbox.routing.registerRoute(
/\/posts.|\/articles/,
async args => {
const offlineRequest = new Request('/offline.html');
try {
const response = await networkFirst.handle(args);
return response || await cacheOnly.handle({request: offlineRequest});
} catch (error) {
return await cacheOnly.handle({request: offlineRequest})
}
}
);
and then rewrite the URL of the browser in your offline.html file:
<head>
<script>
window.history.replaceState({}, 'You are offline', '/offline');
</script>
</head>
The above logic in Option 3 will respond to the requested URL by using the network first. If the network is not available will fallback to the cache and even if the request is not found in the cache, will fetch the offline.html file instead. Once the offline.html file is parsed, the browser URL will be replaced to /offline.

Item cached in the cache storage under relative paths

I am trying to add some static pages to cache using standard technique in the install event of the service worker:
self.addEventListener('install',function(event) {
event.waitUntil(
caches.open(static_cache).then(function(cache){
return cache.addAll([
'/',
'index.html',
'css/styles.css',
'js/dbhelper.js',
'js/main.js',
'js/restaurant_info.js'
])
})
)
})
but when I look into cached items - I see they're cached under relative paths:
So of course when fetch event comes for the full path - there will be no match in cache. Any idea why this is happening?
I may be mistaken but I think the Dev Tools cache interface just displays the relative paths. But I believe they are cached with the full path.
If you log the cache content you should see the full path.
For example my cache interfaces shows "/", but both of these:
caches.match('https://offline-data-driven-pwa.firebaseapp.com/')
.then(res => console.log(res))
caches.match('/')
.then(res => console.log(res))
log the https://offline-data-driven-pwa.firebaseapp.com/ resource

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;
}
}

Resources