feat: basic PWA

This commit is contained in:
Sébastien NOBILI 2023-04-24 12:00:59 +02:00
commit 377d234817
8 changed files with 237 additions and 0 deletions

12
README.md Normal file
View file

@ -0,0 +1,12 @@
# README
This repository contains a simple PWA to base a series of articles on.
What will be covered?
- [x] making it installable
- [x] caching application resources
- [x] caching data resources
- [ ] communicating between service worker & interface
- [ ] updating cached application resources & triggering application reload
- [ ] using Workbox

36
app.js Normal file
View file

@ -0,0 +1,36 @@
document.addEventListener('DOMContentLoaded', () => {
fetch_data(1);
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js');
}
});
async function previous() {
let id = parseInt(document.getElementById('api_id').innerHTML, 10);
id--;
if (id <= 0) {
id = 0;
}
fetch_data(id);
}
async function next() {
let id = parseInt(document.getElementById('api_id').innerHTML, 10);
id++;
fetch_data(id);
}
async function fetch_data(id) {
document.getElementById('api_id').innerHTML = id;
fetch(`https://swapi.dev/api/people/${id}`).then((response) => {
if (response.ok) {
response.json().then((data) => {
document.getElementById('result').innerHTML = JSON.stringify(data, null, 2);
document.getElementById('api_id').focus();
});
} else {
document.getElementById('result').innerHTML = "You're currently offline";
document.getElementById('api_id').focus();
}
});
}

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
icons/icon-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
icons/icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

61
index.html Normal file
View file

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<meta name="mobile-web-app-capable" content="yes" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="theme-color" content="#8d2382" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="manifest" href="manifest.webmanifest" />
<title>Simple PWA</title>
<style type="text/css">
html,
body {
height: 100%;
width: 80%;
margin: 0;
display: flex;
flex-direction: column;
margin-left: auto;
margin-right: auto;
font-family: sans-serif;
}
h1,
div.controls {
text-align: center;
}
#api_id {
font-family: monospace;
text-align: center;
display: inline-block;
width: 5em;
}
pre {
display: flex;
margin-left: auto;
margin-right: auto;
}
</style>
<script type="text/javascript" src="app.js"></script>
</head>
<body>
<h1>Simple PWA</h1>
<div class="controls">
<button onClick="previous()">&lt;</button>
<span id="api_id"></span>
<button onClick="next()">&gt;</button>
</div>
<pre id="result"></pre>
</body>
</html>

22
manifest.webmanifest Normal file
View file

@ -0,0 +1,22 @@
{
"name": "Simple PWA",
"short_name": "Simple PWA",
"description": "A simple PWA, for testing",
"icons": [
{
"src": "icons/icon-32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"start_url": "/pwa-test/index.html",
"display": "fullscreen",
"theme_color": "#B12A34",
"background_color": "#B12A34"
}

106
sw.js Normal file
View file

@ -0,0 +1,106 @@
const appCacheName = 'simple-pwa';
const appContentToCache = [
'/',
'index.html',
'app.js',
'favicon.ico',
'manifest.webmanifest',
'icons/icon-512.png',
];
const dataCacheName = `${appCacheName}-data`;
/**
* First of all, the service worker will react to an 'install' event (triggered automatically)
* We'll put the application content into cache.
*/
self.addEventListener('install', (e) => {
e.waitUntil(async () => {
await caches.delete(appCacheName);
const cache = await caches.open(appCacheName);
await cache.addAll(appContentToCache);
});
});
/**
* Then, the service worker will be involved every time a request is made
*/
self.addEventListener('fetch', (e) => {
e.respondWith(fetch_resource(e.request));
});
/**
* fetch a resource:
* - if resource is in app cache, return in
* - if resource can be obtained from remote server, fetch it
* - if resource is in data cache, return it
* - otherwise return HTTP-408 response
*/
async function fetch_resource(resource) {
response = await get_from_cache(resource, appCacheName);
if (response) {
return response;
} else {
try {
response = await fetch(resource);
await put_into_cache(resource, response);
return response;
} catch (error) {
// TODO: check if error is because we're offline
response = await get_from_cache(resource);
if (response) {
// resource was found in data cache
return response;
} else {
return new Response('offline', {
status: 408,
headers: { 'Content-Type': 'text/plain' },
});
}
}
}
}
/**
* reset app cache that contains application code to be cached for offline use
*/
async function reset_app_cache() {
log('resetting app cache');
await caches.delete(appCacheName);
const cache = await caches.open(appCacheName);
await cache.addAll(appContentToCache);
}
/**
* query cache for resource
*/
async function get_from_cache(resource, cacheName = dataCacheName) {
try {
const cache = await caches.open(cacheName);
const response = await cache.match(resource);
if (response) {
log(
`FROM ${cacheName === appCacheName ? 'APP' : 'DATA'} CACHE: ${
resource.url
}`,
);
}
return response;
} catch (error) {
return;
}
}
/**
* put resource into cache
*/
async function put_into_cache(request, response, cacheName = dataCacheName) {
const cache = await caches.open(cacheName);
await cache.put(request, response.clone());
}
/**
* Log a message to console
*/
function log(message) {
console.log(`[Service Worker] ${message}`);
}