Building PWAs with Angular 5
Introduction
As many studies have shown, the number of mobile users has increased tremendously. In countries like Indonesia, over 90% of the traffic is generated by "mobile" users.
The web technology of 2007 was not ready for mobile formats, so we had to make websites responsive and think “mobile first."
Besides adapting content, we have to face the challenge of unreliable network connections. This happens even in the most modern cities with advanced mobile networks.
Therefore, we can no longer build applications with the desktop mindset where fast and reliable connections are common. We have to start thinking "offline first."
To overcome this challenge, engineers and designers came up with Progressive Web Apps or PWAs.
Progressive web apps (PWAs) are web applications that are regular web pages or websites, but can appear to the user like traditional applications or native mobile applications.
— Wikipedia
It's not my intention to go deeply into the definition of PWAs, because you can already find other articles and conferences that talk about it.
However, I would like to highlight that PWAs are not made of one specific technology but a set of different new web browser technologies.
In this article, I would like to guide you through some of those technologies, how they improve your software solutions, and how to implement them in Angular.
Our demo app
Let’s go through those challenges through a demo app.
Don't worry, it's not another TODO list 😉
JS Magazine
Our demo app is a JavaScript newsletter. The "news" in this app are the latest Angular release notes.
The functionality is simple:
- The home page is a list of news. As you click on a row, it opens the specific article.
- In the news view, you can read the full story.
- Press the header or the back link to navigate back to the home page.
Feel free to clone this app from GitHub or check out the live demo.
First challenge: survive the offline status
Imagine a mobile user is reading JS Magazine on the way to work. Once at the metro station, this is all the user sees:
...nothing! not even a spinner or an error message.
Your app identity is gone. This is really bad because the worst content you can leave your users with is nothing.
Service Workers to the rescue!
Luckily, we have the right tool to solve this issue: Service Workers.
- Are event-driven Web Workers registered against an origin and a path.
- Can intercept and modify navigation and resource requests.
- Run in a different thread, so they are non-blocking.
- Are fully async.
- Have access to storage technologies like IndexedDB.
Consider them like a proxy server installed on the user device that is able to work offline.
You should consider:
- For security reasons, they only work over HTTPS.
- They have no access to the DOM.
- They have no access to any synchronous API (synchronous XHR, localStorage, ...).
- To avoid user-tracking, they are not available on Firefox private mode browsing.
Browser support is already quite good 👏.
Service Workers support in Angular
With the release of Angular 5, the use of Service Workers has become easy. You can implement them from scratch using Angular CLI (from version 1.6).
To create the JS Magazine app, I simply used the Angular CLI command:
ng new js-magazine --service-worker
The --service-worker flag indicates to the CLI to use Service Workers in our project.
You can also add Service Worker support to an existing app by following these steps.
To know if an existing app has the Service Worker functionality already enabled, open the configuration file .angular.cli.json and find the configuration attribute:
"serviceWorker":true
What do we get out of the box?
Let's take a look at the result of using the default configuration.
Angular enables Service Workers by default only for the production environment:
ng build --prod
This will build our app and store all of the built assets in the dist/ directory.
In the imports of the app.module.ts file, you can customize under which environments Service Workers are enabled:
@NgModule({
declarations: [
AppComponent,
HeaderComponent,
],
imports: [
BrowserModule,
ServiceWorkerModule.register('/ngsw-worker.js', { enabled: environment.production }),
RoutingModule,
NewsListModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
The next step is to serve the generated assets. I recommend serving those assets in a different port than the default (4200) so the Service Worker won't interfere with old cached assets.
An easy way to serve those assets is using http-server.
If you don't have it installed yet, simply run:
npm install http-server -g
After the installation, go to the dist folder and run the command:
http-server -c-1
This command starts a http server with the dist/ assets on the port 8080.
The -c-1 option tells the http-server to disable caching. This is important because you want to be sure the Service Worker and not the HTTP Cache stores all of the required assets. Opening http://localhost:8080, we should see the app as it looked before.
To simulate an offline situation, let's stop the http-server process (press Ctrl C at the terminal). Reloading the browser, we should see the app with no news and slightly different:
The online version at the left and the offline at the right
What we have is already much better. We show our app and inform the user that when offline, the app can not reach the news list.
Take a close look and you can spot more differences: when offline, the browser can not load web-fonts coming from other domains/CDN. Instead, it's using the default fonts. As a result, the title of the header looks different, unfamiliar.
Know the tools
Before going further fixing issues, I would like to make sure you are familiar with the available developer tools.
As of publication time, the browser with more tools to debug and test Service Workers is Chrome.
This is the Network pane when our app is offline:
At the top, you can see that I disabled the browser cache and asked the browser to simulate an instance where there is no internet connection.
You can also see a list of the loaded assets. If an asset fails to load, the whole row is red.
Pay attention to the Size column. When the size in bytes is not displayed, it means that the browser took this resource from one of its caching mechanisms and all of the resources that didn't fail are labeled as (from ServiceWorker).
This pane helps you discover requested resources that are not cached by your app.
Another key tool to understand what's happening is the Application pane:
In the Service Workers section, you see the Service Workers installed in the browser, its status, and a direct access to its source code.
While developing, it's handy to:
- Know how much storage space your app is taking.
- Have a button to clear all of the different cache technologies.
In those cases, the clear storage section is the place to go.
If you want to explore the content of any specific cache technology, you can do it in their sections.
For example, in the Cache Storage section you can see and modify some of the app cached assets.
The last tool I want to talk about is the oldest Chrome tool regarding Service Workers. If you type the URL chrome://serviceworker-internals/, then you will see a list of all of the registered Service Workers in your browser and their state. This is useful when the Chrome developer tools stop responding.
How to configure the Angular Service Worker
Next, we are going to cache the fonts with our App Shell adding font URLs in the ngsw-config.json:
{
"index": "/index.html",
"assetGroups": [
{
"name": "js-magazine-app-shell",
"installMode": "prefetch",
"updateMode": "prefetch",
"resources": {
"files": [
"/index.html"
],
"versionedFiles": [
"/*.bundle.css",
"/*.bundle.js",
"/*.chunk.js"
],
"urls": [
"https://fonts.googleapis.com/css?family=Open+Sans:400,700|Playfair+Display+SC",
"https://fonts.gstatic.com/**"
]
}
},
{
"name": "favicon",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/favicon/**"
]
}
}
]
}
This file tells our Service Worker which resources we want to cache and which strategy to use.
For our app, there are two asset groups, our app-shell, and the favicon assets. Each asset group should have a unique name.
Angular offers two cache strategies:
- Prefetch: used when the resources are essential for the app to work and it must be cached immediately.
- Lazy: cache on demand. The resources are only cached when requested.
These strategies are used to set up two important events:
- installMode: what should happen when the Service Worker is installed and sets the cache up for the first time.
- updateMode: what should happen when there is a new version of this asset group.
We can read the settings for the favicon as: cache only the favicon assets that the browser requests. If there is a new version, then update the cache immediately.
After updating ngsw-config.json, building the app and simulating Offline status, this is what we get:
Our app shell is now cached correctly.
Mission accomplished 😎 !
Besides being available offline, the use of Service Workers drastically improves the load time and reduces the traffic load of your servers because, after the first load, all of the cached assets won't need to be resent.
Second challenge: make the app installable.
The official Google checklist to enable apps is quite long, but installable, and boils down to:
- The app is served via https.
- The content is responsive.
- Offline support using Service Workers.
- Detailed app information using Web App Manifest.
Currently our app only lacks the Web App Manifest. It's a simple JSON file where developers can define:
- How the app looks when loading.
- How the app looks when displayed on the home screen.
- Theme color.
- Default language.
- I encourage you to check the docs some more 😉.
First step: create the manifest file.
The manifest file for our app looks like this:
{
"short_name": "JsMagazine",
"name": "JS Magazine",
"lang": "en",
"description": "Magazine covering the latest JavaScript news.",
"background_color": "#F7DF1E",
"theme_color": "#F7DF1E",
"icons": [
{
"src": "/assets/favicon/favicon-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
... some more popular icon resolutions are defined here.
{
"src": "/assets/favicon/favicon-16x16.png",
"sizes": "16x16",
"type": "image/png"
}
],
"start_url": "/",
"display": "standalone"
}
Shortly, we will discuss all of the parameters in a visual way.
Second step, reference it.
We need to include a reference to the manifest in the head section of our index.html. Also, we are going to add some extra meta tags to tell the browser this page is an app, is mobile capable, and the theme color:
<head>
...
<!-- App name -->
<title>JsMagazine</title>
<link rel="manifest" href="./assets/manifest.json">
<meta name="application-name" content="JsMagazine"/>
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="msapplication-TileColor" content="#f7df1e" />
<meta name="msapplication-starturl" content="/">
<meta name="theme-color" content="#f7df1e">
...
</head>
Don't forget to include the manifest.json in the app shell resources so it will be cached by the Service Worker.
ngsw-config.json:
...
{
"name": "js-magazine-app-shell",
"installMode": "prefetch",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/manifest.json",
"/index.html"
],
"versionedFiles": [
"/*.bundle.css",
"/*.bundle.js",
"/*.chunk.js"
],
"urls": [
"https://fonts.googleapis.com/css?family=Open+Sans:400,700|Playfair+Display+SC",
"https://fonts.gstatic.com/**"
]
}
},
...
Third step, basic testing.
Let's make sure all of the linked files and settings are working. Don't forget to first build the application for the production environment.
A quick way to test our App Manifest is with the developer tools.
Go to the Application tab, Manifest section:
All of the parameters are working:
- App identity: name and short name
- Colors of the app.
- How to display the app (standalone).
- All the icons of our app.
Fourth step, test it on a device:
Let's explore the manifest.json parameters on an Android mobile device. If you don't have an Android device, you can download Android Studio and use the built-in emulator.
First, open the app on Chrome:
Do some smoke testing for the app and make sure everything works.
Then press the more options button (top right), you should see a menu:
Press the Add to Home screen option, you should see the following popup:
This is going to be the name of your app on the home screen. This name is pre-filled with the short name attribute of your manifest.json.
You can also see the favicon of the app, specified in the icons list of the manifest.json. By default, Android use the 192x192 pixel icons. Press ADD.
The app should then be on the home screen:
Pay attention to the splash screen:
- The Android header is not black anymore. It's a shade of the theme color attribute.
- The app icon is displayed at the center.
- Below the app icon the Name attribute is displayed.
After the splash screen, the app loads. Let's compare the browser version with the app version:
The browser bar disappeared. This is because in the manifest.json we set the Display attribute to standalone.
Wrapping up
At this point, our app is quite close to a native app but there is much more we can implement to improve the user experience. In following articles, I am going to discuss how to interact with the Angular Service Worker, create your Responsive Service and much more, so stay tuned 😉.
It would be more interesting to move that app on Cordova.
Hello Vijay,
Moving the app to Cordova can have benefits if you target only mobile users.
PWAs allow you to build software solutions that run on any browser: mobile, desktop, smart TV.
I didn’t cover this topic in the article but there’s a lot of debate about Native apps and PWAs and when is a good idea to build one:
https://medium.com/dev-channel/why-progressive-web-apps-vs-native-is-the-wrong-question-to-ask-fb8555addcbb
https://appinstitute.com/pwa-vs-native-apps/
Thanks for the post. How did you get your app on the phone in the fourth step ?
Hello Dave,
Thank you for reading :)
You can get your app on the phone using the “Add to Home screen” functionality on Android.
The basic conditions for Chrome to display this option on the menu are:
You can find more information about this topic on the Google developers site:
https://developers.google.com/web/fundamentals/app-install-banners/
https://developers.google.com/web/updates/2017/02/improved-add-to-home-screen
Hope this helps you.
Thanks for the reply. I am wondering are you actually hosting the app on your network at home (192.168…) as opposed to actually on the internet? And then somehow getting on your phone?
Hello Dave,
Good point, this happens in the process of developing and testing the app.
While developing is usual to serve the app on your computer (localhost or 127.0.0.1) so you can test it on your browser.
When you do this using http-server it actually makes the app available on your local network.
This means that any device connected to the same local network can point to the IP address & port of your computer and try out the app.
If you want to do the same with Angular CLI you need to specify an extra parameter ‘host’ while serving it:
The screenshots for this article are taken using the Android Emulator (from Android Studio) as I don’t have an Android device myself.
For “production” you just serve your app under https, you can find a live demo of the app here:
https://nonsensedevelopment.com/js-magazine/