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.
Related
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.
Im working on my PWA application.
So I have one problem that I can't find any info how to fix.
I use workbox with webpack InjectManifest ( but also tried webpack offline-plugin ).
When I access my webpage at the root and go offline, I can see it's working perfectly. But when I change route to '/authorize' or basically any other route and go offline, it doesn't work.
Is there any requirement that it will work only in case that we are on root path? I can't find anything about it except for this: https://github.com/quasarframework/quasar-cli/issues/131
Ok found it.
So basically it all comes to routing.
https://developers.google.com/web/tools/workbox/modules/workbox-routing#how_to_register_a_navigation_route
https://developers.google.com/web/tools/workbox/modules/workbox-strategies
In my case, I wanted to always serve content as for SPA so I had to add
workbox.routing.registerNavigationRoute('/index.html'); to my workbox config.
At the end it looks like this:
1) Webpack plugin:
const commonPlugins = [
new workboxPlugin.InjectManifest({
swSrc: './src/workbox-sw.js',
swDest: 'workbox-sw.js',
}),
];
2) Content of workbox-sw
/* globals workbox, self */
workbox.setConfig({
debug: true
});
workbox.core.setCacheNameDetails({
prefix: 'sneak-client',
suffix: 'v1',
precache: 'sneak-precache',
runtime: 'sneak-runtime-cache',
});
workbox.routing.registerNavigationRoute('/index.html');
workbox.precaching.precacheAndRoute(self.__precacheManifest);
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.
I am trying to add PWA functionality to an existing website that is hosted on Azure and uses Cloudflare CDN.
I have run the lighthouse testing tool on the site and it passes everything in the PWA section (e.g. service worker installed, served over https, manifest installed etc.) except:
"Service worker does not successfully serve the manifest's start_url."
My manifest.json has '/' as the start URL and "/" as the scope.
The '/' is actually default.aspx which I have cached as well.
My service worker caches '/', e.g.
var cacheShellFiles = [
'/',
'/manifest.json',
'/index.html',
'/scripts/app.js',
'/styles/inline.css'
...
]
// install - cache the app shell
self.addEventListener('install', function (event) {
console.log('From SW install: ', event);
// calling skipWatiing() means the sw will skip the waiting state and immediately
// activate even if other tabs open that use the previous sw
self.skipWaiting();
event.waitUntil(
caches.open(CACHE_NAME_SHELL)
.then(function (cache) {
console.log('Cache opened');
return cache.addAll(cacheShellFiles);
})
);
});
When I view the Cache Storage files in dev tools however, the Content-Length of the / and the .css and .js files is 0:
Image of Chrome Developer tools showing cache storage with Content-Length=0
Is the Content-Length = 0 the reason that it is saying it can't serve the manifest's start URL ?
This is an issue with your service worker's scope (different from the scope option in manifest.json).
Your start_url is set to /, but most likely your service worker file is served from a deeper path, e.g. /some-path/service-worker.js. In this case, your service worker's scope is /some-path/, therefore it will not be able to handle requests to paths outside of it, such as the root path /.
To fix this, you need to make sure that your service worker's scope covers your start_url. I can think of two ways to do this:
In your case, serve the service worker file directly from the root path, e.g. /service-worker.js.
Use the Service-Worker-Allowed response header, which overrides the service worker's scope, so that it wouldn't matter from which path the service worker file is served from.
Choose the one that is more appropriate to your setup.
I registered a service worker successfully, but then the code
navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
// Do we already have a push message subscription?
....
hangs -- the function is never called. Why?
The problem was that the service-worker.js file was stored in an assets sub-directory.
Don't do that: store the service-worker.js in the root of your app (or higher). That way your app can access the service-worker.
See HTML5Rocks article --
One subtlety with the register method is the location of the service worker file. You'll notice in this case that the service worker file is at the root of the domain. This means that the service worker's scope will be the entire origin. In other words, this service worker will receive fetch events for everything on this domain. If we register the service worker file at /example/sw.js, then the service worker would only see fetch events for pages whose URL starts with /example/ (i.e. /example/page1/, /example/page2/).
Like said in the accepted answer, the problem is, indeed, probably because your service worker JS file is in a different path than your current page.
By default, the scope of the service worker is the path to its JS file. If your JS file is reachable at http://www.example.com/assets/js/service-worker.js, your service worker will only work/"be ready" for URL starting with /assets/js/.
But, you can change the scope of the service worker. First, you need to register if using the scope option:
navigator.serviceWorker.register('http://www.example.com/assets/js/service-worker.js', {
scope: '/',
});
If you do, just this, you will get errors in the Chrome console:
The path of the provided scope ('/') is not under the max scope
allowed ('/assets/js/'). Adjust the scope, move the Service Worker
script, or use the Service-Worker-Allowed HTTP header to allow the
scope.
/admin/#/builder:1 Uncaught (in promise) DOMException: Failed to
register a ServiceWorker for scope ('http://www.example.com/') with
script
('http://www.example.com/assets/js/service-worker.js'): The path of
the provided scope ('/') is not under the max scope allowed
('/assets/js/'). Adjust the scope, move the Service Worker script, or
use the Service-Worker-Allowed HTTP header to allow the scope.
You then need to create an .htacess file at the root of your website with the following content:
<IfModule mod_headers.c>
<Files ~ "service-worker\.js">
Header set Service-Worker-Allowed: /
</Files>
</IfModule>
Had the same issue, but putting service worker and installing script in the same directory didn't solve this. For me the solution was to add a "/" to the end of the url.
So i had:
http://localhost:9105/controller/main.js - installing script
http://localhost:9105/controller/sw.js - service worker
http://localhost:9105/controller/index.html - page
And when the url in the browser was http://localhost:9105/controller service worker have never been ready, but when url is http://localhost:9105/controller/ it works fine.
I used code below to control this
if (!window.location.href.endsWith('/')) {
window.location.assign(window.location.href + '/')
}
Add service-worker.js file to your project root directory
You can download service-worker.js file from Link
And use below code to register service worker.
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('./service-worker.js', { scope: './' })
.then(function (registration) {
console.log("Service Worker Registered");
})
.catch(function (err) {
console.log("Service Worker Failed to Register", err);
})
}