Announcement: There is “Part 2: practice” of this article is available.
As a “UIs for the future” engineer I love to experiment with the newest versions of frameworks, libraries, APIs — everything connected with web front-end development. Angular and Progressive Web Apps separately work perfectly for me in that sense, because of constant development, updates, changes (sometimes the breaking ones), but their combination is a just explosive mixture.
Let’s explore the direction which Angular team took in their movement to progressive web apps. Recently released Angular 5 Release Candidate 0 introduces a new Angular Service Worker (NGSW), and this is our main topic.
As it stated in the corresponding pull request, a new service worker is a “conceptual derivative of the existing” one. And this is true. The idea is very similar: we power up our application with a service worker by only providing some JSON configuration instead of writing the code manually. But the implementation, as well as configuration settings, are different. For those who are familiar with NGSW beta 16, the key differences are:
- No integration with Angular CLI yet, but the own mini-CLI included
- Settings file for the service worker itself and the one we create are separated more explicitly now. Actually, we write our own JSON file with any name (we will call it service worker configuration file) and build the one for service worker named ngsw.json using the CLI I mentioned above (we will call it service worker control file). I find this update very useful: in the current version there is constant confusion with ngsw-manifest.json VS web app manifest, also the idea of merging of auto-generated and manually written manifests is not so obvious.
- There is no plugins concept. At the moment it’s not clear how to extend the functionality.
The key difference with other service worker generators (like Workbox, sw-precache) is the fact, that you do not re-generate the service worker file itself, you only update its control file.
Disclaimer: this is just a first release of Angular Service Worker. Not everything works correctly there, and it’s not very developer friendly yet. In addition, the details of implementation, as well as public APIs, could be changed in the next releases.
For our experiments, let’s take my PWA guinea pig app — PWAtter. It’s the simplest Angular 5 RC0 app powered by trivial NodeJS backend. PWAtter can load tweet feeds and subscribe to push notifications. The code is available on GitHub: https://github.com/webmaxru/pwatter/tree/ngsw/
Angular Service Worker is not integrated with Angular CLI yet, so you will not see the service-worker package in node_modules/@angular after scaffolding a new app, let’s install the latest version explicitly:
npm install [@angular/service-worker](http://twitter.com/angular/service-worker)@next --save
What we need from the installed package:
- @angular/service-worker/ngsw-worker.js — the service worker itself. The only non-minimized version is included at the moment. We have to copy this file to our dist folder and register as a service worker.
- .bin/ngsw-config — NGSW Command-line interface
- ServiceWorkerModule exposed by @angular/service-worker — for using within Angular client apps to register and communicate with the service worker.
In our app:
We register a service worker using any of at least 3 options:
- adding registration script to index.html
- using the same code in main.ts after bootstrapModule()
- going “NGSW”-way and using register() method of ServiceWorkerModule, let’s go for this option:
In our build process:
- Build a production version of our app — development build will not work correctly with NGSW
- Copy ngsw-worker.js to the dist folder
- After all, generate ngsw.json — a control file for Angular Service Worker (successor of ngsw-manifest.json) using NGSW CLI
Angular Service Worker Command-line interface is a simple utility, taking a configuration file written by developers, and converting it to ngsw.json — control file to be used by NGSW.
**ngsw-config** outputFolder sourceConfigurationFile baseHref
- outputFolder — where to copy the resulting ngsw.json
- sourceConfigurationFile — configuration file we want to process. Let’s have it in ./src/ngsw-config.json
- baseHref — the value we use in meta tag of index.html. It’s “/” by default and can be skipped
So the command to generate ngsw.json and to put it to the dist folder will be:
node\_modules/.bin/ngsw-config dist ./src/ngsw-config.json
(if we have our app located in the root folder)
Based on the flow we’ve just described, we can add the following command to the scripts section of packages.json to get a full build, including all the operations with the service worker:
“build-prod-ngsw”: “ng build -prod && node\_modules/.bin/ngsw-config dist ./src/ngsw-config.json && cp node\_modules/@angular/service-worker/ngsw-worker.js ./dist/ngsw-worker.js”
Now we have to explore the syntax of ngsw-config.json — the configuration file for Angular Service Worker.
The interface file from NGSW source code looks like this:
- appData —any application metadata for this specific version. For example build hash, package.json version, release date.
From NGSW configuration design doc:
The appData property allows the application to attach metadata to the configuration, which is not used by the SW but may be delivered as part of update notifications or allow the application to make more intelligent decisions about when to update. For example, a particular update could be marked as containing a security fix and could be applied as soon as possible, but a small bugfix update may be applied on the next reload, without bothering the user with a notification.
- index — path to the index.html file (where to redirect all the navigation requests)
- assetGroups — named groups of the explicitly known at build time resources to be cached. The most natural application of this setting is specifying the application shell resources.
- dataGroups — named groups of the resources to be cached during runtime, “on demand” when we have to apply one of the caching strategies. The best example here is API calls.
We specify here:
Name of the group. This will be a part of Cache API storage name
Determines when the resources in the group are fetched and cached. There are 2 possible options:
- prefetch — All resources are downloaded when the service worker is setting up caching for this app version. This mode should be used for all the assets required for bootstrapping the application (application shell) if your application aims to be capable of full offline mode.
- lazy — Each resource is cached individually when it’s requested.
Determines how each cached asset behaves when a new version of the application is downloaded. It has same 2 options:
- prefetch — Refresh the asset (if needed) on every new version. For files with hashes (versionedFiles), the asset is downloaded only if the hash has changed. For URLs in the cache, they will be refreshed always (possibly with an If-Modified-Since request)
- lazy — The above flow will be performed only when the resource was requested
The explicit list of resources to cache. There are 3 ways to set them up:
- files —A list of globs matched against files in the configured distribution directory. These files will have their contents hashed and the hashes will be included in the resulting ngsw.json file’s hashTable node, for accurate versioning. The file paths will be mapped into the URL space of the application, starting with the base href.
- versionedFiles — The same, but for the files, which already include a hash in their name. In case of default production Angular 5 app build, these are html, js, css files. There is no need to calculate the hash, because these files have different names if changed, so the service worker flow could be simplified.
- urls — A list of external URLs (either relative, absolute paths, or on different origins) that should be cached. These are often URLs to CDNs or other external resources, for example, the URLs of Google Font API .woff2 files. URLs cannot be hashed, so they are updated whenever the configuration file changes. If these are resources which aren’t precisely known but which still belong in the set of cached assets, we can use globs here. Please note: using the 3rd party, external URLs is not the best practice for the app shell implementing. It’s much better to have full control on pre-cached resources.
We specify here:
Name of the group. This will be a part of Cache API storage name
The same as in assetGroups — A list of glob patterns which match the URLs of requests.
Settings for the defining of caching strategy and fine-tuning this process:
- maxSize — maximum number of responses cached per group
- maxAge — to specify how long the cached response is valid. Could be set as a number of seconds, minutes, hours or days. Like 30m, 2h, 1d.
- timeout — valid for Freshness strategy (see below). The response waiting time after which there will be a fallback to the cache. Set in the same unit as maxAge.
- strategy — two options: “freshness” VS “performance”. Basically, Network-First VS Cache-First caching strategies.
Now we are ready to create our ngsw-config.json:
Out of curiosity, let’s check how the control file will look like after we run our full build command
npm run build-prod-ngsw
Go to the dist and open ngsw.json:
Despite it’s not for us, but for Angular Service Worker, it’s still pretty readable, which is good for us, developers.
After all, we are ready to serve our app using any static web server or deploy it, and check how our service worker actually works. We’ll go for it in the next article about Angular Service Worker.
- Issues of the new Service Worker submitted by me: unstable app shell, freshness strategy doesn’t work, error during push notification. Be aware of these issues while experimenting with NGSW.
- Angular Service Worker Configuration — public draft. Outdated, but contains explanations about some decisions
- NGSW beta.16 unofficial documentation — a full guide on the current version
I would like to express my gratitude and appreciation to Alex Rickabaugh from the Angular team for a great job on implementing Angular Service Worker and a lot of time spent on the answering my questions and reviewing my code.