Web Apps & service workers:
    nearly a panacea to install apps on ALL platforms

In brief

Web Apps, webApps or “progressive web apps” utilise an HTML5 technology called “service worker” to instantly install, and then serve the application off-line. Thus the usual download + installation steps occur nearly automatically. Fantastic! →Now used in FrACT₁₀.

Details

My Vision Test Battery FrACT₁₀, after porting to JavaScript (using the fantastic Cappuccino framework), runs on all platforms in any modern browser. For off-line situations, however, you need a downloadable application. For this I’ve used Electron and blogged about it here. That yields apps for MacOS (both ARM and Intel code) and Windows, fine, but with huge code sizes (hundreds of megabytes).

Then I read about “Web Apps”, “progressive web applications”, and “Service workers”. This “Mozilla devs: Offline Service workers” sounded promising and says

“Service Workers are a virtual proxy between the browser and the network. They finally fix issues that front-end developers have struggled with for years — most notably how to properly cache the assets of a website and make them available when the user’s device is offline.”

So for my situation I don’t need the service worker to do major work – just work once, namely fetching & caching all code once; then keep quiet, looking for updates in the background. The app itself is quite small (1–2 MB) and starts the corresponding browser for execution. No Internet access is required for the webApp to run. However, if the pertinent browser cache – specifically what Chrome calls “Hosted app data” – is cleared, Internet connection is required for the cache to reload.

Well, knowing me, I knew it would take a while… a week. So here is how I implemented FrACT₁₀ as a “Web App” and look forward to all advice where to improve. The working result is →here.

1. Write a web manifest file. In full boring defining glory here. Mine looks (looked) like this:

Content of file “webApp.webmanifest”
{
  "short_name": "FrACT10 webApp",
  "name": "Freiburg Vision Tests webApp",
  "description": "Vision Test Battery (acuities, contrast, …)",
  "icons": [
    {
      "src": "icons/FrACT_icon-390.png",
      "sizes": "390x390",
      "type": "image/png",
      "purpose": "maskable"
    },
    {
      "src": "icons/FrACT_icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "start_url": "index.html",
  "id": "/fract/FrACT10/FrACT/index.html",
  "theme_color": "#33B",
  "background_color": "#777",
  "display": "standalone",
  "orientation": "landscape"
}

In my main index.html the manifest needs to be referenced like so (it sits “one down” in Resources)
    <link rel="manifest" href="Resources/webApp.webmanifest">
somewhere in the header section.

You see icons referenced in the manifest file. At least one is necessary, and sufficient; add it where you please and reference appropriately. The purpose “maskable” was added to make Lighthouse happy.

2. Write a service worker. Mine looks like this:

Content of file “webAppServiceWorker.js”
/* file "webAppServiceWorker.js" */
const cacheName = 'FrACT10-sw-v5';

/* Fetching content using Service Worker */
self.addEventListener('fetch', (e) => {
  /*console.info("[Service Worker] responding to fetch event…");*/
  e.respondWith((async () => {
    const r = await caches.match(e.request);
    /*console.info(`[Service Worker] Fetching resource: ${e.request.url}`);*/
    if (r) return r;
    const response = await fetch(e.request);
    const cache = await caches.open(cacheName);
    /*console.info(`[Service Worker] Caching new resource: ${e.request.url}`);*/
    cache.put(e.request, response.clone());
    return response;
  })());
});

/* Installing Service Worker */
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(cacheName).then((cache) => {
      return cache.addAll([ /* Cache all these files */
		'index.html',
		'Info.plist',
		'Browser.environment/capp.sj',
		'Browser.environment/dataURLs.txt',
		'Browser.environment/MHTMLData.txt',
		'Browser.environment/MHTMLPaths.txt',
		'Browser.environment/MHTMLTest.txt',
		'Resources/allRewards4800x200.png',
		'Resources/CreditcardPlus2x50.png',
		'Resources/MainMenu.cib',
		'Resources/buttons/butCntC.png',
		'Resources/buttons/butCntE.png',
		'Resources/buttons/butCntLett.png',
		'Resources/buttons/buttonAcuityC.png',
		'Resources/buttons/buttonAcuityE.png',
		'Resources/buttons/buttonAcuityLett.png',
		'Resources/buttons/buttonAcuityLineByLine.png',
		'Resources/buttons/buttonAcuityTAO.png',
		'Resources/buttons/buttonAcuityVernier.png',
		'Resources/buttons/iconAbout.png',
		'Resources/buttons/iconFullscreen.png',
		'Resources/buttons/iconHelp.png',
		'Resources/buttons/iconSettings.png'
      ]);
    })
  );
});
/*  to be done… (really?)
dither, icons, keyMaps, optotypeEs, sounds, TAOs
*/

self.addEventListener('activate', function(event) {
  var cacheWhitelist = [cacheName];
  event.waitUntil(
    caches.keys().then(function(keyList) {
      return Promise.all(keyList.map(function(key) {
        if (cacheWhitelist.indexOf(key) === -1) {
          return caches.delete(key);
        }
      }));
    })
  )
});

In my main index.html this code is called at the very end of body like so

   <script defer> // try to activate the service worker
	 try {
	   if ("serviceWorker" in navigator) {
		 navigator.serviceWorker.register("webAppServiceWorker.js").then((registration) => {
			 /*console.info('Service worker registration succeeded:', registration);*/
		   }, /*catch*/ (error) => {
			 console.error(`Service worker registration failed: ${error}`);
		   });
	   }
	 } catch(error) { }
   </script>

This file has to sit at top level, otherwise scoping problems arise. It’s scope parameter does not help, and on StackOverflow I read that’s a known problem. The code checks "serviceWorker" in navigator before proceeding to make sure the browser is up to snuff. I placed this in a try block to avoid a console error when running the non-webApp version of FrACT₁₀ (but it doesn’t help). The “defer” parameter possibly irrelevant?

3. Testing, inspecting, debugging

My usual browser is Safari. However, in this respect it’s lagging behind, but Chrome works very nicely here. Going to the Developer Tools panes, there is one called Application. There one can inspect whether the manifest and the icon(s) loaded correctly. The Storage section is particularly important for its Clear all site data button. In the Storage>Cache Storage section one can see the named cache (as set in cacheName in my service worker).

The ‘Service worker registration succeeded:’ line is “commented out” now in the main index.html, but it proved very useful during development.

My most frequent issues were with scoping. One example, to reference the icons in the manifest: they are manifest-file-relative; so when I moved the manifest file to Resources, the icons were no longer found…

5. Installing

This is very browser dependent and described in detail →here.

4. Updating

In principle, to update the App one just needs to rename the cacheName constant in the service worker – any change will be noticed by the activate event in the service worker. Yes, but other files like index.html and the manifest need cache clearing – a clear PITA. I had to reduce the expiry times in my .htaccess file.

5. Conclusion

I find it fantastic that the web app approach allows to install one and the same appliction on Android, iOS, Linux, MacOS & Windows! While the actual install moves are still a little obnoxious, they are markedly easier than circumventing the irksome obstacles an app faces that is not from a certified developer. Furthermore, the size of FrACT₁₀ as a Web App is only a few megabytes, a saving by a factor of >10! The working result is →here.

Congrats to anyone who made it thus far :). Any suggestions, advice, corrections, whathaveyou, welcome!