Create Progressive Web Apps with Angular and other free tools!

paco_ita

Francesco Leardini

Posted on July 7, 2019

Create Progressive Web Apps with Angular and other free tools!

Who said that creating PWAs is difficult?

In this session we will discover some practical solutions to build our next Progressive Web App with ease.

Before starting, just a quick recap of what we learned so far:

  • Introduction: provided us the background and an overview about the benefits of progressive web apps.

  • Install a PWA: described what a web app manifest is and how can we configure it.

  • Caching strategies: faced service workers (SW) and how we can configure caching strategies to leverage their full potential.
     

The article is composed by three sections, feel free to jump to a specific one or follow along if you prefer:

 

PWA Builder

PWA Builder

PWA Builder is an open source project from Microsoft (repo). The current version (2.0) brings a complete new layout and more functionalities to better assist developers.

Accessing the web page we have in the header two menu items:

  • My hub (opened by default)

  • Feature store
     

My hub page

The goal of this section is to analyse a given web site and provide hints to make it completely PWA ready.

pwa-builder-site

By entering the url address of our web application, PWA Builder begins searching for the presence of a web app manifest, an installed service worker and a secure connection, along with several other parameters.

Below I used https://angular.io web site to show an example where the target is already a PWA:

scan-site

Three "report cards" display the analysis results for the web manifest, the service worker and the security, respectively. A score is given for each box (the overall total is 100). This aims to help identifying missing PWA settings and to comply to best practices.

Let's take now another web site: www.repubblica.it.
Here no service worker is installed, reducing the score to a value of only 60. This case might reflect the current situation of our web site, if we don't have implemented any SW yet.

example-no-sw

 
Let's now describe in detail the manifest and service worker section.

Web manifest section

The manifest page allows to drill down into the details of the web manifest:

manifest-info

If any error is present in the file, it will be displayed at the bottom right corner of the right panel where the final web manifest is displayed.

If no manifest file is available at all for the target web site, the application tries to guess some values from the page, like the title for the app name or images from the page content. Those values would then be proposed in a form, whose fields coincide with the web manifest properties.
We can manually edit those fields or upload new images and the PWA Builder would directly update the final json file.

The settings tab allows to define further properties. With the help of drop downs we do not need to remember all the possible values, allowing us to tune the web manifest with ease:

manifest-settings

Service worker

This section is probably more interesting as it allows to choose among a set of the most common SW scenarios, like displaying a simple offline page or implementing the stale while revalidate caching strategy (it has been covered in the previous article if you want to know more details about it).

When we select one of the options offered, the code snippets on the right side are updated accordingly. All what we have to do at this point is to download and upload the file into our web application.

sw
 

Feature store page

This page collects preconfigured code snippets allowing to further enhance our PWA. We just have to select one feature and import the code into our project. Done, yay!! 😀

feature-store-page

The Microsoft team is working to add more snippets in the future release.
 

Build my PWA

Aside from working with single files individually, PWA Builder offers also the possibility to generate a whole, basic application targeting different platforms.

build-pwa

 
You can find the tool documentation here 📔
 

 

Workbox

workbox-logo

Workbox is an open source project from Google (here the repo).

It consists in a set of libraries and node modules abstracting the complexity of service workers. This allows to focus on the application business logic, without having to care about the underlying PWA details.

Setup

Workbox gives developers more powerful and granular control compared to PWA Builder, but on the other side it also requires a minimum of Javascript and service workers know how.

To get started we first need to create a service worker, where we import the workbox file workbox-sw.js:

importScripts('https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js');

if (workbox) {
  console.log(`Workbox is loaded!!`);
} else {
  console.log(`Workbox failed to load`);
}
Enter fullscreen mode Exit fullscreen mode

The importScripts() method belongs to the WorkerGlobalScope interface and imports synchronously one or more scripts, comma separated, into the worker's scope.

In Workbox, routes are used to target which requests have to match, according to our requirements.
For this we can use different approaches:

  • Strings
workbox.routing.registerRoute(
  // Matches a Request for the myTargetFile.js file
  '/myTargetFile.js',
  handlerFn
);
Enter fullscreen mode Exit fullscreen mode
  • Regular expressions
workbox.routing.registerRoute(
// Matches image files
  /\.(?:png|gif|jpg|jpeg|svg)$/,
  handlerFn
);
Enter fullscreen mode Exit fullscreen mode
  • Callbacks
const myCallBackFn = ({url, event}) => {
  // Here we can implement our custom matching criteria

  // If we want the route to match: return true
  return true;
};

const handlerFn = async ({url, event, params}) => { 
  return new Response(
   // Do something ...
  );
};

workbox.routing.registerRoute(
  myCallBackFn,
  handlerFn
);
Enter fullscreen mode Exit fullscreen mode

 

Once a defined route matches a request, we can instruct Workbox about what to do through caching strategy modules or custom callbacks (like in the third example above).

Caching strategy modules let us implement one of the caching strategies with just one line of code:

workbox.routing.registerRoute(
  /\.css$/,
  new workbox.strategies.StaleWhileRevalidate({

    // We can provide a custom name for the cache
    cacheName: 'css-cache',
  })
);
Enter fullscreen mode Exit fullscreen mode

The code above caches .css files and implements the StaleWhileRevalidate strategy. Compared to the code we saw in the previous post, we have to admit it is much more concise!!

The supported strategies are:

  • Network First
  • Cache First
  • Stale While Revalidate
  • Network Only
  • Cache Only

Custom callbacks are suited for scenarios where we need to enrich the Response or develop some other specific action not provided by the predefined caching strategies.

Routes and caching modules are the basis of Workbox, but the tool offers much more. We can pre-cache files to make a web app responding even when offline or we can use plugins to manage a background sync queue in case one network request fails, for instance.

The code below shows how it is possible to define how many entries to cache and for how long to retain them:

workbox.routing.registerRoute(
  /\.(?:png|jpg|jpeg|svg)$/,
  new workbox.strategies.CacheFirst({
    cacheName: 'img-assets',
    plugins: [
      new workbox.expiration.Plugin({
        maxEntries: 50,
        maxAgeSeconds: 7 * 24 * 60 * 60,  // 7 days
      }),
    ],
  }),
);
Enter fullscreen mode Exit fullscreen mode

 

Debugging info

While developing our application, it can be useful to debug and see what's going under the hood of Workbox.

The debug builds of Workbox provide many details that can help in understanding if anything is not working as expected.

We need to enable Workbox to use debug builds:

workbox.setConfig({
  debug: true
})
Enter fullscreen mode Exit fullscreen mode

The debug builds log messages to the JavaScript console with specific log levels. If you don’t see some logs, check the log level is set in the browser console. Setting it to Verbose level will show the most detailed messages.

These functionalities constitutes only a little subset of the Workbox potential. If you want to learn more, have a look at the documentation about all the modules currently available.
 

 

Angular

angular_pwa

While the previous tools are framework agnostic, we can implement progressive web apps also with Angular and we will see how easy it is!

Setup

If you are already familiar with angular and have the CLI installed, you can go straight to the next section

For the demo I will work with Visual Code, but you can use any editor you like.
We will also need @angular/cli. If you don't have it installed yet, you can execute the following command:

// using npm 
npm install -g @angular/cli@latest
Enter fullscreen mode Exit fullscreen mode

To verify everything went good, digit ng help in the console and you should see all the available commands:

ng-cli

Let's create a new project:

ng new angular-pwa
Enter fullscreen mode Exit fullscreen mode

After all the node_modules are installed, use the serve command to build and run the application:

ng serve
Enter fullscreen mode Exit fullscreen mode

Opening the browser at http://localhost:4200/ you should see the default angular page:

angular_boilerplate

Good! Now we are set and ready to start.

ready

 

Add PWA capabilities

The add schematics allows to empower an Angular application with PWA features. Execute the following command in the console:

ng add @angular/pwa
Enter fullscreen mode Exit fullscreen mode

We can notice that different things have been updated in our project

added-files

Let's start analysing the updated files first.

angular.json

  "build": {
             ...
           "configurations": {
             "production": {

                ...

                "serviceWorker": true,
                "ngswConfigPath": "ngsw-config.json"
               }
             }
            }
Enter fullscreen mode Exit fullscreen mode

We have two new properties: serviceworker: true and "ngswConfigPath": "ngsw-config.json". The first property will instruct the production build to include the service worker files (ngsw-worker.js and ngsw.json) in the distribution folder, while the latter specifies the path to the service worker configuration file.
 

index.html

  <link rel="manifest" href="manifest.webmanifest">
  <meta name="theme-color" content="#1976d2">
Enter fullscreen mode Exit fullscreen mode

The command registered the web manifest and added a default theme color for our PWA.
 

app.module.ts

TheServiceWorkerModule is downloaded and the service worker file (ngsw-worker.js) is registered.

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production })
  ],
  bootstrap: [AppComponent]
})
Enter fullscreen mode Exit fullscreen mode

However, if we search for the ngsw-worker.js file we cannot find it in our project. The reason is that the file is taken directly form the node_modules folder and placed in the distribution folder (per default /dist, but it can be configured in the angular.json file) after a production build.

⚠️ Note! We should not edit manually the service worker file, as this will be overwritten at each production build, deleting any changes we would have added.

{ enabled: environment.production } is a flag condition that allows the ServiceWorkerModule to register the service worker file. It uses the environment.production variable to enable the registration only after a production build: ng build --prod.

Among the newly generated files, there are a set of images (Angular logos)
in different sizes and place them in the assets/icons folder. These will be used for the home screen icon - once the PWA is installed - and for the splash screen, if the browser supports it.
 

manifest.webmanifest.json
A web-manifest file (manifest.webmanifest.json) is created with default values.

{
  "name": "my-pwa",
  "short_name": "my-pwa",
  "theme_color": "#1976d2",
  "background_color": "#fafafa",
  "display": "standalone",
  "scope": "./",
  "start_url": "./",
  "icons": [
    {
      "src": "assets/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "assets/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "assets/icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "assets/icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "assets/icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "assets/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "assets/icons/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "assets/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "maskable any"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

 

Let's analyse now the SW configuration file, as it is here that the interesting things will happen!

ngsw-config.json

{
  "$schema": "./node_modules/@angular/service-worker/config/schema.json",
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "app",
      "installMode": "prefetch",
      "resources": {
        "files": [
          "/favicon.ico",
          "/index.html",
          "/*.css",
          "/*.js"
        ]
      }
    }, {
      "name": "assets",
      "installMode": "lazy",
      "updateMode": "prefetch",
      "resources": {
        "files": [
          "/assets/**",
          "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
        ]
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

$schema property addresses the configuration schema in the node_module folder. It assists developers by providing validation and hints while editing the file. If you try to add an invalid attribute, the IDE should display a warning:

error

 
index property holds the path to the index page, usually index.html.

 
The assetGroups array has two cache configuration objects:

  • app: this group targets all the static files that constitute the core of our application ("app shell"), therefore we want to fetch them proactively. The property "installMode": "prefetch" specifies to retrieve them while the service worker is installing and make them already available in the cache. If the SW fails gathering the files, the install step is interrupted. On a page reload a new attempt is triggered again.

If we want to include also external resources, as example web fonts, we can add a new attribute url, accepting a string array with resources paths in the glob format.

 "resources": {
        "files": [
          "/favicon.ico",
          "/index.html",
          "/manifest.webmanifest",
          "/*.css",
          "/*.js"
        ],
        "urls": [
          "https://fonts.googleapis.com/**"
        ]
      }
Enter fullscreen mode Exit fullscreen mode

 

  • assets: targets resources that are not immediately needed (eg. images, font files). "installMode": "lazy" tells the service worker to gather the requested data only when requested a first time, not before. prefetch and lazy are the two possible values for the installMode property and describe how eagerly we want to get the underlying resources. "updateMode": "prefetch" specifies how the SW has to behave if a new version of the resource is detected. With the value "prefetch", it retrieves the new version immediately, while "lazy" would let the SW fetch it only if requested again.

⚠️ Note! "updateMode": "lazy" works only if also installMode has a value "lazy".

The fetched files are stored in the Cache Storage, an interface for all the caches accessible by the service worker.

assetGroups is reserved for asset resources and created automatically with the ng add @angular/add command. We can add another array though, called dataGroups, for caching data requests.
Let's add the following code in the ngsw-config.json file (just after assetGroups):

  "dataGroups": [{
    "name": "jokes-cache",
    "urls": [ "https://icanhazdadjoke.com/"],
    "cacheConfig": {
      "strategy": "performance",
      "maxSize": 5,  
      "maxAge": "15m"
    }
  },
  {
    "name": "stocks-cache",
    "urls": [ "https://api.thecatapi.com/v1/images/search"],
    "cacheConfig": {
      "strategy": "freshness",
      "maxSize": 10,
      "maxAge": "1d",
      "timeout": "5s"
    }
  }]
Enter fullscreen mode Exit fullscreen mode

After defining a name for each cache, we set the API endpoints we are interested to cache through the urls property.
The cacheConfig section defines the policy to apply to the matching requests:

  • maxSize: the max number of responses to cache.
     

  • maxAge: sets the cache entries lifespan. After this period the cached items are deleted.
    Accepted suffixes:
    d: days
    h: hours
    m: minutes
    s: seconds
    u: milliseconds
     

  • timeout: using the freshness strategy, it refers to a network timeout duration after which, the service worker will attempt to retrieve the data from the cache.
     

As described in the Angular docs only those two caching strategies are available:

Performance, the default, optimises for responses that are as fast as possible. If a resource exists in the cache, the cached version is used, and no network request is made. This allows for some staleness, depending on the maxAge, in exchange for better performance. This is suitable for resources that don't change often; for example, user avatar images.

Freshness optimises for currency of data, preferentially fetching requested data from the network. Only if the network times out, according to timeout, does the request fall back to the cache. This is useful for resources that change frequently; for example, account balances.

In our example, we use the performance strategy for the icanhazdadjoke.com endpoint. This API returns random jokes at each access. As we want to deliver only one new joke every 15 minutes, we can provide the data from the cache setting the lifetime accordingly.

On the other side we adopt the freshness strategy for the api.thecatapi.com endpoint, returning a random image of a cat. We could have used an API providing details about the stock market, but I thought some cats photos would have been cuter. As we really like cats, we decided for freshness strategy, because we want to have the latest up to date details.

The service worker is going to access the network any time the API is called and only if there is a timeout of 5 seconds, as in the case of discontinue or no connection, it will deliver the requested data from the cache.

⚠️ Note! To improve the user experience, especially if the requested data freshness is critical, we should inform the user the provided information comes from the cache and is not up to date.

For the demo I created a simple service for the HTTP calls and changed the default app-component template to show the API calls results.
You can get the full code from the Github repository, but I won't go in detail here about this part. The PWA demo is also available online.
 

Make a PROD build

Now it is time to make a production build with the following command:

ng build --prod
Enter fullscreen mode Exit fullscreen mode

A dist folder (if you left the default settings) will be created. Since we cannot use the ng serve command to test service workers locally, we need to use a web server. I opted for the Chrome extension "web server":

web-server

Accessing the URL proposed with the web server, you should be able to see our Angular project with the following layout:

app-demo

Open the DevTools (F12 in Chrome) and in the Application tab we have our service worker installed:

sw

The DevTools network tab shows us the caching strategies in action:

network-tab

The icanhazdadjoke.com is served from the cache (unless it is expired), while the cats API is fetched from the network. Everything works as planned!

If we switch our connection to airplane mode (on a mobile device) or clicking the offline checkbox in the DevTools to simulate no network connection and refresh the page, we can see that our page is still rendered, without showing the default offline page.

We created a PWA with Angular, easy right?

happy
 

Analysing our PWA

How can we be sure that everything is in order for our newly created PWA? Luckily for us there are different guidelines and tools we use to verify our PWA.
 

PWA Check list

Google engineers released a check list with a lot of points to follow in order to ensure our PWA follows the best practices and will work flawlessly.
The list is divided in several sections. For each of them, some actions are presented to test and fix the specific topic (Lighthouse tool is used to run some of the suggested tests):

pwa-check-list

You can find the complete list here
 

Lighthouse

Lighthouse, from Google, is an open source tool for auditing web pages.
It is possible to target performance, accessibility, progressive web apps and others aspects of a web site.

If any audit fails, it will be reported within its specific section. Scores up to 100 describe how good our web site is:

lighthouse-audit

Focusing on the PWA audit, if we have the "PWA Badge" displayed, it means there are no failing points. In that case we made a good job and deserve a nice cup of coffee ☕!!

pwa-audit

The Lighthouse PWA Audits follow the PWA Check List we mentioned above.

Bonus link

A final little gift 🎁 for having reached the end of the article! 🎉

Have a look at pwa.rocks web site, where you can find a collection of PWAs examples. Some of them might inspire you 💡!
 
See you at the next article!!

 

You can follow me on:
 
follow-me-twitter

💖 💪 🙅 🚩
paco_ita
Francesco Leardini

Posted on July 7, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related