A new Angular Service Worker — creating automatic progressive web apps. Part 2: practice

Announcement: I present about Angular Service Worker this Tuesday, November 7 at AngularConnect conference in London. If you wish to follow me at the live video stream, please mark your calendars: 11:20 am, GMT (right after the keynote), the main stage.

There is some time left since the first part of this article was published, and this pause was taken intentionally — the functionality was ready to practical usage only after a few more Release Candidates since the initial appearance. Now the Angular 5 release is here, and Angular Service Worker (hereafter NGSW) is ready for being explored.

We are going to use our PWAtter — the tiny Twitter client — again. The source code is available on the GitHub (branch ngsw). Also, we need a simple backend to serve tweet feed and handle push notifications. Source code and detailed setup instructions are also available on the GitHub.

Since this is a practical approach, let’s define some requirements for our PWA:

  • Immediate start in both offline and online modes: we need an application shell to achieve this.
  • We have some external files (webfont with material icons) as a part of the application shell. By “external” we mean that these resources are not the part of our dist folder.
  • Optimized networking for API calls (runtime caching): we could serve some earlier accessed data during offline, as well as we could speed up online mode by serving this cached data instead of the network roundtrip.
  • Push notifications: subscription for the notifications and displaying the ones. The client-side app must provide the functionality to initiate the subscription flow and pass subscription object to the backend (we always store our subscriptions at the backend). Plus, our backend will generate a push message each time someone tweet about “javascript”, so Angular Service Worker should get this event and display a message. Let’s also add a couple of extra features. First, let’s give the possibility to our user to unsubscribe from the web push notifications. Second, let’s demo, that we can propagate the notifications to the client-side app, in addition to showing them as notification popups.
  • The application shell should have the regular for PWAs “stale-while-revalidate” update flow: if possible, we always serve the latest cached version (to show it almost immediately). At the same time, service worker checks if there is a newer version of the app shell. If yes, we download and cache this version to use it for the next application run. Also, we might want to ask the user if they want to reload the current tab with the application right now.

Let’s go point by point, but first, we have to make some notes about Angular CLI state.

Angular CLI support of Angular Service Worker (upcoming)

At the moment we have Angular CLI 1.5 as a release. Unfortunately, there is no NGSW support yet, it’s planned for 1.6. But by the exploration of corresponding pull requests (to CLI and to DevKit) we can easily understand how will it look and reproduce the main functionality. Let’s have a look into the nearest future.

New app with the service worker

The command will be following:

ng new myApp --service-worker (or using the alias — -sw )

Having this service worker flag, Angular CLI 1.6 will do some automation for us:

  1. Angular Service Worker package will be installed
  2. Build support for NGSW will be enabled
  3. NGSW will be registered for your application
  4. NGSW configuration file will be created with some smart defaults

Anyway, even after CLI 1.6 will be released, it’s good to know how to reproduce these steps, because we have to perform them manually to add NGSW support to the existing app. Let’s go to add Angular Service Worker to PWAtter.

Adding Angular Service Worker to the existing app

Let’s manually perform the same 4 steps from above:

  1. Install NGSW

npm install @angular/service-worker --save

2. Enable build support (only for Angular CLI 1.6, see the notice below)

ng set apps.0.serviceWorker=true

or manually add/edit this parameter in .angular-cli.json file.

Important! For the moment, when we use Angular CLI 1.5, please make sure that you don’t have this property in .angular-cli.json, it will cause build errors. See how to emulate this step in Angular CLI 1.5 below.

3. Register NGSW in your AppModule. This is how it will look in Angular CLI 1.6:

4. Create NGSW configuration file (default name is src/ngsw-config.json). Here is the default content will be generated by Angular CLI 1.6 (you can find more details on the format of this configuration file in the previous article):

At the moment, while using Angular CLI 1.5 we also have to emulate build support from the step 2. Actually, there are 2 extra actions should perform in addition to ng build --prod command (it’s important to use production build in order to use NGSW!):

2.1 Generate NGSW control (manifest) file ngsw.json based on NGSW configuration file src/ngsw-config.json using NGSW CLI ngsw-config. You can find more details on this small utility in the previous article.

2.2 Copy NGSW itself from the npm_modules package folder to our dist folder.

To have one simple command to generate production build with NGSW support let’s add some npm scripts:

Now if we run npm run build-prod-ngsw we’ll have Angular PWA in the dist folder. Optionally, we could serve it using the simplest http-server by running npm run serve-prod-ngsw.

Important! Do not use ng serve to test your Angular Service Worker. This development server was not designed to work in collaboration with PWA flow. Always build a production version of the app and serve it from your distributive folder using any static web server.

Application shell

If we perform the above actions and run npm run build-prod-ngsw — the Angular PWA in its default form is ready for us! Deploy the application or just run it locally using any static web server (http-server package in my case, you run npm run serve-prod-ngsw to build and serve). You will see the following picture:

PWAtter — tiny Angular 5 app

Now it’s time to check the offline mode! We are going to use Chrome DevTools for this:

Emulating offline mode using Chrome DevTools
Important notice: Chrome v62 and earlier might handle Offline switch incorrectly, allowing service worker to send some requests to the network. To emulate the offline mode which is closer to the real one I recommend to use Chrome Canary (v64 at the moment). Also Chrome Canary has better UI for the Cache Storage, allowing us to see the contents of the stored HTTP response.

The application is working after we went offline. Why? Because NGSW cached all the resources listed in theassetGroups section of the configuration file, and now it’s responsible for serving them from the Cache Storage, which is full of records now:

Service Worker is registered and active
We can view the content of cached response (available only in Chrome Canary at the moment)

NGSW uses Cache Storage to store both HTTP responses data and some metadata to handle versioning:

Types of the storages by NGSW
  • Entries with postfix :cache — actual HTTP responses.
  • Entries with postfix :meta — to store the versioning meta information. Later this kind of stored data might be moved to indexedDB.
TIP: If you keep DevTools open, the entries inside Cache Storage section most likely will not be updated automatically after each action from service worker side. If you wish to see the actual data, right-click and choose Refresh Caches.

But the application is looking weird:

No webfont icons

Right. The default form of NGSW configuration file is not enough for our case because we use Material Icons webfont. Obviously, these resources (corresponding CSS and WOFF2 files) were not cached by NGSW, but we can easily fix it by adding one more group to assetGroups in addition to default app and assets ones. Let’s call it fonts :

It makes sense to specify these resources using globs syntax because the exact URL of the font file could change from time to time to support webfont versioning. Also, you may notice that we have specified neither installMode nor updateMode. On the one hand, both will be set as prefetch in the resulting NGSW control file as this is a default value. On the other hand, they will be cached only after they were requested because the specifics of urls-way to list the resources.

After we rebuild, run and switch to offline mode we will see the normal state of the application with all the icons in the place.

In the Cache Storage we’ll see two new entries:

Storages generated by NGSW

We can even preview the cached font:

Preview of the cached resource

Awesome, we have the full application shell up and running in offline mode. What about the requests our app sends to APIs (data requests)? Obviously, we can’t precache them because we don’t know the exact list of URLs. You may ask: but we can use the same idea with globs as we apply for the external resources caching: assetGroups / resources / urls / { https://our-backend.com/api/** }. Technically this will cache these responses, but this will ruin the update flow of app shell. There is a fundamental difference between assetGroups and dataGroups (from our next chapter) sections of the NGSW configuration file:

  • assetGroups are keeping track of the app [shell] version. If one or more resources from these groups were updated — we consider there is a new version of the app available, corresponding update flow started (we’ll have a look on this soon)
  • dataGroups are independent of the app version. They are cached using their own cache policies, and it’s the proper section to handle our API responses.

Let’s cache our tweet feeds for offline access as well as for optimized online experience.

Runtime caching

I decided to use Network-First strategy for my /timeline API endpoint and Cache-First strategy for the /favorites endpoint. The corresponding setup in src/ngsw-config.json will look like:

There is a main switch defining the behavior of NGSW: cacheConfig / strategy. For network-first strategy, it’s freshness, for cache-first — performance. You can get more details about the rest of parameters in my previous article.

Now build, serve, click Load my timeline and Load my favorites buttons to get and cache API responses, and switch to offline. You’ll see tweets are displayed there as well!

Runtime caching in offline demo

You may notice that for the Timeline NGSW is trying to reach the network (red line in the log) — this is how network-first strategy works. In the contrast, by clicking Favorites we just grab data from the Cache Storage.

What about the optimization for online mode we’ve mentioned? Return back to online and click Timeline / Favorites once or twice. It’s clearly visible that Favorites are loaded immediately, just because we skip the whole network trip and get the data from the cache. How to specify for how long to cache? Using settings in cacheConfig section — we have the fine-grain control there!

Great. NGSW helped us a lot with some really smart network optimizations, requiring only some JSON configuration from us. What about more sophisticated PWA features like Push notifications?

Push notifications

They just work in NGSW without any need to set up anything in the configuration file. We have to just follow some simple conventions on our backend to display a notification. But let’s start from the very beginning — user’s subscription for the notification. We are free to choose from two options here:

  • use JavaScript native navigator['serviceWorker'] object methods related to Web Push API
  • or use the ServiceWorkerModule’s SwPush class. If we open the SwPush API documentation, there is requestSubscription() method we need.

Of course, we go for the second option. Then the subscription flow will look like:

Some clarifications on this code:

  • I assume that we have VAPID public key somewhere in the app configuration (I created a simple ConfigService for this). You can generate a VAPID key pair here.
  • To simplify the code I moved the negotiations with my backend to another tiny service called pushService. You will find the full working source code in my repo (branch ngsw).

Let’s check the result:

Push subscription + notifications

Awesome! We received a bunch of notifications right after the subscription (yes, people are very active in tweeting about JavaScript).

It’s important to know, that to let NGSW properly handle and display notification we have to follow some simple conventions on our backend:

  • We send notification data straight away with the send notification request as a payload (NGSW can’t request this payload later, which is possible in general).
  • We send this data object in the notification property of the payload. It might contain the following fields: title — notification title (required), and all the fields from options of Notification object spec (but not wrapped by options container). The example of such an object:

What else could we achieve with the methods by SwPush class? We can get an active subscription using subscription observable (for example to start the unsubscription process):

Also, we can subscribe to messages observable and receive the notifications data in our app:

Then we can populate one more tweet feed:

Bottomline: push notifications are super simple to implement using Angular Service Worker.

Update flow

Now let’s go back to our application shell and its versions. How exactly NGSW handles the updates of our app?

There are two core principles in NGSW app update implementation:

  • The user workflow should not be interrupted by the unexpectedly updated application. The app version in the opened browser tab will remain the same until the tab close.
  • NGSW should keep app integrity. If any single file in application distributive was updated, we treat the whole corresponding version as a new one. This way we make sure that our application always has a consistent set of files.

How do we achieve these goals?

Let’s explore the NGSW versioning concept. By the version, we mean a set of resources that represent a specific build of our app. If any of the app files changed during the build, the NGSW control (manifest) file ngsw.json will be different (because of calculated hashes for assetGroups / resources / files section and/or different filenames in assetGroups / resources / versionedFiles section). The unique identifier of the app version calculated based on this ngsw.json file content and we have this hash as a part of the Cache Storage names.

Ok, we deployed a newer version of the app with the updated NGSW manifest file. During its start, Angular Service Worker tries to download this ngsw.json with a cache buster in the query string, in the network tab of DevTools it looks like this:

/ngsw.json?ngsw-cache-bust=0.36217997891166953

If this file is different from the previous, NGSW processes it and precaches the resources based on the updated configuration. It’s all done in the background.

But this new version will not be activated immediately, it means that user will still see the older version until they do the next page reload. This is a well-known trade-off of PWAs. Thanks to service worker we can to load the application immediately from the cache, but there might be a newer version on the network… Most likely you already have seen many websites showing popups like “There is a newer version available, would you like to refresh?

Could we organize the same flow using NGSW? Yes! We have everything for this in SwUpdate class of ServiceWorkerModule. According to the documentation, we have an observable available for our needs:

The result:

Update app version promt
Important notice: when the DevTools open, service worker never stops/starts but constantly runs in the background. Since NGSW checks for the updated control file on its start, this behavior might break NGSW update flow. Just close the DevTools before testing this feature.

If we wish to organize the custom update flow (like periodical checks and/or forced activation) there are two useful methods in SwUpdate: checkForUpdate() and activateUpdate(). You can check how do they work at Update Flow tab of PWAtter. Here is the code:

What’s next?

Let’s test this great new feature on the real-world applications! You are welcome to request the new features and submit the bugs to the main Angular repo on the GitHub. Sooner or later we’ll have Angular PWA created by default like it happened for the create-react-app starter.

If you wish to join the conversation about PWAs, you are welcome to the open PWA slack with 800+ developers.

I’m open for the offers to hold Progressive Web Apps / Angular / Angular Service Worker workshops and/or sessions for your conference, meeetup or company anywhere in the world, reach out to me via salnikov@gmail.com.