How to take your Angular apps offline

Everything you need to know about Service Workers

Unplug your app from the internet!

We’ve all been there — you are filling up a form, click submit, and oops — something went wrong with your connection. Now you need to refresh the page and fill everything up again because the app broke 😤

The reality of this is that intermittent connections are prevalent worldwide— as of 2017, 43 million brits have reported unreliable copper connections . In Germany, a decision to stick with copper over fibre-optic connections in the 1980s has left the internet infrastructure there to be one of the more unreliable ones in the EU (at least until optic-fibre is implemented in 2025) .

As you probably already know, there’s a way to improve experiences for offline users. Let’s take a simple todo list as an example — ideally, offline users can still take new notes which get synced once you get back online.

TL;DR: If you don’t want an explanation, you can view the github link directly . Explanations are grouped by branch.

What’s next?

To take your apps offline, you first need to add a service worker. At its core, a service worker is a Javascript file that intercepts and caches requests. The cool thing about it is that it runs separate from the main thread, so it can get in touch with a server while the app is not running. You can use this to add push notifications to your app, for example.

Where’s the catch?

Since service workers are designed to be run completely asynchronously from the main app, there are a few things you need to keep in mind:

  • No access to local storage — the official docs by google state that this is due to its asynchronous design . You can use something like indexedDB are instead.
  • Service workers only run over HTTPS . We’re mentioning this only because the default Angular development environment runs over HTTP.
  • Service workers can become idle and can also restart when the browser deems fit. This means that you need to persist your state.
  • No access to the DOM due to separation of concerns in the design

📰 The good news

One of the many advantages of converting your app to a PWA is that you also auto-magically add a service worker during the installation !

While converting your app into a PWA may seem counter-intuitive, keep in mind that PWAs are installable — so your app works while it’s offline.

And more good news! Angular’s implementation of service workers already caches your requests. You have these nifty features right out of the box:

  • Cached files are tied to your app version — so if you update your app on the server, the client’s app won’t suddenly break.
  • New versions of the app are downloaded in the background . This means that your site viewers will view the latest version of their app after revisiting the page. You can also hook onto refresh events — which can be a powerful way to handle app updates
  • Service workers only download changed resources and try to conserve bandwidth when possible

🌶 Forms are hard

Here’s where things get spicy. Service workers primarily cache incoming requests, not outgoing requests. This means that each app will need to build its own implementation of form syncing. Let’s jump right in!

You don’t have time to waste, so let’s start off with all the boring bits taken care of . The linked demo includes a simple json backend server (json-server), with a pwa todo list.

Please note that due to the deployment limitations on stackblitz, this demo needs to be run locally. This is due to a multitude of reasons — the normal “ng-serve” does not work with service workers, and a backend needs to be available on a separate port. To view the service worker, you need to run the provided “ start:service-worker ” command.

It should also be noted that hot reload does not work when you’re using the “ start:service-worker ” command

If you have any questions or issues any of the steps, please refer to the Q&A at the bottom of the article.

If everything is working as expected, you should be able to see the service worker in the development tools, and your app should have an installation prompt (the small plus icon on the right of the URL bar in the case of chrome)

What’s next?

The first step is to detect whether the user is online or offline. The window object has some nifty events which we can quickly attach to an rxjs subject which you can read more on here .

import { Injectable } from '@angular/core';
import {BehaviorSubject} from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class OnlineService {
  isOnline$ = new BehaviorSubject<boolean>(window.navigator.onLine);

  constructor() {
    this.listenToOnlineStatus();
  }

  listenToOnlineStatus(): void {
    window.addEventListener('online', () => this.isOnline$.next(true));
    window.addEventListener('offline', () => this.isOnline$.next(false));
  }
}

The next step is to initialize the indexDB. It is important that you do NOT store your data in variables, as your app may restart at any point in time.

There are several wrappers we can use to wrap the browser’s native indexDB in Angular; For this example, we will be using ngx-indexed-db .

...
const dbConfig: DBConfig  = {
  name: 'MyDb',
  version: 1,
  objectStoresMeta: [{
    store: 'tasks',
    storeConfig: { keyPath: 'id', autoIncrement: true },
    storeSchema: [
      { name: 'name', keypath: 'name', options: { unique: false } },
      { name: 'isComplete', keypath: 'isComplete', options: { unique: false } },
      { name: 'isOnServer', keypath: 'isOnServer', options: {unique: false}}
    ]
  }]
};

@NgModule({
  declarations: [...],
  imports: [
   ...
    NgxIndexedDBModule.forRoot(dbConfig),
    ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production })
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

We now need to rework our task service to sync when online and use the dbService when you’re offline. To keep the demo simple, we:

  • Disable the delete and editing buttons when the user is offline — this means that I don’t need to keep track of records to delete or updates when syncing
  • One service is being used to handle API communication and offline handling — by keeping the logic together, it is easier to understand (but not as maintainable)

You can see what the final code looks like by checking out our git repo .

Key takeaways

Making your app work offline is expensive. Adding offline functionality required almost 3x as much code in the service (without accounting for tests).

Here’s a quick summary of what needs to be done:

  • Make use of a database on the front end and sync with your remote data
  • Handle data migrations for new upgrades on your browser database without losing the old sync data
  • Keep your backend APIs backwards-compatible for possible syncs on older app versions
  • Handling conflicts: what happens if two offline users edit the same record? There’s two main approaches — either avoiding this situation entirely by making sure that shared data can’t be modified offline, or merging records.
  • Handling of Angular app versioning — while you can allow angular to update your app “naturally” in the background, you may want to just push users to the latest version directly
  • Error handling of incomplete syncing
  • Reworking of all your services to switch between 2 sources of truth
  • Integration works better if you are using angular-redux or ngrx.

This may look like a lot of work — because it is. While adding a service worker and converting your app to a PWA is an easier task with strong performance benefits, making your app work offline requires getting your hands dirtier and modifying critical parts of your app’s architecture.

You will also need significant backend rework, and possible optimization of APIs to handle the very large syncing requests.

Q&A

What’s the “start:service-worker” command doing?

☢️ The command adds your device to a global botnet which is using brute force attacks to hack nuclear codes.

Just kidding — The command concurrently:

  • builds the production version of the project in the dist folder
  • Deploys the built code on port 8080 using http-server — since we can’t use ng-serve, we need a new way to view the deployed files
  • Runs a simple json backend —the backend needs to be separate from the app for this demo to be realistic

My changes aren’t showing up, HELP!

Keep in mind that the angular service workers prioritize app speed and stability over showing you the latest changes. What this means to you is that the service worker downloads the latest version of your app when you open it, but does not switch to it immediately. You need to refresh/hard-refresh your page to show the latest app version.

If using the latest version of the app is absolutely necessary, you can listen to the service worker events and manually call window.reload().

@Injectable()
export class LogUpdateService {

  constructor(updates: SwUpdate) {
    updates.available.subscribe(event => {
      console.log('current version is', event.current);
      console.log('available version is', event.available);
    });
    updates.activated.subscribe(event => {
      console.log('old version was', event.previous);
      console.log('new version is', event.current);
    });
  }
}

Any other development tips?

1. You should keep in mind that when using developer tools, the service worker is kept running in the background and never restarts.

2. If you want to remove the service worker entirely, remove the ngsw.json file. When the Angular service worker can’t find this file, it deactivates itself as a fail-safe.

3. When switching your app versions on the frontend, you might want to migrate your data. You can read more about migration handling in the official ngx-indexed-db quickstart guide

4. It is recommended to keep a versioning system in your API — this way you can segregate older APIs from newer ones and make it easier to deprecate the old APIs.

5. Consider adding some form of logging in your app to track the app versions out there in the wild. This will make future decisions on API deprecations easier.

Are pages really being cached? How can I confirm this?

Files cached by the service worker can be viewed in the “cache storage” section. As can be seen in the screenshot below, the main and the index files are being stored in this specific cache. The files stored here depend largely on the contents in your ngsw-config file. You can read more about configuring it in the official docs .

If you hit refresh, you can also see that your app should load much faster but the todo app is small in itself, so you can fix this by slowing down your connection speed manually with the chrome dev tools:

The connection was slowed down to “Slow 3G”. The tasks API request took 2s, but the main bundle was loaded in 10ms due to the service worker caching!

Tags

Angular
Business
Service Workers
menu_icon
Gabriel Stellini

18th October 2020