The Ultimate Guide to Building Offline Angular Apps with Service Workers
Tags: angular, webdev, javascript
Published: 2024-05-26 06:19:49

Service workers and Progressive Web Apps represent a significant advancement in web technology. By using service workers, developers can create PWAs that offer enhanced speed, offline functionality, and improved user engagement. This guide provides an in-depth look at service workers, their functionality, and how they enhance PWAs and how to implement these technologies in Angular applications.

## What are Service Workers?

**Background Scripts:** Service workers are special scripts that run in the background of your web browser.

**Persistence:** These scripts stay active even after you close the tab, allowing them to handle tasks like push notifications or background syncs.

**Separate Thread:** They run on a different thread from your main web page, so they don’t slow down your page directly.

Service workers act as **network proxies**, intercepting outgoing HTTP requests and determining how to respond to them. The service worker can intercept any requests your browser makes to load things like images, files, or data from the internet whether it’s through programmatic APIs like **fetch**.

**Handling Requests:** The service worker can decide how to respond to these requests. It can:

- Serve a cached version if available.
- Forward the request to the network if the cached version isn’t 
  available or up-to-date.

Adding a service worker to an Angular application is one of the steps for turning an application into a **Progressive Web App** (PWA).

## What is a Progressive Web App (PWA)?

A PWA is a web application that uses modern web technologies to deliver an app-like experience. Key features of PWAs include:

**- Offline Functionality:** PWAs can work offline or with poor network conditions.
**- App-like Experience:** They look and feel like native apps.
**- Push Notifications:** PWAs can send notifications to users.
**- Installable:** Users can install PWAs on their devices from the browser.

## Benefits of Service Workers and PWA

**1. Caching:** Using a service worker to cache resources enhances site speed and reliability.

**2. Installable Applications:** Making your site installable provides easy access for customers from their home screen or app launcher.

**3. Network Reliability:** PWAs improve user experience in markets with unreliable networks or expensive mobile data by providing offline functionality.

**4. Seamless Offline Experience:** Keeping users in your PWA when offline offers a more seamless experience compared to the default browser offline page.

Angular’s service worker follows certain rules to ensure a reliable and consistent experience for users.

- For instance, it caches the entire application as a single 
  unit, ensuring all files update together. This prevents users 
  from seeing a mix of old and new versions of the application.
- Additionally, the service worker conserves bandwidth by only 
  downloading resources that have changed since the last cache 
  update.

## Browser Support

To benefit from the Angular service worker, your application must run in a web browser that supports service workers. To make sure your app still runs smoothly for everyone, regardless of their browser, you need to check if the Angular service worker is enabled using **SwUpdate.isEnabled** before your app tries to use it. You can check if **SwUpdate** is enabled before using it by injecting it into your component or service and then checking its **isEnabled** property. This simple step can help prevent errors and ensure a better experience for all users.

```
export class AppComponent {
  constructor(private swUpdate: SwUpdate) {
    if (this.swUpdate.isEnabled) {
      // SwUpdate is enabled, you can use it here
    } else {
      console.log('Service worker updates are not enabled.');
    }
  }
}
```

## HTTPS Requirement

For service workers to be registered, the application must be accessed over **HTTPS**, not **HTTP**. Browsers enforce this requirement to prevent potential security vulnerabilities. However, browsers do not require a secure connection when accessing an application on localhost.

## Caching

Service workers provide access to the Cache interface, a caching mechanism separate from the HTTP cache.

Service workers utilize two caching concepts:

**· Precaching:**
Precaching involves caching assets ahead of time, typically during installation, improving page speed and offline access.

**· Runtime caching:**
Runtime caching applies caching strategies to assets requested from the network during runtime

## Managing Caching

To manage caching effectively, Angular’s service worker uses a manifest file called **ngsw.json**. This file describes which resources to cache and includes unique identifiers (hashes) for each file’s content. When there’s an update to the application, the manifest file changes, prompting the service worker to download and cache the new version.

## The ngsw.json File

**Creation and Updates:**

The **ngsw.json** file is created automatically based on a configuration file called ngsw-config.json.

The **ngsw-config.json** JSON configuration file specifies which files and data URLs the Angular service worker should cache and how it should update the cached files and data.

When you deploy an update to your Angular application, the contents of **ngsw.json** change, signaling the service worker to download the new version.

**Service Worker Update Behavior:**

**Service workers** serve the cached version for speed but periodically check for updates in the background.

When an update is available, the service worker installs it in the background and switches to the updated version on the next page load or reload.

## Adding a service worker to your project

To set up the Angular service worker in your project, run the following CLI command:

```
ng add @angular/pwa 
```
The above command:

1. Adds the **@angular/service-worker** package to your project.

2. Imports and registers the service worker with the application’s root providers.

3. Updates the **index.html** file:

- Includes a link to add the **manifest.webmanifest** file.
- Adds a meta tag for **theme-color**.
- Installs icon files to support the installed Progressive Web 
  App (PWA).

4. Creates the service worker configuration file called **ngsw-config.json**, which specifies the caching behaviors and other settings.

Now build and run the server using the following commands:

```
ng build
npx http-server -p 8080 -c-1 dist/project-name/browser
```

## Service worker configuration properties

**appData:**
This section allows you to pass additional data describing the current version of the application.

It’s commonly used to provide information for update notifications.

```
"appData": {
  "version": "1.0",
  "releaseDate": "2024-05-12"
}
```
**index:**
This says which file should be shown when someone opens your app. Usually, it’s the **index.html** file.

```
"index": "/index.html"
```
**assetGroups:**
These are groups of things your app needs, like images or scripts and their caching policies. These resources can originate from the application’s domain or external sources such as Content Delivery Networks (CDNs)

It’s recommended to organize asset groups in descending order of specificity. More specific groups should appear higher in the list. For instance, a group matching **/foo.js** should precede a group matching **\*.js**.

**Key Properties of an asset group**

Each object in the assetGroups array adheres to the AssetGroup interface

```
interface AssetGroup {
  name: string;
  installMode?: 'prefetch' | 'lazy';
  updateMode?: 'prefetch' | 'lazy';
  resources: {
    files?: string[];
    urls?: string[];
  };
  cacheQueryOptions?: {
    ignoreSearch?: boolean;
  };
}
```
**- name:** this is mandatory and serves to identify the group of 
  assets
**- installMode Property:** Determines how resources are 
  initially cached. It defaults to prefetch.

**_prefetch:_** Fetches and caches all listed resources while caching the current application version.

**_lazy:_** Defers caching until a resource is requested. Only caches resources that are explicitly requested, useful for conserving bandwidth.

**- updateMode Property:** Governs caching behavior for resources 
  already in the cache when a new application version is 
  detected. It defaults to the value of installMode.

**_prefetch:_** Immediately caches changed resources.

**_lazy:_** Defers caching of changed resources until they are requested again. This mode is applicable only if the installMode is also set to lazy.

**- resources Property:** Specifies files or URLs to cache.

**- cacheQueryOptions Property:** Modifies the matching behavior 
  of requests. Currently, only **ignoreSearch** is supported, 
  which disregards query parameters when matching requests.

**dataGroups:**
Data groups are used for caching data, like API responses.

Data groups follow this Typescript interface:

```
export interface DataGroup {
  name: string;
  urls: string[];
  version?: number;
  cacheConfig: {
    maxSize: number;
    maxAge: string;
    timeout?: string;
    strategy?: 'freshness' | 'performance';
  };
  cacheQueryOptions?: {
    ignoreSearch?: boolean;
  };
}
```
**Key Properties of a data group**

**- name:** Uniquely identifies the data group.
**- urls:** Patterns that define which requests should be cached. 
  Only non-mutating requests (GET and HEAD) are cached.
**- version:** It’s like a little label attached to the data that 
  tells the app what version of the API it came from. This helps 
  the app know if the cached data matches up with the version 
  it’s currently using. If it doesn’t match because the API has 
  changed, the app knows it needs to get rid of the old cached 
  data and fetch fresh stuff that matches the new version of the 
  API.
**- cacheConfig:** Defines how requests matching the URLs should 
  be cached. It includes the following properties:
maxSize: Limits the number of cached entries or responses.

**_maxAge:_** Specifies how long a response can stay in the cache before being considered invalid.

**_timeout:_** Determines how long the ServiceWorker should wait for a network response before using a cached version.

**_strategy:_** Defines how caching should be handled. It can prioritize **performance** (This strategy focuses on speed. If a requested resource is already in the cache, it will be used without making a network request) or **freshness** (This strategy prioritizes getting the most current data. It will try to fetch the requested data from the network first. Only if the network request fails (times out) will it use the cached version) of the data.

**- cacheQueryOptions Property:** Modifies the matching behavior 
  of requests. Currently, only ignoreSearch is supported, which 
  disregards query parameters when matching requests.

**navigationUrls:**
Optional section to specify URLs that should be redirected to the index file.

```
"navigationUrls": [
    "/**"
]
```
**Navigation Request Strategy:**
The **navigationRequestStrategy** property allows configuring how navigation requests are handled, with options like ‘**performance**’ and ‘**freshness**’.

```
"navigationRequestStrategy": “freshness”
```

## SwUpdate Service

Enabling service worker support does more than just register the service worker; it also provides services you can use to interact with the service worker and control the caching of your application.

The **SwUpdate** service in Angular helps manage updates for your web application by notifying you about new updates and giving you control over when to check for and activate these updates

## Using SwUpdate Service

The **SwUpdate** service in Angular gives you access to events that indicate when the service worker discovers and installs an available update for your application. For example:

```
import { Injectable } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';

@Injectable({ providedIn: 'root' })
export class LogUpdateService {
  constructor(private updates: SwUpdate) {
    updates.versionUpdates.subscribe((evt) => {
      switch (evt.type) {
        case 'VERSION_DETECTED':
          console.log(`Downloading new app version: ${evt.version.hash}`);
          break;
        case 'VERSION_READY':
          console.log(`New app version ready for use: ${evt.latestVersion.hash}`);
          break;
        case 'VERSION_INSTALLATION_FAILED':
          console.error(`Failed to install app version '${evt.version.hash}': ${evt.error}`);
          break;
      }
    });
  }
}
```
**Checking for Updates**

You can manually ask the service worker to check if any updates are available. This is useful if you want updates to occur on a schedule or if your site changes frequently. For example:

```
import { Injectable } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';
import { interval } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class CheckForUpdateService {
  constructor(private updates: SwUpdate) {
    interval(6 * 60 * 60 * 1000).subscribe(() => {
      updates.checkForUpdate().then(updateFound => {
        console.log(updateFound ? 'A new version is available.' : 'Already on the latest version.');
      }).catch(error => {
        console.error('Failed to check for updates:', error);
      });
    });
  }
}
```
In the above, the service checks for updates every 6 hours using the **checkForUpdate()** method.

**Updating to the Latest Version**

You can prompt the user to update to the latest version by reloading the page when a new version is ready. For example:

```
import { Injectable } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';

@Injectable({ providedIn: 'root' })
export class PromptUpdateService {
  constructor(private swUpdate: SwUpdate) {
    swUpdate.versionUpdates.subscribe(evt => {
      if (evt.type === 'VERSION_READY') {
        if (confirm('A new version is available. Do you want to update?')) {
          document.location.reload();
        }
      }
    });
  }
}
```
The above service listens for the ‘**VERSION_READY**’ event and prompts the user to update.

**Handling Unrecoverable States**

In some cases, the application version used by the service worker might be in a broken state that cannot be recovered without a full page reload. You can handle such scenarios by subscribing to the unrecoverable event of **SwUpdate**. For example:

```
import { Injectable } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';

@Injectable({ providedIn: 'root' })
export class HandleUnrecoverableStateService {
  constructor(private updates: SwUpdate) {
    updates.unrecoverable.subscribe(event => {
      alert('An error occurred that we cannot recover from. Please reload the page.');
    });
  }
}
```
The above service notifies the user to reload the page if an unrecoverable error occurs.

## Example
Let’s create a basic News Application to demonstrate the concepts of service workers and Progressive Web Apps (PWAs).

Run the following command to generate a new project:

```
ng new news-app
```
Run the following command to generate new services:

```
ng generate service news
ng generate service check-for-update
```
Run the following command to generate a new interface:

```
ng generate interface article
```
Now, let’s modify the **article.ts** file in the **src/app** directory to implement the Article interface:

```
export interface Article {
    title: string;
    imageUrl: string;
    comments: string[];
  }
```
Now, let’s modify the news.service.ts file in the **src/app** directory to implement the NewsService:

```
import { Injectable } from '@angular/core';
import { Article } from './article'; 

@Injectable({
  providedIn: 'root'
})
export class NewsService {
  private articles: Article[] = [
    { 
      title: 'Article 1', 
      imageUrl: 'https://source.unsplash.com/300x300', 
      comments: ['Article 1 Comment 1', 'Article 1 Comment 2', 'Article 1 Comment 3'] 
    },
    { 
      title: 'Article 2', 
      imageUrl: 'https://source.unsplash.com/300x300', 
      comments: ['Article 2 Comment 1', 'Article 2 Comment 2', 'Article 2 Comment 3'] 
    },
    { 
      title: 'Article 3', 
      imageUrl: 'https://source.unsplash.com/300x300', 
      comments: ['Article 3 Comment 1', 'Article 3 Comment 2', 'Article 3 Comment 3'] 
    },
    // Add more articles as needed
  ];

  constructor() {}

  getArticles(): Article[] {
    // Simulate fetching articles from an API
    return this.articles;
  }
}
```
Run the following command to generate a new component:

```
ng generate component article
```
Now, let’s modify the **article.component.ts**:

```
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Article } from '../article';

@Component({
  selector: 'app-article',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './article.component.html',
  styleUrl: './article.component.css'
})
export class ArticleComponent {
  @Input()
  article!: Article;

  constructor() {}
}
```
Now, let’s modify the **article.component.html**

```
<div class="article">
  <h3>{{ article.title }}</h3>
  <img [src]="article.imageUrl" alt="Article Image" />
  <div class="comments">
    <h4>Comments</h4>
    <ul>
      <li *ngFor="let comment of article.comments">{{ comment }}</li>
    </ul>
  </div>
</div>
```
Run the following command to generate a new component:

```
ng generate component news

```
Now, let’s modify the **news.component.ts** file to use the NewsService:

```
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NewsService } from '../news.service';
import { ArticleComponent } from '../article/article.component';
import { Article } from '../article';

@Component({
  selector: 'app-news',
  standalone: true,
  imports: [CommonModule, ArticleComponent],
  templateUrl: './news.component.html',
  styleUrl: './news.component.css'
})
export class NewsComponent implements OnInit {
  articles: Article[] = [];

  constructor(private newsService: NewsService) {}

  ngOnInit(): void {
    this.articles = this.newsService.getArticles();
  }
}
```
Now, let’s modify the **news.component.html** to display the news articles:

```
<div *ngFor="let article of articles">
    <app-article [article]="article"></app-article>
 </div>
```
Now, let’s modify the **app.component.ts** file to use the **NewsComponent** and the **CheckForUpdateService**:

```
import { Component } from '@angular/core';
import { NewsComponent } from './news/news.component';
import { CheckForUpdateService } from './check-for-update.service';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [NewsComponent],
  providers: [CheckForUpdateService],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent {
  title = 'news-app';
  constructor(public checkForUpdateService: CheckForUpdateService){}
}
```
Now, let’s update the **app.component.html** file to remove the default content and render the news component:

```
<app-news></app-news>
```
Run the following command to set up the Angular service worker in your project

```
ng add @angular/pwa
```
Now lets modify the **ngsw-config.json** file to specify which files and data URLs the Angular service worker should cache

```
{
  "$schema": "./node_modules/@angular/service-worker/config/schema.json",
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "app",
      "installMode": "prefetch",
      "resources": {
        "files": [
          "/favicon.ico",
          "/index.html",
          "/manifest.webmanifest",
          "/*.css",
          "/*.js"
        ]
      }
    },
    {
      "name": "assets",
      "installMode": "lazy",
      "updateMode": "prefetch",
      "resources": {
        "files": [
          "/assets/**",
          "/media/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
        ]
      }
    }
  ],
  "dataGroups": [
    {
      "name": "unsplash-images",
      "urls": [
        "https://source.unsplash.com/300x300"
      ],
      "cacheConfig": {
        "strategy": "freshness",
        "maxSize": 50,
        "maxAge": "12h"
      }
    }
  ]
}
```
Now, let’s modify the **check-for-update.service.ts** file in the **src/app** directory to implement the **CheckForUpdateService**. This service checks for updates every 6 seconds and prompts the user to reload the page if a new version is ready.

```
import { ApplicationRef, Injectable } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';
import { concat, interval } from 'rxjs';
import { first } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class CheckForUpdateService {
  constructor(appRef: ApplicationRef, updates: SwUpdate) {
    if (updates.isEnabled) {
      const appIsStable$ = appRef.isStable.pipe(
        first((isStable) => isStable === true)
      );
      const everySixSeconds$ = interval(6 * 1000); // 6 seconds
      const everySixSecondsOnceAppIsStable$ = concat(
        appIsStable$,
        everySixSeconds$
      );

      everySixSecondsOnceAppIsStable$.subscribe(async () => {
        try {
          const updateFound = await updates.checkForUpdate();
          if (updateFound) {
            if (confirm('New version available. Load new version?')) {
              // Reload the page to update to the latest version.
              document.location.reload();
            }
          } else {
            console.log('Already on the latest version.');
          }
        } catch (err) {
          console.error('Failed to check for updates:', err);
        }
      });
    }
  }
}
```
Now lets modify the **app.config.ts** to configure service worker using **provideServiceWorker** inorder to be able test in dev mode

```
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';
import { provideServiceWorker } from '@angular/service-worker';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes), provideServiceWorker('ngsw-worker.js', {
        enabled: true,
        registrationStrategy: 'registerImmediately'
    })]
};
```
Now you can build and run the server using the following commands:

```
ng build
npx http-server -p 8080 -c-1 dist/news-app/browser
```

## Testing Offline Mode

To simulate a network issue:

- In Chrome, open Developer Tools (**Tools > Developer Tools**).
- Go to the **Network** tab.
- Select **Offline** in the **Throttling** dropdown menu.

This action disables network interaction for your application.

Despite the network issue, the application should still load normally on refresh due to the service worker.

When testing Angular service workers, it’s a good idea to use an incognito or private window in your browser to ensure the service worker doesn’t end up reading from a previous leftover state, which can cause unexpected behavior.

## Conclusion

This article provided a comprehensive overview of service workers and Progressive Web Apps (PWAs). We examined how service workers operate as background scripts that run independently from web pages, enhancing performance through caching and enabling offline capabilities. PWAs were highlighted for their ability to deliver app-like experiences directly from the browser, offering features such as offline functionality, push notifications, and installability.

We also covered the practical steps to integrate service workers into Angular applications, including the use of Angular CLI, managing caching strategies with **ngsw-config.json**, and utilizing the **SwUpdate** service for handling updates

To get the whole code check the link below👇👇👇
[https://github.com/anthony-kigotho/NewsApp](https://github.com/anthony-kigotho/NewsApp)

## CTA
Many developers and learners encounter tutorials that are either too complex or lacking in detail, making it challenging to absorb new information effectively.

[Subscribe to our newsletter](https://bytewave.substack.com/) today, to access our comprehensive guides and tutorials designed to simplify complex topics and guide you through practical applications.