Sunteți pe pagina 1din 50

Angular 2 Progressive Web Applications

Workshop

Maxim Salnikov
Angular GDE
@webmaxru
salnikov@gmail.com
Contents
Introduction to PWA

Setting up the environment

Step 1: Creating Angular 2 application


Cloning the existing repo
If something went wrong

Step 2: Adding Angular Material 2


Installing dependencies
Adding styles to index.html
Adding imports to App Module and Places Module
Styling toolbar
Designing a list of places
If something went wrong

Step 3: Creating and registering a Service Worker


Creating and registering a SW
Adding events (Install, Activate, Fetch)
Forcing SW to skip waiting the update
If something went wrong

Step 4: Creating a App Shell


Going offline
Angular Mobile Toolkit App Shell
Installation
Updating a SW
If something went wrong

Step 6: Using Angular Basic Service Worker


Installation
More tools for easier development of offline experiences
If something went wrong

Step 7: Add to home screen and splash screens


Creating manifest file
Debugging and setting up your browser
Controlling the Add to Home Screen banner
Adding a Splash Screen to your app
If something went wrong

Step 8: Push Notifications


Setting up the credentials
Setting up the backend
Setting up the client
Receiving the push message in your service worker
If something went wrong

Step 9: Testing and deploying


Installing and running Lighthouse
Deploying using Surge

Useful resources
Standards and APIs
Cookbooks and guides
Collections
Personal experiences
Credits
Introduction to PWA
Slides
Setting up the environment
We need (latest versions):
Git
Node
NPM
Yarn
Chrome 52 or above
Firefox Developer Edition (latest)

You can use your favorite editor:


WebStorm
Atom
VisualStudio Code
etc
Step 1: Creating Angular 2 application

Cloning the existing repo


Unfortunately we cant use Angular CLI for the moment - the Mobile Toolkit integration is
temporary disabled and all the current examples are not based on WebPack (used for CLI).
Lets take the project from GitHub repo. It contains some more files - test images and JSON
data well work with:

git clone https://github.com/webmaxru/pwa-guide-ngpoland


cd pwa-guide-ngpoland
git checkout step-init
npm install -g yarn
yarn install
gulp build-noshell
gulp serve

Open http://localhost:8080/
https://code.facebook.com/posts/1840075619545360
https://angular.io/docs/ts/latest/cookbook/aot-compiler.html
Result:
If something went wrong

git checkout step-init


Step 2: Adding Angular Material 2

Installing dependencies

npm install --save @angular/material

Marked red = no need to do it! (We have everything installed)

Docs:
https://github.com/angular/material2/blob/master/GETTING_STARTED.md
Adding styles to index.html
<style>
body {
margin: 0;
}

</style>
<link href="/indigo-pink.css" rel="stylesheet">

Adding imports to App Module and Places Module

import {MaterialModule} from '@angular/material';

imports: [
...
import {MaterialModule} from '@angular/material';
],
Styling toolbar
File src/root.html

<md-toolbar color="primary">
<span>{{title}}</span>
</md-toolbar>

<place-list></place-list>

Docs:
https://github.com/angular/material2/blob/master/src/lib/toolbar/README.md
Designing a list of places

File src/places/place-list.html

<md-list>
<md-list-item *ngFor="let place of places">
<img md-list-avatar src="/assets/img/{{place.imgName}}"
alt="{{place.title}}">
<h3 md-line>
{{place.title}}
</h3>
<p md-line class="line-secondary">
{{place.description}}
</p>
<ul md-line class="list-tags">
<li *ngFor="let tag of place.tags">
{{tag}}
</li>
</ul>
</md-list-item>
</md-list>

Docs:
https://github.com/angular/material2/blob/master/src/lib/list/README.md

Result:
If something went wrong

git checkout step-material


Step 3: Creating and registering a Service Worker

Creating and registering a SW


A service worker is an event-driven worker registered against an origin and a path. It takes the
form of a JavaScript file that can control the web page/site it is associated with, intercepting and
modifying navigation and resource requests, and caching resources in a very granular fashion
to give you complete control over how your app behaves in certain situations (the most obvious
one being when the network is not available.)

Create an empty file src/sw.js

Register it by adding to src/index.html:

<script defer>
if (navigator.serviceWorker) {
navigator.serviceWorker.register('/sw.js').then(() => {
console.log('Service worker installed')
}, err => {
console.error('Service worker error:', err);
});
}
</script>

You can call register() every time a page loads without concern; the browser will figure out if the
service worker is already registered or not and handle it accordingly.

Docs:
https://developers.google.com/web/fundamentals/primers/service-worker/register?hl=en

Verify that this has worked by visiting Chrome Dev Tools (right-click > Inspect Element
or F12)
In latest Chrome: Application -> Service Workers
In earlier versions: go to Resources panel and look for Service Workers in the
left-hand panel
In versions of Chrome <48, visit chrome://serviceworker-internals to understand
the current state of the system
In both cases, visit the http://localhost:8080/ and observe that you now have a
Service Worker in the Active state
To verify this in Firefox Dev Tools, use Firefox Developer Edition
Open a new tab and visit about:debugging#workers or about:#serviceworkers
and ensure that a worker appears for your URL
If it isnt there, it may have been killed so try refreshing the source document

Result:
Adding events (Install, Activate, Fetch)

To build your application, youll need to ensure that youre handling the important events for
your application. Let's start by logging out the various parts of the lifecycle.

File src/sw.js:

var log = console.log.bind(console);


var err = console.error.bind(console);

self.addEventListener('install', (e) => {


log('Service Worker: Installed');
});

self.addEventListener('activate', (e) => {


log('Service Worker: Active');
});

self.addEventListener('fetch', (e) => {


log('Service Worker: Fetch');
});

In Chrome you can use the Application -> Service Workers panel in DevTools to remove previous
versions of the Service Worker, or use Shift-Ctrl-R to refresh your application. In Firefox
developer edition you can use the controls in about:serviceworkers to accomplish the same
thing.

Once the new Service Worker become active, you should see logs in the DevTools console
confirm that. Refreshing the app again should show logs for each request from the application.

Tip: during development, to ensure the SW gets updated with each page refresh, go to Application
-> Service Workers, and check Update on reload.

Result:
Forcing SW to skip waiting the update
Normally when you modify your service worker code, the currently open tabs continue to be
controlled by the old service worker. The browser will only swap to using the new service worker
at the point where no tabs (which we call clients in this context) are still open.

To avoid this, you can use a set of features: skipWaiting and Clients.claim().

This code will prevent the service worker from waiting until all of the currently open tabs on your
site are closed before it becomes active:

File src/sw.js:

self.addEventListener('install', (e) => {


e.waitUntil(self.skipWaiting());
log('Service Worker: Installed');
});

And this code will cause the new service worker to take over responsibility for the still open
pages.

self.addEventListener('activate', (e) => {


e.waitUntil(self.clients.claim());
log('Service Worker: Active');
});
If something went wrong

git checkout step-sw


Step 4: Creating a App Shell

Going offline
Run
git checkout step-app-shell

The goal of a Progressive Web App is to ensure that our app starts fast and stays fast.

To accomplish this we want all resources for our Application to boot to be cached by the Service
Worker and ensure theyre sent to the page without hitting the network on subsequent visits.

Service Workers are very manual. They dont provide any automation for accomplishing this
goal, but they do provide a way for us to accomplish it ourselves.

Let's replace our dummy Service Worker with something more useful. Edit src/sw.js:

var version = '4';


var cacheName = 'pwa-ngpoland-guide-v' + version;
var dataCacheName = 'pwa-ngpoland-guide-data-v' + version;
var appShellFilesToCache = [
'./',
'./index.html',
'./app.js',
'./indigo-pink.css',
'./Reflect.js',
'./zone.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);
})
);
});
Result:

This code is relatively self explanatory, and adds the files listed above to the local cache. If
something is unclear about it, ask and we can talk through it.

Next, lets make sure that each time a new service worker is activated that we clear the old
cache. You can see here that we clear any caches which have a name that doesnt match the
current version set at the top of our service worker file. Edit src/sw.js:

var version = '2';


...
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);
}

}));
})
);

});
Ok, we have our apps UI in cache. How could we use this to allow offline access to our app?
Were going to ensure that we hand back resources we know are in the cache when we get an
onfetch event inside the Service Worker. Add the following to your Service Worker:

self.addEventListener('fetch', (e) => {


log('Service Worker: Fetch');

e.respondWith(
caches.match(e.request).then((response) => {

if (response) {
log('Service Worker: returning ' + e.request.url + ' from cache');
return response;
} else {
log('Service Worker: returning ' + e.request.url + ' from net');
return fetch(e.request);
}

// w/o debug info: return response || fetch(e.request);

})
);

});

Result:

A few things to note about this code:

First, we respondWith() a Promise, in this case the result of an operation that searches
all the caches for a matching resource for the request we were sent. To search a specific
cache, caches.match() takes an optional parameter.
Next, when we dont get a response from the cache, it still uses the success chain in the
Promise. This is because the underlying storage didnt have an error, it just didnt have
the result we were looking for.
To handle this case, we check to see if the response is a truthy object
Lastly, if we dont get a response from the cache, we use the fetch() method to check for
one from the network
Weve just witnessed something new: were always responding from the local cache for
resources we know we have. This is the basis for Application Shells!

Angular Mobile Toolkit App Shell

https://www.npmjs.com/package/@angular/app-shell

Installation
npm install @angular/app-shell --save

App Shell will provide handy utilites for specifying parts of your app to be included into an App
Shell. Contains directives:

shellRender
shellNoRender

AppShellModule can be installed as AppShellModule.prerender() or AppShellModule.runtime()

Edit src/root.html:

<md-progress-bar mode="indeterminate" *shellRender></md-progress-bar>

<div *shellNoRender>

<place-list></place-list>

</div>

Updating a SW
In addition to the shell, we also will want to cache data. To do this we can add a new cache
name at the top of sw.js, and then whenever a fetch for data is made, we add the response into
the cache.

var dataCacheName = 'pwa-ng-poland-guide-data-v' + version;

Now, in the fetch handler, well handle data separately (you can replace your old fetch handler
with this). File src/sw.js:

self.addEventListener('fetch', (e) => {


log('Service Worker: Fetch URL ', e.request.url);

// Match requests for data and handle them separately


if (e.request.url.indexOf('data/') != -1) {

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();
});
});
})

);

} else {

// The code for App Shell


e.respondWith(
caches.match(e.request).then((response) => {
return response || fetch(e.request);
})
);

}
});
Or we could use a network-first variant:

self.addEventListener('fetch', (e) => {


log('Service Worker: Fetch URL ', e.request.url);

// Match requests for data and handle them separately


if (e.request.url.indexOf('data/') != -1) {

e.respondWith(

fetch(e.request)
.then((response) => {
return caches.open(dataCacheName).then((cache) => {

cache.put(e.request.url, response.clone());
log('Service Worker: Fetched & Cached URL ', e.request.url);
return response.clone();

});
})

);

} else {

// The code for App Shell


e.respondWith(
caches.match(e.request).then((response) => {
return response || fetch(e.request);
})
);

}
});

Were now seeing two separate caching strategies at work:

For the App Shell, were using a Cache First strategy


For data, were using a Network First strategy

Because we can use promises to handle these scenarios, we could imagine an alternative
version of the same logic that tried to use the network first for App Shell:
fetch(e.request).catch((err) => {
return caches.match(e.request);
})

As in the previous example, we could add read-through caching to this as well. The skys the
limit!

Well see many of these patterns return later as we investigate SW Toolbox and the strategies it
implements.
If something went wrong

git checkout step-app-shell


Step 6: Using Angular Basic Service Worker

Installation
npm install @angular/service-worker --save

Contains four things:

1. @angular/service-worker/bundles, which has (among other files) some pre-built SW


scripts, ready to be registered in your app. In whatever build system you have for your
project, one step might be to copy one of those SW scripts to your deployment directory.

2. The second thing is the webpack plugin for generating manifests. That's in the module
@angular/service-worker/webpack. You can require and use that in your webpack config
to automatically generate a file called ngsw-manifest.json, which provides data for the
worker script, including which files the worker should cache.
3. The third piece is the "companion" library, which you can use in your app by installing
ServiceWorkerModule from @angular/service-worker. That module allows your app to
talk to the worker itself, and do things like register to receive push notifications.
4. And the fourth piece is @angular/service-worker/worker, which you would only import
from if you were writing a plugin for the worker itself.

The scaffolds for SW could look like:

import {bootstrapServiceWorker} from '../bootstrap';


import {StaticContentCache} from '../../plugins/static';
import {RouteRedirection} from '../../plugins/routes';
import {Push} from '../../plugins/push';

bootstrapServiceWorker({
manifestUrl: '/ngsw-manifest.json',
plugins: [
StaticContentCache(),
RouteRedirection(),
Push(),
],
});

https://github.com/angular/mobile-toolkit/blob/master/service-
worker/worker/src/worker/builds/basic.ts
More tools for easier development of offline experiences
https://github.com/GoogleChrome/sw-toolbox
https://github.com/TalAter/UpUp
If something went wrong

git checkout step-angular-sw


Step 7: Add to home screen and splash screens

Creating manifest file


In order to become an app, your site must provide a manifest of metadata about itself. This
enables features such as add to home screen and splash screens.

Lets use http://realfavicongenerator.net/ service for the generation of graphic assets and
manifest.json file. Just use a logo.png, located in /assets folder and #DE3541 as theme color.
Place all the generated files to /assets/favicons folder. Your manifest.json will look like:

{
"name": "ngPoland Guide",
"icons": [
{
"src": "\/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image\/png"
}
],
"theme_color": "#DE3541",
"display": "standalone"
}

We have to add at least two more properties:

"short_name": "ngPoland",
"start_url": "index.html"

Next, inside your index.html, we'll need to link to the manifest. This is done by adding a
reference to the manifest inside your <head>:

<link rel="manifest" href="/manifest.webmanifest">


Debugging and setting up your browser
Often subtle issues are caused by invalid JSON or missing fields in a manifest. To avoid
this, its useful to paste your manifest contents into the handy Manifest Validator tool:
https://manifest-validator.appspot.com/.
Required properties for Add-to-Homescreen prompt triggering include:
name
short_name
icons, one of which needs to be a PNG of at minimum 144x144px, and
must specify its type as image/png
start_url
Debugging Add-to-Homescreen prompting
Set the following flags in your chrome profile via chrome://flags:

chrome://flags/#bypass-app-banner-engagement-checks
chrome://flags/#enable-add-to-shelf

These will ensure that the A2HS banner appears immediately instead of waiting
for 5 minutes, and enables the feature on desktop for testing.
Checking you've configured Chrome correctly:
Visit airhorner.com and click install, checking that the A2HS prompt
appears
Result:

If there is still no prompt, just click Add to homescreen link:


Controlling the Add to Home Screen banner
You are able to control the timing of the add to home screen banner by listening for the
beforeinstallprompt events.

First, in index.html add the following:

/*****************************************************************************
*
* Listen for the add to home screen events
*
****************************************************************************/

window.addEventListener('beforeinstallprompt', function(e) {
console.log('[App] Showing install prompt');

e.userChoice.then(function(choiceResult) {
console.log(choiceResult.outcome);
});
});

This event is fired just before the browser shows the add to home screen banner. You can call
e.preventDefault() to tell the browser youre not ready yet, for example if you want to wait for the
user to click a particular button. If you call e.preventDefault(), you can call e.prompt() at any
point later to show the banner.

As you can see, the e.userChoice promise gives you the ability to observe the users response
to the banner, for analytics or to update the UI appropriately.
Adding a Splash Screen to your app

Splash screens are automatically generated from a combination of the short_name property, the
icons you specify, and colors you may include in your manifest. The manifest you've added so
far will already include short_name and icons. To fully control the colors, add something like this
to your manifest file:

"background_color": "#DE3541"

Sadly the only way to verify this today is on-device, so if youve set-up on-device, use port
forwarding to check the results of your work on localhost!

Notes:
to debug this on-device youll likely also need to set chrome://flags/#bypass-app-banner-
engagement-checks on your Chrome for Android
this does not work in Chrome for iOS or any other iOS browser today
If something went wrong

git checkout step-homescreen


Step 8: Push Notifications

Setting up the credentials


To set up push notifications, first you need to provide your Google Cloud Messaging sender ID.
For this project we will be providing one, but for production you should obtain one by following
the instructions here.

Note: there are good instructions for setting up push notifications on your site at:
bit.ly/webpushguide.

First, in your manifest.json add

"gcm_sender_id": "70689946818"

https://developers.google.com/web/updates/2016/07/web-push-interop-wins
https://web-push-codelab.appspot.com/
Setting up the backend
We have to create a backend part of our app. Let it be a simple express-powered server:

npm install body-parser --save


npm install express --save
npm install web-push --save
npm install cors --save

Create push-server.js file in a root folder:

var express = require('express');


var cors = require('cors');
var app = express();
app.use(cors());
app.options('*', cors());

var bodyParser = require('body-parser');


var jsonParser = bodyParser.json();
var PORT = process.env.PORT || 8090;
var webPush = require('web-push');

// The GCM API key is AIzaSyDNlm9R_w_0FDGjSM1fzyx5I5JnJBXACqU


webPush.setVapidDetails(
'mailto:salnikov@gmail.com',
'BHe82datFpiOOT0k3D4pieGt1GU-xx8brPjBj0b22gvmwl-HLD1vBOP1AxlDKtwYUQiS9S-
SDVGYe_TdZrYJLw8',
's-zBxZ1Kl_Y1Ac8_uBjwIjtLtG6qlJKOX5trtbanAhc'
);

app.use(express.static(__dirname));

app.use(bodyParser.json());

app.use("/push", function(req, res, next) {


//console.log(res.body);
if (req.body.action === 'subscribe') {
var endpoint = req.body.subscription;

console.log(req);

// Send a push once immediately after subscribing, and one 5 seconds


later
sendNotification(endpoint);
setTimeout(function() {
console.log("[[SENDING PUSH NOW: " + endpoint + "]]");
sendNotification(endpoint);
}, 5000);
res.send({
text: 'Sending push in 5',
status: "200"
});
} else {
throw new Error('Unsupported action');
}
});

app.use("/pushdata", function(req, res, next) {


res.send(JSON.stringify({
msg: "We have " + parseInt(Math.random() * 9 + 1) + " new reviews"
}));
});

app.listen(PORT, 'localhost', function() {


console.log('express server listening on port', PORT);
});

function sendNotification(endpoint) {
console.log('endpoint', endpoint)
webPush.sendNotification(endpoint)
.then(function(response) {
if (response) {
console.log("PUSH RESPONSE: ", response);
} else {
console.log("PUSH SENT");
}
})
.catch(function(err) {
console.error("PUSH ERR: " + err);
});
}

Run it:
node push-server

Test it:
http://localhost:8090/pushdata

Result:
{
msg: "We have 6 new reviews"
}
Setting up the client
We first want to setup push when the user clicks on some UI offering push notifications. Lets
add a simple button:
<button id="btnNotification">Push</button>

Now we have to register an event listener. The flow here is to request permission to send
notifications, then get a reference to the serviceWorker, which we can use to trigger the client to
subscribe with the push server.

Add to index.html:

function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) %
4);
const base64 = (base64String + padding).replace(/\-/g,
'+').replace(/_/g, '/');

const rawData = window.atob(base64);


const outputArray = new Uint8Array(rawData.length);

for (let i = 0; i < rawData.length; ++i) {


outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}

const vapidPublicKey = 'BHe82datFpiOOT0k3D4pieGt1GU-


xx8brPjBj0b22gvmwl-HLD1vBOP1AxlDKtwYUQiS9S-SDVGYe_TdZrYJLw8';
const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);

document.querySelector('#btnNotification').addEventListener('click', (e) => {


// Request permission to send notifications
Notification.requestPermission().then(() => {
// Get a reference to the SW
return navigator.serviceWorker.ready;
}).then((sw) => {
// Tell it to subscribe with the push server
return sw.pushManager.subscribe({userVisibleOnly: true,
applicationServerKey: convertedVapidKey});
}).then((subscription) => {
// Send details about the subscription to the server
});
});

Finally, once we have the subscription we simply pass it up to the server via a network request.
On your site you may also send up a user ID etc, but for now were just going to post it to the
/push endpoint on our server.

// Send details about the subscription to the server


return fetch('http://localhost:8090/push', {
method: 'POST',
body: JSON.stringify({action: 'subscribe', subscription:
subscription}),
headers: new Headers({'Content-Type': 'application/json'})
});

Resulting code will look like:

function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) %
4);
const base64 = (base64String + padding).replace(/\-/g,
'+').replace(/_/g, '/');

const rawData = window.atob(base64);


const outputArray = new Uint8Array(rawData.length);

for (let i = 0; i < rawData.length; ++i) {


outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}

const vapidPublicKey = 'BHe82datFpiOOT0k3D4pieGt1GU-


xx8brPjBj0b22gvmwl-HLD1vBOP1AxlDKtwYUQiS9S-SDVGYe_TdZrYJLw8';
const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);

document.querySelector('#btnNotification').addEventListener('click', (e) => {

// Request permission to send notifications


Notification.requestPermission().then(() => {
// Get a reference to the SW
return navigator.serviceWorker.ready;
}).then((sw) => {
// Tell it to subscribe with the push server
return sw.pushManager.subscribe({userVisibleOnly: true,
applicationServerKey: convertedVapidKey});
}).then((subscription) => {
// Send details about the subscription to the server
return fetch('http://localhost:8090/push', {
method: 'POST',
body: JSON.stringify({action: 'subscribe',
subscription: subscription}),
headers: new Headers({'Content-Type':
'application/json'})
});
});
});

Result:

The node server has been configured to send a push message immediately when a new client
subscribes and again 5 seconds later.

At this point you should check in the network inspection panel that what looks like a valid
subscription is being sent to the server.
Receiving the push message in your service worker
Whenever server decides to push an event to your device, a push event will be fired in your
service worker, so let's start by adding an event listener in push-sw.js.

self.addEventListener('push', function(e) {

});

When that event fired, your service worker needs to decide what notification to show. To do this,
we will send a fetch request to our server, which will provide the JSON for the notification we will
show. Before we start, we must call e.waitUntil so the browser knows to keep our service worker
alive until were done fetching data and showing a notification.

e.waitUntil(
fetch('http://localhost:8090/pushdata').then(function(response) {
return response.json();
}).then(function(data) {
// Here we will show the notification returned in `data`
}, function(err) {
err(err);
})
);

Now we have the notification to show, we can actually show it. To do so, replace the comment
above with:

var title = 'ngPoland';


var body = data.msg;
var icon = '/assets/logo.png';
var tag = 'static-tag';
return self.registration.showNotification(title, {
body: body,
icon: icon,
tag: tag
});

Now remember to increment your service worker version at the top of sw.js, then go back to
your web browser, hard refresh the page (or clear the old SW to ensure youre on the latest
version) and then press the notification bell. If everything is working, after waiting 5 seconds you
should see a notification appear.
Result:
If something went wrong

git checkout step-push


Step 9: Testing and deploying

Installing and running Lighthouse


Lets use the tool from Google to test our app:

npm install -g GoogleChrome/lighthouse

Run:
lighthouse http://localhost:8080/

Result:

Also its available as Chrome extension:


https://chrome.google.com/webstore/detail/lighthouse/blipmdconlkpinefehnmjammfjpmpbjk
Deploying using Surge

https://surge.sh

Install:

npm install --global surge

Lets build our app:

gulp build

Publish:

cd dist
surge
Useful resources

Standards and APIs


https://github.com/w3c/ServiceWorker
https://developer.mozilla.org/en-US/docs/Web/API/Cache
https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API

Cookbooks and guides


https://serviceworke.rs/
https://developers.google.com/web/fundamentals/primers/service-worker/
https://developer.mozilla.org/en-
US/docs/Web/API/Service_Worker_API/Using_Service_Workers

Collections
https://github.com/hemanth/awesome-pwa
https://pwa.rocks/
https://jakearchibald.github.io/isserviceworkerready/

Personal experiences
Stuff I wish I'd known sooner about service workers
https://gist.github.com/Rich-Harris/fd6c3c73e6e707e312d7c5d7d0f3b2f9

Credits
Some parts of the workshop:
Alex Rickabaugh (Angular Team)
Alex Russell (Google)
Owen Campbell-Moore (Google)
Aditya Punjani (Flipkart)