Create Progressive Web Apps with Angular and other free tools!
Tags: angular, pwa, beginners, javascript
Published: 2019-07-07 21:36:59

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](https://dev.to/paco_ita/a-gentle-introduction-to-progressive-web-apps-step-1-24da): provided us the background and an overview about the benefits of progressive web apps.

- [Install a PWA](https://dev.to/paco_ita/install-a-pwa-on-the-user-s-device-step-2-27pa): described what a `web app manifest` is and how can we configure it.

- [Caching strategies](https://dev.to/paco_ita/service-workers-and-caching-strategies-explained-step-3-m4f): faced `service workers` (SW) and how we can configure caching strategies to leverage their full potential.
&nbsp; 

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

- [PWA Builder](#builder) 
- [Workbox](#wbox) 
- [Angular PWA demo](#angular) 

<span id="builder">&nbsp;</span>
#PWA Builder
![PWA Builder](https://thepracticaldev.s3.amazonaws.com/i/mgdi9lgup0sf0m7j93v3.png)

[PWA Builder](https://www.pwabuilder.com/) is an open source project from Microsoft ([repo](https://github.com/pwa-builder/PWABuilder-CLI)). 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
&nbsp;

###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](https://thepracticaldev.s3.amazonaws.com/i/ycl3hi4o50g2ges0dgzo.png)

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](https://thepracticaldev.s3.amazonaws.com/i/5avjk3qvepro5i59o9ck.png)

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](https://thepracticaldev.s3.amazonaws.com/i/qvbbizwmw1mn422xj41v.png)

&nbsp;
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](https://thepracticaldev.s3.amazonaws.com/i/369qwdkw24mb0qedpmwk.png)

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](https://thepracticaldev.s3.amazonaws.com/i/ywvv3gyzyb3y8cfoxj5d.png)

### 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](https://dev.to/paco_ita/service-workers-and-caching-strategies-explained-step-3-m4f#stale-strategy) 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](https://thepracticaldev.s3.amazonaws.com/i/o2zpsec316qvbbug78ms.png)
&nbsp;

###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!! :grinning:

![feature-store-page](https://thepracticaldev.s3.amazonaws.com/i/1lvbqdecrnc7owed24jq.png)

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

###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](https://thepracticaldev.s3.amazonaws.com/i/eyr1i8efkwm0bo40nsnz.png)

&nbsp;
You can find the tool documentation [here](https://docs.pwabuilder.com/) :notebook_with_decorative_cover:
&nbsp;

<span id="wbox">&nbsp;</span>
#Workbox
![workbox-logo](https://thepracticaldev.s3.amazonaws.com/i/9nl0mq5vb4xrwflaizfp.png)

Workbox is an open source project from Google (here the [repo](https://github.com/GoogleChrome/workbox)).

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`:

```javascript
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`);
}
```

The `importScripts()` method belongs to the [WorkerGlobalScope](https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope#Methods) 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**
```javascript
workbox.routing.registerRoute(
  // Matches a Request for the myTargetFile.js file
  '/myTargetFile.js',
  handlerFn
);
```

- **Regular expressions**
```javascript
workbox.routing.registerRoute(
// Matches image files
  /\.(?:png|gif|jpg|jpeg|svg)$/,
  handlerFn
);
```

- **Callbacks**
```javascript
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
);
```
&nbsp;

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:

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

    // We can provide a custom name for the cache
    cacheName: 'css-cache',
  })
);
```

The code above caches `.css` files and implements the `StaleWhileRevalidate` strategy. Compared to the code we saw in the [previous post](https://dev.to/paco_ita/service-workers-and-caching-strategies-explained-step-3-m4f#caching-strategy), 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:

```javascript
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
      }),
    ],
  }),
);
```
&nbsp;

### 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:

```json
workbox.setConfig({
  debug: true
})
```

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](https://developers.google.com/web/tools/workbox/modules/) about all the modules currently available.
<span id="angular">&nbsp;</span>

&nbsp;

#Angular
![angular_pwa](https://thepracticaldev.s3.amazonaws.com/i/4wi96mbg4sq7yemmsdh0.png)

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](#pwa) 

For the demo I will work with [Visual Code](https://code.visualstudio.com/), 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
```
To verify everything went good, digit `ng help` in the console and you should see all the available commands:

![ng-cli](https://thepracticaldev.s3.amazonaws.com/i/wfksxm64v4xtjyxqijv4.png)

Let's create a new project:
```
ng new angular-pwa
```

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

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

![angular_boilerplate](https://dev-to-uploads.s3.amazonaws.com/i/g53tp9k1n5ztrt6kpd39.png)

Good! Now we are set and ready to start.

![ready](https://media.giphy.com/media/Lm63QU87HvgVEuTV63/giphy.gif)

<span id="pwa">&nbsp;</span>
###Add PWA capabilities
The [add schematics](https://angular.io/guide/schematics#add-schematics) allows to empower an Angular application with PWA features. Execute the following command in the console:

```
ng add @angular/pwa
```

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

![added-files](https://thepracticaldev.s3.amazonaws.com/i/9ewbipy96svyeuo5dwf6.png)

Let's start analysing the updated files first.

*angular.json*

```json
  "build": {
             ...
           "configurations": {
             "production": {
             
                ...

                "serviceWorker": true,
                "ngswConfigPath": "ngsw-config.json"
               }
             }
            }
```

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. 
&nbsp;

*index.html*
```html
  <link rel="manifest" href="manifest.webmanifest">
  <meta name="theme-color" content="#1976d2">
```

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

*app.module.ts*

The`ServiceWorkerModule` is downloaded and the service worker file (ngsw-worker.js) is registered.

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

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.<br>
>⚠️ **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.
<br>`{ 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.
&nbsp;

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

```json
{
  "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"
    }
  ]
}
```
&nbsp;

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


*ngsw-config.json*

```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)"
        ]
      }
    }
  ]
}
```

`$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](https://dev-to-uploads.s3.amazonaws.com/i/3noi1qlcagg31pewfq56.png)

&nbsp;
`index` property holds the path to the index page, usually index.html.
 
&nbsp;
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.

```json
 "resources": {
        "files": [
          "/favicon.ico",
          "/index.html",
          "/manifest.webmanifest",
          "/*.css",
          "/*.js"
        ],
        "urls": [
          "https://fonts.googleapis.com/**"
        ]
      }
```
&nbsp;

- **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`](https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage), 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):

```json
  "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"
    }
  }]
```

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.
&nbsp;

- **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
&nbsp;

- **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.
&nbsp;

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`](https://icanhazdadjoke.com/api) 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`](https://api.thecatapi.com/v1/images/search) 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](https://github.com/pacoita/angular-pwa-boilerplate), but I won't go in detail here about this part. The PWA demo is also [available online](https://pacoita.github.io/angular-pwa-boilerplate/).
&nbsp;

###Make a PROD build
Now it is time to make a production build with the following command:
```javascript
ng build --prod
```
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](https://thepracticaldev.s3.amazonaws.com/i/enzpljc6zq3yivgf90pz.png)

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

![app-demo](https://dev-to-uploads.s3.amazonaws.com/i/s7j46pkqimw6iip7gwzt.png)

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

![sw](https://thepracticaldev.s3.amazonaws.com/i/gn0g0xsxc5kitvoxmanv.png)

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

![network-tab](https://dev-to-uploads.s3.amazonaws.com/i/ekd71cy2oeyocfg9l8ak.png)

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](https://media.giphy.com/media/l46C52IJdRfvCGyVW/giphy.gif)
&nbsp;

#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.
&nbsp;

###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](https://thepracticaldev.s3.amazonaws.com/i/v59w5rx8momate5enipc.png)

You can find the complete list [here](https://developers.google.com/web/progressive-web-apps/checklist)
&nbsp;

###Lighthouse 

[Lighthouse](https://developers.google.com/web/tools/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](https://dev-to-uploads.s3.amazonaws.com/i/eo412teci1m2568hyzzn.png)

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 :coffee:!!

![pwa-audit](https://dev-to-uploads.s3.amazonaws.com/i/8c2vdqcporj0mtpy8zkn.png)

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


##Bonus link
A final little gift :gift: for having reached the end of the article! :tada:

Have a look at [pwa.rocks](https://pwa.rocks/) web site, where you can find a collection of PWAs examples. Some of them might inspire you :bulb:!
&nbsp;
See you at the next article!!

&nbsp;

You can follow me on:
&nbsp;
<a href="https://twitter.com/paco_ita">![follow-me-twitter](https://thepracticaldev.s3.amazonaws.com/i/fgw3ppch67r8ued10oef.png)</a>