Service Workers... OMG

Posted: 14 November, 2018 Category: code Tagged: service workersworkboxPWA

THIS. TOOK. MY. WHOLE. DAY.

The Why

Why? Because PWA (Progressive Web Apps), that's why. In my case, I wanted to persist structured data locally, in-browser. That was the rabbit-hole into PWA-world that I personally fell into. Your journey might be a tad different.

The What

Service Workers. Javascript processes that don't block the main thread. Because javascript is single-threaded... and the main thread is way more blockable than you realize. Even advertised-as-async functions aren't... well, aren't 100% asynchronous.

The Gist of Things

It appears that:

  • DOM, XHR, Cookies etc are not available to Service Workers (rem the whole point is that you DON'T want to interrupt the regular browser workload that renders your pages and handles user interactions, animations etc). However:
  • A Cache API, Push API, Notifications API, Fetch API etc are available. That's a pretty neat tradeoff (full list here).
  • All this has to run on https (localhost being an obvious exception, eg during development) as otherwise service workers could expose the guts of your app and all its levers of control to the world.
  • Once wired into your app, service workers install on first visit, and can do nothing until either:

    • a page reload happens
    • a visit to another page/route happens thereafter. Similarly:
  • Updated services workers (browser detects changes) generally have to wait for all instances of earlier versions to die. i.e. a complete KILL of all existing browser tabs using the existing service worker code (not just tab refresh). You could also go into the developer console, and kill directly. The activate method is a lifecycle hook that's a good place to do cleanup after any such earlier incarnations of your worker.

Implementation

More-or-less obligatory diversions

  • Talking directly to the service workers api is kinda gnarly, so a few libs have popped up to abstract you a bit from the gory details. I went with workbox. It actually is available via CDN so you don't need to install it (unless you want to). However you will likely end up installing whatever abstractions/plugins have been designed for it, based on your programming environment. (Are you keeping track? Thats 2 layers of abstraction already, around native service workers).
  • To make your app a true PWA, it also needs a manifest file: a json structure with name, icons (their urls and sizes etc), the start_url, background-color etc. It's usually called site.webmanifest, manifest.json etc and generally lives at the root of your site for max discoverability. If you go to one of those favicon generator sites, they might build one for you too along with your icon pack. note: *The newer CLI tools for frontend dev can stub these out for you., as well as the main service worker file (with workbox under the hood), when you're creating your app the first time*. If you missed that boat (as I did with a vuejs project), then read the next section.

Retro-fitting Service Workers to a pre-existing Vuejs app.

The operative word here is retro-fitting.

(OK OK so the easy way wouldve been just to kick off a new CLI v3 app, and then copy /src files over. But I think you'd agree that wouldn't be sufficiently self-flagellatory. Also, I'm new to service workers, so I wanted to know how they worked in a bit more detail).

Should be similar for other frameworks eg react. For vanilla JS, I guess you don't need the webpack-related stuff. In Summary:

  1. Globally install the workbox-cli yourself, e.g. npm install -g workbox-cli

  2. Install the plugin for PWA manifests: npm install @vue/cli-plugin-pwa --save-dev

  3. Install the webpack plugin for configuring workbox (i.e. for hooking workbox-ified service worker code into the webpack build pipeline): npm install workbox-webpack-plugin --save-dev

  4. Do npm install register-service-worker (used in step 8). Light wrapper for native Service Worker registration calls. (Are you keeping track? Thats 3 layers of abstraction now).

  5. Tip: not a bad idea to update your cli tools!! I had outdated settings in my old vue app, so did:

    npm i -g @vue/cli@latest
    npm i @vue/cli-service@latest

    do whatever upgrades make sense in your environment, or skip this step.

  6. create your top-level service worker .js file that defines what your service worker does (e.g. src/sw.js).

    • To get the entire pipeline in place, you are strongly encouraged to have a fairly useless service worker file that does nothing much beyond a "console.log".
    • Once you've convinced yourself the whole thing is working end to end, you can come back to this step and use the workbox cli to build a more tailored, realistic worker.
  7. Introduce a top-level (project-level) config file for your framework, if one does not already exist. Ex. with vuejs, that'd be vue.config.js. In this file, we want to tweak webpack builds in order to a) generate the manifest.json file for our PWA, and b) incorporate the new serviceworker(s) code into our webpack build pipeline. Note: if you're rolling your own webpack config, due to workbox bug #1513 open at the time of writing, you'd need to build the service worker .js file, then pass the built file to the rest of your regular webpack build.

    • Sample:

      module.exports = {
      pwa: { 
        name: 'My App',
        // ... other stuff like icons, app colors etc for the manifest
        // ...
      
        workboxPluginMode: 'InjectManifest',
        workboxOptions: {
          swSrc: 'src/sw.js'
        }
      }
      }
    • Tip: there's actually also a swDest option, but in 'InjectManifest' mode, if you don't specify it, the output file is named according to the filename in 'swSrc'

  8. Introduce /src/registerServiceWorker.js into your codebase if it does not already exist. This is where your SW registration code must go: the following is the default spat out by the v3 vue-cli build tools... YOU MUST MODIFY THIS if you're doing a retrofit (in particular, if you used the InjectManifest plugin mode, the destination service worker file will be named after the swSrc filename. In my examples thus far, your output service worker file would therefore be named "sw.js", NOT the default "service-worker.js" shown here):

    /* eslint-disable no-console */
    import { register } from 'register-service-worker'
    
    if (process.env.NODE_ENV === 'production') {
      register(`${process.env.BASE_URL}service-worker.js`, {
        ready () {
          console.log(
            'App is being served from cache by a service worker.\n' +
            'For more details, visit https://goo.gl/AFskqB'
          )
        },
        cached () {
          console.log('Content has been cached for offline use.')
        },
        updated () {
          console.log('New content is available; please refresh.')
        },
        offline () {
          console.log('No internet connection found. App is running in offline mode.')
        },
        error (error) {
          console.error('Error during service worker registration:', error)
        }
      })
    }
    /* eslint-enable no-console */
  9. Build the production version of your site, and serve it, in order to see your cutesy little log message from step 6. Why? Because you're probably using hot module reloading in dev, and the cacheing facilities would clash. So for example with the vue-cli, they don't let you build the service worker file in dev. It will never be spat out. Ever. Period. So you need to:

    • do a production build, and then
    • you need to install or handroll a tiny lil httpserver or some such and add an entry into your package.json "scripts" block to kick it off. eg:
    npm run build
    npm start #kick off an httpserver, pointed at /dist or whatever, to see your sw in action

    Note: in a vanilla JS environment all you'd have had to do after defining your service worker and registering it properly would have been to restart / reload your app... none of this production build shenanigans...


Resources: