I've been actively working in web development now for about seven years, and in that time the landscape has changed a lot. jQuery ruled the frontend when I started. Native apps were the best way to go for companies wishing to reach their customers on their phones. There were ways to share code between Android apps and iOS apps, but they weren't necessarily great. Over the years, things have changed quite a bit. And one of the things I'm most excited about is the emergence and growing popularity of progressive web apps.
A progressive web app is a web application that implements best practices so as to function similar to a native application or desktop application. In turn, it allows you to write and maintain one codebase while at the same time allowing your users to have the best experience possible. There are a lot of moving parts on progressive web apps, and that naturally causes some amount of complexity. But Angular steps in again to make a somewhat complex task much easier than it would normally be. This article will go over how to turn your Angular app into a PWA and a couple of the data caching techniques that go along with PWAs. This is by no means a comprehensive tutorial, but it should get you started. In the next part I'll go over how to show an update prompt and how to manually show a button to install the application on your device.
Let's get started by generating a new Angular application with the CLI:
PWA Generation
ng new pwa-test
After answering the CLI prompts, you should have a default Angular application. Now, let's go ahead and turn this into a PWA. We will do so by running the following command:
ng add @angular/pwa --project pwa-test
Obviously, replace pwa-test with the name of your project. This command does the following to your project:
- Adds a manifest.json to the root of the project (with information about the PWA)
- Adds a ngsw-config.json to the root of the project (for the service worker)
- Adds some default icons that the app will use when installed
- Updates the app.module.ts file to register the service worker for you
- Updates the angular.json file to add the manifest.json and ngsw-config.json files to the assets list
- Updates the index.htmlto include a link to the manifest.json, a meta tag for theme-color, and a noscript tag
Now, from right here you could move forward and have a PWA application. According to the docs, service workers don't work with the ng serve command. You can use that during development, but you can't test the service worker with it. The other thing I noticed is that in the app.module.ts
file, the service worker is only set up to be enabled in production mode. You could change that, I suppose, but that's what it is set to by default. So with those defaults, you can run the following command to view your application:
ng build --prod && npx http-server -p 8080 -c-1 dist/pwa-test
You don't have to use npx here to run the http-server. You could globally install http-server, or use a different server of your choice.
That command starts up a server on port 8080 with the production built code. You can open that port in your browser and as you watch the network tab, you'll see the service worker's config file, ngsw-config.json, pop up a couple times. The service worker is running! I'll show an example of that a little later.
Asset Groups
Now that we have a working application, let's go back to look at what the default service worker configuration gives us. By default, the service worker is set up to cache a couple of assetGroups. Basically, you can determine which static assets should be cached and how they should be cached. The first group it defines is the following:
{
"name": "app" // give each asset group a unique name
"installMode": "prefetch", // valid selections are prefetch and lazy, default is prefetch
"resources": {
"files": ["/favicon.ico", "/index.html", "/*.css", "/*.js"] // files that should be cached in this group
}
}
That asset group defines the first set of files that should be cached. Basically, it tells the service worker to get the favicon, the index.html file (since that's really all that's needed for HTML files for an Angular app), all CSS files and all JS files and prefetch them. Prefetch means that they are downloaded and cached immediately. One other option, that's not explicitly set here, is updateMode. It determines how these cached files are updated when a new version is available. It defaults to the installMode.
The default config file has a second asset group defined that gets the other static assets that are part of the application. They are lazily loaded and cached.
What all this default information means is that when you make a change to the code, rebuild, and reload the application, you're not going to see immediate changes. I find it's best to do this in an incognito tab, but just always leave at least one tab open so you don't lose your cache. Anyways, make a change to the HTML in your app, and re-run the build and serve command from earlier. When the http-server is ready, open the network tab and refresh the page. You'll notice that immediately the old version of the app loads. Then, in the network tab, you'll see the service worker make a request to get the new files. The update happens in the background! Then, when you refresh the page, you'll see the updated app.
I don't know about you, but when I first did this I was amazed and excited. I couldn't believe how easy it was to get the service worker set up to cache HTML files. If your app is a static application, for perhaps a blog or something like that, you're basically done. People can install your application to their homescreen and have offline access to it. The work needed for this is so minimal...it's incredible, honestly. But most apps need more than just the static files. We make HTTP calls all the time, so let's look at what we can do to cache data from HTTP calls.
Data Groups
By default, the service worker that's configured as part of your application will not cache any calls to get data from an API or something like that. If it's a JS or CSS file, or the index.html, or a file in the assets folder or an image or a font file or something along those lines, it will be cached. But to get data to be cached, we need to create a data group. The Angular docs say the following about data groups:
Unlike asset resources, data requests are not versioned along with the app. They're cached according to manually-configured policies that are more useful for situations such as API requests and other data dependencies.
You can define a data group and how API requests and such are cached by putting an object that looks like the following in an array attribute called dataGroups
:
{
"name": "api-name",
"urls": ["https://my-api-url.com/**"],
"cacheConfig": {
"strategy": "freshness", // performance (cache first only) or freshness (network first, then cache)
"maxSize": 100, // number of entries or responses in a cache; required
"maxAge": "3d", // how long until entries are considered invalid and evicted
"timeout": "10s" // how long to wait for the network until cache is used
}
}
Using this configuration object, you can determine how your project's API is cached. This way your users can still use the application when they are offline or have a bad connection. It's incredible, though, how easy this is again to set up and use. When testing, I decided to use the Star Wars API (swapi.co) to get a list of Star Wars people that I could list in the app. I set up a data group for it, and when I had a network connection it made the call to the API. Then, in the network tab of Chrome I checked the "offline" button and refreshed the app. The HTTP call fails but the Star Wars people still show up because the service worker cached the data.
Conclusion
At this point, you should have a working PWA. (If you deploy to Netlify you could actually install the application on your device.) Files and API calls will be cached. The app will work offline. With very little work or configuration on our part, the PWA is ready. This is another example of why I love working with Angular, and how the Angular team has given us such great tools. They've done a lot of the heavy lifting for us and make it so easy to get up and running and to be efficient.
In part two, coming soon, I'll go over how to show an update prompt and how to manually show a button to install the application on your phone. Keep an eye out for it, and let me know what you think about this article!
Top comments (2)
I'm not sure there is a default value, but here's a link to the docs on maxAge:
angular.io/guide/service-worker-co...
Any idea on the default and maximum values for "maxAge" in dataGroups: cacheConfig?
I've set mine to "9999d" but the data didn't persist after five days of downtime.