Heads up… this post is old!
For an updated version of this post, see Build Your First Progressive Web Application with Angular and Spring Boot on the Okta developer blog.
Progressive Web Applications are the new hotness in web development. If you search for information on Progressive Web Applications (PWAs), you’ll find that most of the information comes from Google and its employees. This isn’t surprising as Google is often promoting the latest and greatest web technologies. They also develop their own browser, Chrome, which makes it possible to use many of the new standards in HTML5, JavaScript and beyond.
Progressive Web Applications are those that leverage Transport Layer Security (TLS), webapp manifests and service workers to make an application installable with offline capabilities. In other words, a PWA is like a native app on your phone, but it’s built with web technologies like HTML5, JavaScript, and CSS3. If built right, a PWA is indistinguishable from a native application.
You might ask, why is this important? Alex Russell recently stated “we’ve failed on mobile” when talking about Adapting to the Mobile Present. In his talk, he mentions a DoubleClick report that found 53% of visits are abandoned if a mobile site takes more than 3 seconds to load. That same report said the average mobile sites load in 19 seconds. Russell says one of the biggest problems is developers use powerful laptops and desktops to develop their mobile applications, rather than using a $200 device on a 3G connection. Using this environment is “ground truth” the majority of web users in the world. It’s cool to develop native applications, but people with slow phones and internet don’t want to download a 60MB app, they just want to use the web. PWAs are one of the easiest ways to make web applications faster and easier to use, allowing developers to build a better internet for everyone.
To fight back and build better mobile apps, Russell recommends five techniques:
- Implement the PRPL pattern
- Get a ~$150-200 unlocked Android (e.g. Moto G4)
- Use chrome://inspect && chrome://inspect?tracing
- Install Lighthouse and use it to analyze your applications
- Use Chrome DevTools Network and CPU Throttling
Google engineer Addy Osmani describes the PRPL pattern in an article on Google’s Web Fundamentals site.
PRPL is a pattern for structuring and serving Progressive Web Apps (PWAs), with an emphasis on the performance of app delivery and launch. It stands for:
- Push critical resources for the initial URL route.
- Render initial route.
- Pre-cache remaining routes.
- Lazy-load and create remaining routes on demand.
The Progressive Web App Checklist lists all the things you’ll need to make a progressive webapp. However, I like the simple list that Alex Russell lists on What, Exactly, Makes Something A Progressive Web App?.
This article will show you how to build a PWA with a Spring Boot backend and an Angular frontend. It’ll use Stormpath to secure everything and I’ll show how to deploy it to the cloud.
Run a Spring Boot API
In part 1 of this series, I showed you how to create an API with Spring Boot and lock it down with Stormpath. We’ll be using the same project in this tutorial, but adding offline capabilities by turning it into a PWA.
To begin, clone the project from GitHub and checkout the pwa-start
branch.
1 2 |
git clone https://github.com/stormpath/stormpath-spring-boot-angular-pwa-example.git git checkout pwa-start |
If you don’t have a Stormpath account, you’ll need to create one.
The Stormpath Spring Boot Quickstart shows how to create an API key; here’s the abridged version:
From the Home tab of the Admin Console select Manage API Keys under the Developer Tools heading. Click the Create API Key button to trigger a download of a apiKey-{API_KEY}.properties file. Move the file to ~/.stormpath/apiKey.properties
.
Open the “server” project in your favorite IDE and run DemoApplication
or start it from the command line using ./mvnw spring-boot:run
.
Re-build your application and navigate to http://localhost:8080/good-beers. You should be prompted to login. After entering valid credentials, you should see the list of good beers in your browser.
You can also see the result in your terminal window by using basic authentication and HTTPie.
1 |
http localhost:8080/good-beers --auth yourusername:yourpassword |
Progressive Web Apps with Angular
I started my PWAs learning journey while sitting in a conference session with Josh Crowther at The Rich Web Experience. His Progressive Web Apps: The Future of the Web presentation taught me everything I needed to know to get started. However, his examples used Polymer and I wanted to create a PWA with Angular.
When I first started researching how to build PWAs with Angular, I found mobile.angular.io.
This website seemed to be exactly what I needed and I was pumped to find a tutorial showing how to build a PWA with Angular CLI. After installing Angular CLI, I tried the tutorial’s recommended first step:
1 |
ng new hello-mobile --mobile |
I was disappointed to find the latest version of Angular CLI (1.0.0-beta.24) does not support the mobile flag.
After searching through the project’s GitHub issues, I found a reference to Maxim Salnikov’s PWA demo app. Maxim created this repo as part of a half-day workshop at ngPoland and the project’s README
said to contact him for workshop instructions. I emailed Maxim and he politely shared his Angular 2 PWA Workshop instructions and slides.
Transform your Angular App to be a PWA
There are a number of steps you need to perform to make the Angular client work offline and be a PWA.
- Add Angular Material
- Create and register a Service Worker
- Create an App Shell
- Add a manifest to make it installable
Add Angular Material
Installing Angular Material is not a necessary step, but it will make the client look much nicer. Make sure you’re in the client
directory, then install it using npm.
1 |
npm install --save @angular/material |
Add MaterialModule
as an import in app.module.ts
:
1 2 3 4 5 6 7 8 |
import { MaterialModule } from '@angular/material'; @NgModule({ ... imports: [ ... MaterialModule ] |
Add Material icons and a theme to styles.css
:
1 2 3 4 5 6 7 |
@import '~https://fonts.googleapis.com/icon?family=Material+Icons'; @import '[email protected]/material/core/theming/prebuilt/deeppurple-amber.css'; body { margin: 0; font-family: Roboto, sans-serif; } |
Change the HTML templates to use Material components. For app.component.html
, you can change the <h1>
to be an <md-toolbar>
and restructure the welcome message so it displays on the right.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<md-toolbar color="primary"> <span>{{title}}</span> </md-toolbar> <sp-authport></sp-authport> <div *ngIf="(user$ | async)"> <span class="pull-right"> Welcome, {{ ( user$ | async ).fullName }}<br> <a href="" (click)="logout(); false">Logout</a> </span> <div> <beer-list></beer-list> </div> </div> |
In beer-list.component.html
, change it to use <md-list>
and its related components.
1 2 3 4 5 6 7 8 9 10 |
<h2>Beer List</h2> <md-list> <md-list-item *ngFor="let b of beers"> <img md-list-avatar src="{{b.giphyUrl}}" alt="{{b.name}}"> <h3 md-line> {{b.name}} </h3> </md-list-item> </md-list> |
After making these changes, the app should look a little better. Below is a screenshot using Chrome’s device toolbar.
To prove that there’s still work to do, you’ll notice that if you toggle offline mode in the Network tab of Chrome’s developer tools, the app does not work.
Create and Register a Service Worker
To create a service worker, start by creating a src/sw.js
file that logs when events have been triggered.
1 2 3 4 5 6 7 8 9 10 11 |
self.addEventListener('install', (e) => { console.log('Service Worker: Installed'); }); self.addEventListener('activate', (e) => { console.log('Service Worker: Active'); }); self.addEventListener('fetch', (e) => { console.log('Service Worker: Fetch'); }); |
Register this service worker by adding the following to index.html
.
1 2 3 4 5 6 7 8 9 |
<script defer> if (navigator.serviceWorker) { navigator.serviceWorker.register('/sw.js').then(() => { console.log('Service worker installed') }, err => { console.error('Service worker error:', err); }); } </script> |
After making these changes, you should see log messages in your console.
You can also go to DevTools > Application > Service Workers to see that it’s been registered.
TIP: To ensure the service worker gets updated with each page refresh, check the “Update on reload” checkbox.
The goal of adding PWA features to a webapp is to make sure it starts fast. To make this possible, you want to make sure all the resources are cached by the service worker and they’re sent to the page without a network request. To make this work, replace the contents of sw.js
with the following file that caches local resources and network requests.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
let log = console.log.bind(console); let err = console.error.bind(console); let version = '1'; let cacheName = 'pwa-client-v' + version; let dataCacheName = 'pwa-client-data-v' + version; let appShellFilesToCache = [ './', './index.html', './inline.bundle.js', './styles.bundle.js', './vendor.bundle.js', './main.bundle.js' ]; self.addEventListener('install', (e) => { e.waitUntil(self.skipWaiting()); log('Service Worker: Installed'); e.waitUntil( caches.open(cacheName).then((cache) => { log('Service Worker: Caching App Shell'); return cache.addAll(appShellFilesToCache); }) ); }); self.addEventListener('activate', (e) => { e.waitUntil(self.clients.claim()); log('Service Worker: Active'); e.waitUntil( caches.keys().then((keyList) => { return Promise.all(keyList.map((key) => { if (key !== cacheName) { log('Service Worker: Removing old cache', key); return caches.delete(key); } })); }) ); }); self.addEventListener('fetch', (e) => { log('Service Worker: Fetch URL ', e.request.url); // Match requests for data and handle them separately e.respondWith( caches.match(e.request.clone()).then((response) => { return response || fetch(e.request.clone()).then((r2) => { return caches.open(dataCacheName).then((cache) => { console.log('Service Worker: Fetched & Cached URL ', e.request.url); cache.put(e.request.url, r2.clone()); return r2.clone(); }); }); }) ); }); |
After making this change and refreshing, you’ll notice that caches are created for both local assets.
And for network requests.
Create an App Shell
Angular App Shell is a project that provides shellRender
and shellNoRender
directives. Using these directives, you can specify which parts of your app are included in an app shell. Start by installing app-shell
into your project.
1 |
npm install @angular/app-shell --save |
Import the AppShellModule
into app.module.ts
and specify it as an import.
1 2 3 4 5 6 7 8 9 |
import { AppShellModule } from '@angular/app-shell'; @NgModule({ ... imports: [ ... AppShellModule.runtime() ], ... |
Modify app.component.html
to use app-shell’s directives.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<md-toolbar color="primary"> <span>{{title}}</span> </md-toolbar> <sp-authport></sp-authport> <div *ngIf="(user$ | async)"> <span class="pull-right"> Welcome, {{ ( user$ | async ).fullName }}<br> <a href="" (click)="logout(); false">Logout</a> </span> <md-progress-bar mode="indeterminate" *shellRender></md-progress-bar> <div *shellNoRender> <beer-list></beer-list> </div> </div> |
Add a manifest to make it installable
The final step to making your app a PWA is to add a manifest that describes the application. This also enables the ability for people to install your app in Chrome as well as on smart phones.
You can use Favicon Generator to generate graphic assets and a manifest.json
file. For an app icons, I searched for “beer icons” and found this one, developed by Freepik. I generated a favicon, changed the generator options to use assets/favicons
for the path, and downloaded the favicon package.
Copy the contents of favicons.zip
to src/assets/favicons
and add the following HTML to the <head>
of index.html.
1 2 3 4 5 6 7 8 |
<link rel="apple-touch-icon" sizes="180x180" href="assets/favicons/apple-touch-icon.png"> <link rel="icon" type="image/png" href="assets/favicons/favicon-32x32.png" sizes="32x32"> <link rel="icon" type="image/png" href="assets/favicons/favicon-16x16.png" sizes="16x16"> <link rel="manifest" href="assets/favicons/manifest.json"> <link rel="mask-icon" href="assets/favicons/safari-pinned-tab.svg" color="#5bbad5"> <link rel="shortcut icon" href="assets/favicons/favicon.ico"> <meta name="msapplication-config" content="assets/favicons/browserconfig.xml"> <meta name="theme-color" content="#ffffff"> |
Create a manifest.webmanifest
in the src
directory and populate it with the contents of assets/favicons/manifest.json
, plus three additional properties: short_name
, background_color
, and start_url
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
{ "name": "Good Beers", "short_name": "Beers", "icons": [ { "src": "\/assets\/favicons\/favicon-32x32.png", "sizes": "32x32", "type": "image\/png" }, { "src": "\/assets\/favicons\/android-chrome-192x192.png", "sizes": "192x192", "type": "image\/png" } ], "background_color": "#CA3627", "theme_color": "#ffffff", "display": "standalone", "start_url": "index.html" } |
You’ll need to add a reference to this file in index.html
.
1 |
<link rel="manifest" href="manifest.webmanifest"> |
If you refresh your app and Chrome doesn’t prompt you to install the app, you probably need to turn on a couple of features. Copy and paste the following URLs into Chrome to enable each feature.
- chrome://flags/#bypass-app-banner-engagement-checks
- chrome://flags/#enable-add-to-shelf
After making these changes, you should see a prompt at the top of the screen to install the app.
If you open Chrome developer tools > Network and enable offline, you’ll notice the app still loads when the user is offline. Yippee!
Test with Lighthouse
Install the Lighthouse extension for Chrome and click its icon to audit your app.
79 is a pretty good score, but you might notice that the app is not served over HTTPS. Deploying the app to a cloud provider can make this possible.
Deploy to the cloud!
There are many cloud providers that support Spring Boot. For the Angular client, any web server will do since the app is full of static files after it’s been built. However, there are a couple things you’ll need to do to prepare the application for production.
First, modify .angular-cli.json
to include the service worker and manifest files.
1 2 3 4 5 6 7 8 9 10 11 12 |
{ ... "apps": [ { "root": "src", "outDir": "dist", "assets": [ "assets" "sw.js", "manifest.webmanifest" ], ... |
In the client
directory, run the following command to build and optimize for production.
1 |
ng build --prod --aot |
Since you hard-coded the paths to local files in sw.js
, you’ll need to update the paths to match the generated file names. Open dist/sw.js
and change the *.bundle.js
references in the appShellFilesToCache
array to match the file names in the dist
directory. Note that styles.bundle.js
file reference should be changed to style.*.bundle.css
. Later, I’ll show you how to automate this with a script.
You’ll also need to modify the Stormpath configuration to use a different endpointPrefix
for production.
1 2 3 4 5 6 7 8 9 |
export function stormpathConfig(): StormpathConfiguration { let spConfig: StormpathConfiguration = new StormpathConfiguration(); if (environment.production) { spConfig.endpointPrefix = 'https://pwa-server.cfapps.io'; } else { spConfig.endpointPrefix = 'http://localhost:8080'; } return spConfig; } |
Cloud Foundry
Now, let’s look at how to deploy it on Cloud Foundry with Pivotal Web Services. The instructions below assume you have an account and have logged in (using cf login
).
Deploy the client
To deploy a static application to Cloud Foundry is very easy. In the dist
directory, create an empty Staticfile
.
1 |
touch Staticfile |
Run the following commands to push the client, set it to force HTTPs, then start it.
NOTE: You may have to change the name from “pwa-client” to a unique name that’s not being used.
1 2 3 |
cf push pwa-client --no-start cf set-env pwa-client FORCE_HTTPS true cf start pwa-client |
Navigate to the deployed application in your browser (e.g. https://pwa-client.cfapps.io) and ensure it loads. If it does, you’ll likely have a 404 in its console for when it tries to access the server.
Deploy the server
To deploy the Spring Boot backend, you first need to add CORS configuration for the new client. In application.properties
, add the location of the deployed client.
1 |
stormpath.web.cors.allowed.originUris = http://localhost:4200,https://pwa-client.cfapps.io |
Next, build the app, set some environment variables for Stormpath, then start it. If you run the following commands from the server
directory, all of this should happen for you.
1 2 3 4 5 |
mvn clean package cf push -p target/*jar pwa-server --no-start cf set-env pwa-server STORMPATH_API_KEY_ID <your-api-key-id> cf set-env pwa-server STORMPATH_API_KEY_SECRET <your-api-key-secret> cf start pwa-server |
After deploying to Pivotal’s Cloud Foundry, I ran a Lighthouse audit again and found my score to be 96/100. Not too shabby!
Automation
There were quite a few steps involved to deploy this application and update files for production. For that reason, I wrote a deploy.sh
script that automates everything and uses random domain names for both servers. Kudos to Josh Long for creating it.
Source Code
You can find the source code associated with this article on GitHub. If you find any bugs, please file an issue, or leave a comment on this post. Of course, you can always ping me on Twitter too.
What’s Next?
This article showed you how to develop a Spring Boot backend, and lock it down with Stormpath. You learned how to develop an Angular front end and use Stormpath’s Angular SDK to communicate with the secure backend. You also learned how to create a progressive web application and deploy it to the cloud.
One of the recommendations in the Lighthouse report was to use HTTP/2. In a future post, I’ll write about HTTP/2, what cloud providers support it, and how to deploy to the ones that do.
I’d like to give a big thanks to all the engineers that’ve been developing progressive web apps and documenting how to do it. It’s new and exciting stuff, and may become the best way to write mobile applications in the future. Add in a little best-of-breed authentication tools, like Stormpath, and you’re well on your way to being a bleeding edge developer!