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 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.
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:
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.
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:
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:
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.
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!! 😀
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.
You can find the tool documentation here 📔
Workbox
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`);
}
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
);
- Regular expressions
workbox.routing.registerRoute(
// Matches image files
/\.(?:png|gif|jpg|jpeg|svg)$/,
handlerFn
);
- 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
);
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',
})
);
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
}),
],
}),
);
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
})
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
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
To verify everything went good, digit ng help
in the console and you should see all the available commands:
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:
Good! Now we are set and ready to start.
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
We can notice that different things have been updated in our project
Let's start analysing the updated files first.
angular.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.
index.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.
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]
})
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"
}
]
}
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)"
]
}
}
]
}
$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:
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/**"
]
}
-
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
andlazy
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"
}
}]
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
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":
Accessing the URL proposed with the web server, you should be able to see our Angular project with the following layout:
Open the DevTools (F12 in Chrome) and in the Application tab we have our service worker installed:
The DevTools network tab shows us the caching strategies in action:
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?
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):
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:
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 ☕!!
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!!
Top comments (2)
excelente articulo !!
Awesome! Workbox helped me. Thanks.