Modern Desktop Browsers and JavaScript becoming more powerful now a days. Due to Corona Pendemic, entire world’s IT Infrstructure put under test, where desktops, tablets become primary need. Material UI transformed the web experience which narrows the gap where OmniChannel experience put as primary need.
Most API’s in the PWA space are built on JavaScript Promises and the Fetch API. This video provides an explanation of these technologies, as well as a brief introduction to ES2015.
We’re going to add @angular/pwa
to our bundle through AngularCLI which does the following:
index.html
file to include a link to the manifest.json
file and adds the meta tags for theme-color
. - Installs icon files to support the installed PWA.ngsw-config.json
, which specifies the caching behaviours and other settings.ng add @angular/pwa
For a PWA to run, we need several extra files. First, we need a manifest.json
, which is the web app manifest: a simple JSON file that tells the browser about your web application and how it should behave when “installed” on the user’s mobile device or desktop.
Notable attributes in this manifest are name
, short_name
, description
, theme_color
, and background_color
. Make sure these are properly entered. The short_name is used as name of your application when added to a home screen, so make sure to add it.
The icons are more important than you think, so make sure you create them in the proposed sizes. If you’re practical, you can use one of the many online fav-icon
generators available. Make sure icons of 192 x 192 pixels and 512 x 512 pixels exist, since these are used by Android to display the “Add to Home Screen” install prompt and used by the Microsoft Store to automatically index and package your app.
If you look in the manifest.json
, you’ll see a node icons. Take note of the sizes there that are automatically generated. My advice is that you make sure you have suitable icons for all these sizes.
{
…
"icons": [
…
{
"src": "path/to/maskable_icon.png",
"sizes": "196x196",
"type": "image/png",
"purpose": "any maskable" // <-- New property value `"maskable"`
}
]
…
}
As stated earlier, the ngsw-config.json
contains the settings on how the Angular-CLI build process will create your service worker. The default config will basically consist of an index attribute, usually pointing to your index.html, and the assetGroups. With this last attribute you can specify which files will utilise a certain install or update strategy.
{
"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)"
]
}
}
]
}
This strategy is to tell the application how it should install the app on the user’s device. By default, the ngsw-worker.js
has set the install strategy (installMode) to prefetch, meaning all files specified will be fetched while it’s caching the current version of the app. This is bandwidth-intensive but ensures resources are available whenever they’re requested, even if the browser is currently offline.
{
"name": "app",
"installMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.html",
"manifest.json",
"/*.css",
"/*.js"
]
}
}
Caching Assets Your assets are configured by default as assetsGroup assets. Again we have the prefetch and lazy strategies available.
{
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**",
"/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
]
}
}
Notice that the installMode is set to lazy while the updateMode is set to prefetch, meaning that assets will be loaded on demand, but if an assets has already been cached it will be pro-actively updated. If you want to add assets from external servers or CDNs (like Google fonts), amend the resources like this:
"resources": {
"files": [
"/assets/**",
"/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
],
"urls": [
"https://fonts.googleapis.com/**"
]
}
Data Groups are different than assets in the sense that they are not packaged with the version of your app. They could, for example, come from an API. They can be cached with two different strategies: freshness and performance. The freshness strategy is useful for resources that change frequently and you don’t want to show outdated data for. This strategy will always try to get a new version of the resource before falling back to the cache.
"dataGroups": [{
"name": "api-freshness",
"urls": [ "https://my.apipage.com/user" ],
"cacheConfig": {
"strategy": "freshness",
"maxSize": 5,
"maxAge": "1h",
"timeout": "3s"
}
}]
This snippet will get data from the /user endpoint and will be cached for at most 1 hour, a maximum of 5 responses and a timeout of 3 seconds. After this it will fallback to the cache. The performance strategy, the default strategy, is more useful for resources that don’t change a lot. It will always show the cached version of a resource if available, but this can obviously lead to staleness of the response.
Angular PWA provides the SWUpdate
service.
available
and activated
.
By subscribing to these events, we now if the cached app is current or outdated.
There is a checkForUpdate
method to check the server if there is an update.
This is required if you want to check for update on your own — even if user is not launching the app.
Then you have the activateUpdate
method that will actually update the current app to the latest.
A good practice of PWA app would be to determine if there is an update and provide an option for user to update. To do this in an angular app, you could write a small service.Forcing Update Activation Strategy
import { Injectable } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';
@Injectable({
providedIn: 'root'
})
export class AppUpdateService {
constructor(private readonly updates: SwUpdate) {
this.updates.available.subscribe(event => {
this.informUpdate();
});
}
informUpdate() {
this.updates.activateUpdate().then(() => document.location.reload());
}
}
app.module.ts
.app.component.ts