feat: basic PWA
This commit is contained in:
commit
377d234817
12
README.md
Normal file
12
README.md
Normal 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
36
app.js
Normal 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
BIN
favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
BIN
icons/icon-32.png
Normal file
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
BIN
icons/icon-512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 135 KiB |
61
index.html
Normal file
61
index.html
Normal 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()"><</button>
|
||||
<span id="api_id"></span>
|
||||
<button onClick="next()">></button>
|
||||
</div>
|
||||
|
||||
<pre id="result"></pre>
|
||||
</body>
|
||||
</html>
|
22
manifest.webmanifest
Normal file
22
manifest.webmanifest
Normal 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
106
sw.js
Normal 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}`);
|
||||
}
|
Loading…
Reference in a new issue