Angular PWA: Building Progressive Web Apps

This tutorial will guide you through the process of building a Progressive Web App (PWA) using Angular. We will start by understanding what a PWA is and the benefits it offers. Then, we will dive into the steps required to set up an Angular project and build the app shell. We will also explore how to add offline support, push notifications, and optimize the performance of our PWA.

angular pwa building progressive web apps

Introduction

What is a Progressive Web App?

A Progressive Web App is a web application that combines the best features of both web and native mobile apps. PWAs can be accessed through a browser like a traditional website, but they can also be installed on a user's device and function offline. PWAs are designed to be responsive, reliable, and provide an engaging user experience.

Benefits of Progressive Web Apps

There are several benefits to building a PWA:

  1. Offline functionality: PWAs can work offline or with a poor internet connection, enabling users to access content and perform actions without interruption.
  2. App-like experience: PWAs provide a native app-like experience, with features such as full-screen mode, push notifications, and home screen installation.
  3. Cross-platform compatibility: PWAs work on any device or platform with a modern web browser, including desktop, mobile, and tablet devices.
  4. Improved performance: PWAs can load quickly and respond to user interactions instantly, providing a smooth and seamless user experience.
  5. Easy installation: Users can install a PWA on their device's home screen without going through an app store, making it more convenient and accessible.

Why Angular for Progressive Web Apps

Angular is a popular framework for building web applications, and it provides excellent support for building PWAs. Angular offers a powerful set of tools for creating responsive and interactive user interfaces, managing data flow, and optimizing performance. It also has built-in support for service workers, which are essential for adding offline functionality to PWAs. With Angular, you can easily build a high-quality PWA that works across different platforms and devices.

Getting Started

Setting up Angular CLI

Before we can start building our PWA, we need to set up the Angular CLI. The Angular CLI is a command-line interface that helps us scaffold, develop, and test Angular applications efficiently.

To install the Angular CLI, open your terminal and run the following command:

npm install -g @angular/cli

Creating a new Angular project

Once the Angular CLI is installed, we can create a new Angular project using the following command:

ng new angular-pwa

This will create a new directory called angular-pwa with all the necessary files and dependencies for our project.

Understanding the project structure

The project structure of an Angular application consists of several directories and files. Here is a brief overview of the important files and directories:

  • src: This directory contains the source code of our application.
    • app: This directory is where most of our application code will reside.
      • app.component.ts: This file defines the main component of our application, which is the entry point for our app.
      • app.module.ts: This file defines the root module of our application, where we can import and configure other modules and components.
    • assets: This directory is used to store static assets such as images, fonts, and stylesheets.
    • index.html: This file is the main HTML file of our application, where we can include scripts, stylesheets, and other resources.
  • angular.json: This file contains configuration settings for our Angular project, such as build options, asset paths, and other project-specific settings.
  • package.json: This file lists all the dependencies and scripts required for our project.

Building the App Shell

Creating the main component

The app shell is the minimal HTML, CSS, and JavaScript required to power the user interface of our PWA. We will start by creating the main component of our application.

In the src/app directory, create a new file called app.component.ts and add the following code:

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <header>
      <h1>Welcome to my Angular PWA</h1>
    </header>
    <nav>
      <ul>
        <li><a routerLink="/">Home</a></li>
        <li><a routerLink="/about">About</a></li>
        <li><a routerLink="/contact">Contact</a></li>
      </ul>
    </nav>
    <router-outlet></router-outlet>
  `,
  styles: [
    `
    /* Add your custom styles here */
    `
  ]
})
export class AppComponent {}

This code defines the main component of our application, which includes a header, navigation menu, and a <router-outlet> where the content of each route will be rendered.

Defining the routes

Next, we need to define the routes for our application. In the src/app directory, create a new file called app-routing.module.ts and add the following code:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

// Import your components for each route
import { HomeComponent } from './home.component';
import { AboutComponent } from './about.component';
import { ContactComponent } from './contact.component';

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'contact', component: ContactComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

In this code, we define three routes: one for the home page, one for the about page, and one for the contact page. Each route is associated with a specific component.

Implementing lazy loading

To optimize the loading time of our PWA, we can use lazy loading to load modules and components only when they are needed. To implement lazy loading, we need to modify our app-routing.module.ts file.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  { path: '', loadChildren: () => import('./home.module').then(m => m.HomeModule) },
  { path: 'about', loadChildren: () => import('./about.module').then(m => m.AboutModule) },
  { path: 'contact', loadChildren: () => import('./contact.module').then(m => m.ContactModule) }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

In this code, we use the loadChildren property instead of the component property to load the modules lazily. The loadChildren property takes a function that returns a promise for the module to be loaded.

Offline Support

Caching static assets

To provide offline support in our PWA, we need to cache the static assets such as HTML, CSS, JavaScript, and images. We can achieve this by using service workers.

To get started, we need to modify our src/ngsw-config.json file to specify which assets to cache. Open the file and update it with the following code:

{
  "index": "/index.html",
  "dataGroups": [
    {
      "name": "app-shell",
      "urls": [
        "/index.html",
        "/favicon.ico",
        "/*.css",
        "/*.js",
        "/*.woff2",
        "/*.woff",
        "/*.ttf",
        "/*.svg",
        "/*.png",
        "/*.jpg"
      ],
      "cacheConfig": {
        "maxSize": 10,
        "maxAge": "7d",
        "strategy": "freshness"
      }
    }
  ]
}

In this code, we define a data group named app-shell and specify the URLs of the assets to cache. We also set the maximum size and age of the cache and the caching strategy.

Using service workers

Angular provides built-in support for service workers through the @angular/service-worker package. To enable service workers in our PWA, we need to modify our angular.json file.

Open the file and locate the projects > <project-name> > architect > build > options section. Add the following code:

"serviceWorker": true,
"ngswConfigPath": "src/ngsw-config.json"

This code enables service workers and specifies the path to the ngsw-config.json file.

Handling offline requests

To handle offline requests in our PWA, we need to modify our src/app/app.module.ts file and add the following code:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ServiceWorkerModule } from '@angular/service-worker';
import { AppComponent } from './app.component';
import { environment } from '../environments/environment';

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

In this code, we import the ServiceWorkerModule from @angular/service-worker and add it to the imports array of our root module. We also pass the ngsw-worker.js file and enable service workers based on the environment.

Push Notifications

Setting up Firebase Cloud Messaging

To enable push notifications in our PWA, we can use Firebase Cloud Messaging (FCM). FCM is a cross-platform messaging solution that allows us to send push notifications to users.

To get started, we need to create a Firebase project and configure FCM. Follow these steps:

  1. Go to the Firebase console and create a new project.
  2. Enable Firebase Cloud Messaging for your project.
  3. Generate a new server key or add a new web app to get the configuration details.

Once you have the configuration details, open the src/environments/environment.ts file and add the following code:

export const environment = {
  production: false,
  firebaseConfig: {
    apiKey: 'YOUR_API_KEY',
    authDomain: 'YOUR_AUTH_DOMAIN',
    projectId: 'YOUR_PROJECT_ID',
    storageBucket: 'YOUR_STORAGE_BUCKET',
    messagingSenderId: 'YOUR_MESSAGING_SENDER_ID',
    appId: 'YOUR_APP_ID'
  }
};

Replace the placeholders with your own Firebase configuration details.

Implementing push notifications

To implement push notifications in our PWA, we need to modify our src/app/app.component.ts file and add the following code:

import { Component, OnInit } from '@angular/core';
import { SwPush } from '@angular/service-worker';
import { environment } from '../environments/environment';

@Component({
  selector: 'app-root',
  template: `
    <!-- Add your HTML code here -->
  `,
  styles: []
})
export class AppComponent implements OnInit {
  constructor(private swPush: SwPush) {}

  ngOnInit() {
    if (this.swPush.isEnabled) {
      this.swPush
        .requestSubscription({ serverPublicKey: environment.vapidPublicKey })
        .then(subscription => {
          // Save the subscription details to your server
        })
        .catch(error => console.error('Error subscribing to push notifications:', error));
    }
  }
}

In this code, we import the SwPush service from @angular/service-worker and inject it into our component. We then check if push notifications are enabled and request a subscription using the VAPID public key from our environment config.

Handling notification clicks

To handle notification clicks in our PWA, we need to modify our src/app/app.component.ts file and add the following code:

import { Component, OnInit } from '@angular/core';
import { SwPush, SwNotificationClickEvent } from '@angular/service-worker';
import { environment } from '../environments/environment';

@Component({
  selector: 'app-root',
  template: `
    <!-- Add your HTML code here -->
  `,
  styles: []
})
export class AppComponent implements OnInit {
  constructor(private swPush: SwPush) {}

  ngOnInit() {
    if (this.swPush.isEnabled) {
      this.swPush.notificationClicks.subscribe((event: SwNotificationClickEvent) => {
        // Handle the notification click event
      });
    }
  }
}

In this code, we subscribe to the notificationClicks event emitted by the SwPush service. This allows us to handle the click event and perform any necessary actions.

Optimizing Performance

Code splitting

To optimize the performance of our PWA, we can use code splitting to split our application into smaller chunks that can be loaded on-demand. This reduces the initial load time and improves the overall performance of our app.

To implement code splitting, we need to modify our route definitions in the src/app/app-routing.module.ts file. Update the code as follows:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  { path: '', loadChildren: () => import('./home.module').then(m => m.HomeModule) },
  { path: 'about', loadChildren: () => import('./about.module').then(m => m.AboutModule) },
  { path: 'contact', loadChildren: () => import('./contact.module').then(m => m.ContactModule) }
];

@NgModule({
  imports: [RouterModule.forRoot(routes, { enableTracing: true })],
  exports: [RouterModule]
})
export class AppRoutingModule {}

In this code, we pass an additional option { enableTracing: true } to the RouterModule.forRoot() method. This enables route tracing, which logs the route loading process in the console for debugging purposes.

Lazy loading modules

To enable lazy loading of our modules, we need to modify our route definitions in the src/app/app-routing.module.ts file. Update the code as follows:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  { path: '', loadChildren: () => import('./home/home.module').then(m => m.HomeModule) },
  { path: 'about', loadChildren: () => import('./about/about.module').then(m => m.AboutModule) },
  { path: 'contact', loadChildren: () => import('./contact/contact.module').then(m => m.ContactModule) }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

In this code, we update the path for each module to include the directory name. This ensures that the modules are loaded lazily and split into separate chunks.

Performance testing and optimization

To test and optimize the performance of our PWA, we can use tools such as Lighthouse and WebPageTest. These tools analyze our app and provide recommendations for improving performance.

To run a performance audit using Lighthouse, open your terminal and navigate to the root directory of your Angular project. Then, run the following command:

ng build --prod

This command builds the production version of our app. Once the build is complete, run the following command:

ng run <project-name>:server:production

Replace <project-name> with the name of your project. This command starts a local server to serve the production build of our app. Finally, open your browser and navigate to http://localhost:4000. Open the Developer Tools and go to the "Audits" tab. Click on "Perform an audit" and select the desired options. Click on "Run audit" to start the performance audit.

Conclusion

In this tutorial, we learned how to build a Progressive Web App (PWA) using Angular. We started by understanding what a PWA is and the benefits it offers. Then, we explored the steps required to set up an Angular project and build the app shell. We also added offline support, push notifications, and optimized the performance of our PWA. By following this tutorial, you should now have a solid foundation for building your own Angular PWAs and providing an engaging user experience for your users.