PWAs are regular web applications which make extensive use of new browser APIs. These APIs make PWAs:
Users have the impression they use a native app.
is a W3C standard implemented by most browsers
(...) provides information about an application (such as name, author, icon, and description) in a JSON text file. The purpose of the manifest is to install web applications to the home screen of a device
manifest.json
{
"name": "Book Application",
"short_name": "BookApp",
"start_url": "/",
"icons": [
{
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
index.html
<link rel="manifest" href="assets/manifest/manifest.json">
Preparing all assets needed by the Web App Manifest may be time consuming, so let's use one of available online generators: https://realfavicongenerator.net/
As a base picture use assets/capgemini-spade.svg
1-manifest
branch...What is an App Shell?
An application shell is the minimal HTML, CSS, and JavaScript powering a user interface
How to build an App Shell into our app?
We'll make use of Angular Universal and Angular CLI
What does an App Shell have to do with Angular Universal?
We are going to pre-render the AppComponent
at build time using Angular Universal
Let's add an Universal bundle to our app:
ng generate universal ssr-app
npm install
Let's make use of the Angular CLI's App Shell generator:
ng generate app-shell book-app-shell
--universal-app=ssr-app --route=app-shell
Because of this bug comment out the RouterModule
and its import from the app.module.ts
before.
What does
ng generate app-shell
do?
AppShellComponent
/app-shell
route linked to the AppShellComponent
AppShellComponent
and the /app-shell
route only to the Universal bundle (AppServerModule
)appShell
section to angular-cli.json
Now executing
ng build --prod
:
/app-shell
routedist/index.html
AppShellComponent
may (and should) be changed
app-shell.component.html
(<div>
is added and its height is set because of this bug)
<div class="container">
<mat-spinner color="accent" [diameter]="140"></mat-spinner>
</div>
app-shell.component.scss
div.container {
height: 160px;
mat-spinner {
margin: 0 auto;
}
}
app.server.module.ts
@NgModule({
imports: [... MatProgressSpinnerModule]
})
export class AppServerModule {}
We'll add a convenience script for starting the server in production mode
npm install express-history-api-fallback --save-dev
server/server.ts
const fallback = require('express-history-api-fallback');
const root = `${__dirname}/../dist`;
app.use(express.static(root));
app.use(fallback('index.html', {root}));
package.json
"scripts": {
"server:prod": "./node_modules/.bin/npm-run-all --parallel server build"
}
App Shell in action
2-app-shell
branch...A service worker is a type of web worker. It's essentially a JavaScript file that runs separately from the main browser thread, intercepting network requests, caching or retrieving resources from the cache, and delivering push messages.
https://developers.google.com/web/ilt/pwa/introduction-to-service-worker
Add Service Worker to the app (1)
npm install @angular/service-worker
app.module.ts
import {ServiceWorkerModule} from '@angular/service-worker';
@NgModule(
imports: [
ServiceWorkerModule.register('/ngsw-worker.js',
{enabled: environment.production})]
})
export class AppModule {}
angular-cli.json
"apps": [
{
"serviceWorker": true
}
]
Add Service Worker to the app (2)
src/ngsw-config.json
{
"index": "/index.html",
"assetGroups": [
{
"name": "app",
"installMode": "prefetch",
"resources": {
"files": [
"/assets/manifest/favicon.ico",
"/index.html"
],
"versionedFiles": [
"/*.bundle.css",
"/*.bundle.js",
"/*.chunk.js"
]
}
},
{
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**"
]
}
},
{
"name": "fonts",
"resources": {
"urls": [
"https://fonts.googleapis.com/**",
"https://fonts.gstatic.com/**",
"http://covers.openlibrary.org/**"
]
}
}
],
"dataGroups": [
{
"name": "api-freshness",
"urls": [
"/api/book"
],
"cacheConfig": {
"strategy": "freshness",
"maxSize": 100,
"maxAge": "3d",
"timeout": "10s"
}
}
]
}
For details on configuration please refer to Angular Guide
Now executing
ng build --prod
adds:
/dist/ngsw-worker.js
which is in fact the Service Worker/dist/ngsw.json
used to check which files should be cached, which expired, etc.Service Worker in action
3-service-worker
branch...Web Push Notifications are in fact based on two browser standards:
We can't push notifications from our server directly. Instead, a Push Service specific to a browser is to be used.
https://developers.google.com/web/fundamentals/push-notifications/
Therefore we are going to generate a VAPID (Voluntary Application Server Identification) key pair for that.
We make use of the web-push library:
npm install web-push
Generating VAPID keys for our server:
node_modules\.bin\web-push generate-vapid-keys --json
Subscribing to Push Notifications
sw-push-sub-btn.component.ts
(full example)
import {SwPush} from '@angular/service-worker';
@Component(...)
export class SwPushSubBtnComponent {
private readonly VAPID_PUBLIC_KEY = 'BK8...';
constructor(private swPush: SwPush) {}
subscribe() {
this.swPush.requestSubscription({
serverPublicKey: this.VAPID_PUBLIC_KEY
})
.then(subscription => {
// send the subscription to our server
})
.catch(err => {
console.error('swPush.requestSubscription() failed', err);
});
}
}
Send a Push Message
server.ts
(full example)
import * as webPush from 'web-push';
const vapidKeys = {
publicKey: 'BK8...',
privateKey: '77ne...'
};
webPush.setVapidDetails(
'https://github.com/devonfw-ng-adv-training',
vapidKeys.publicKey,
vapidKeys.privateKey
);
app.post('/api/message', (req, res) => {
Promise.all(subscriptions.map(sub => webPush.sendNotification(
sub, JSON.stringify({
notification: {
title: 'Hello PWA User!',
body: 'This is a Push Notification!',
icon: 'assets/manifest/android-chrome-192x192.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
}
}
}))))
.then(() => res.sendStatus(200))
.catch(err => {
console.error('Error sending notification, reason: ', err);
res.sendStatus(500);
});
}
);
Push Notifications in action
4-push-notifications
branch...