Created
March 13, 2021 22:17
-
-
Save prof3ssorSt3v3/7724c092b7acd45048a2499c3ba223b4 to your computer and use it in GitHub Desktop.
Code for Service Workers 9 - Integrating IndexedDB into site with a Service Worker
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const APP = { | |
SW: null, | |
DB: null, //TODO: | |
init() { | |
//called after DOMContentLoaded | |
//register our service worker | |
APP.registerSW(); | |
document | |
.getElementById('colorForm') | |
.addEventListener('submit', APP.saveColor); | |
//TODO: | |
APP.openDB(); | |
}, | |
registerSW() { | |
if ('serviceWorker' in navigator) { | |
// Register a service worker hosted at the root of the site | |
navigator.serviceWorker.register('/sw.js').then( | |
(registration) => { | |
APP.SW = | |
registration.installing || | |
registration.waiting || | |
registration.active; | |
}, | |
(error) => { | |
console.log('Service worker registration failed:', error); | |
} | |
); | |
//listen for the latest sw | |
navigator.serviceWorker.addEventListener('controllerchange', async () => { | |
APP.SW = navigator.serviceWorker.controller; | |
}); | |
//listen for messages from the service worker | |
navigator.serviceWorker.addEventListener('message', APP.onMessage); | |
} else { | |
console.log('Service workers are not supported.'); | |
} | |
}, | |
saveColor(ev) { | |
ev.preventDefault(); | |
let name = document.getElementById('name'); | |
let color = document.getElementById('color'); | |
let strName = name.value.trim(); | |
let strColor = color.value.trim(); | |
if (strName && strColor) { | |
let person = { | |
id: Date.now(), | |
name: strName, | |
color: strColor, | |
}; | |
console.log('Save', person); | |
//send the data to the service worker | |
//, otherAction: 'hello' | |
APP.sendMessage({ addPerson: person }); | |
} | |
}, | |
sendMessage(msg) { | |
//send some structured-cloneable data from the webpage to the sw | |
if (navigator.serviceWorker.controller) { | |
navigator.serviceWorker.controller.postMessage(msg); | |
} | |
}, | |
onMessage({ data }) { | |
//got a message from the service worker | |
console.log('Web page receiving', data); | |
//TODO: check for savedPerson and build the list and clear the form | |
if ('savedPerson' in data) { | |
APP.showPeople(); | |
document.getElementById('name').value = ''; | |
} | |
}, | |
showPeople() { | |
//TODO: check for DB | |
if (!APP.DB) { | |
APP.openDB(); | |
} | |
//TODO: start transaction to read names and build the list | |
let tx = APP.DB.transaction('colorStore', 'readonly'); | |
let store = tx.objectStore('colorStore'); | |
let req = store.getAll(); | |
req.onsuccess = (ev) => { | |
let list = document.getElementById('people'); | |
let ppl = ev.target.result; | |
list.innerHTML = ppl | |
.map((person) => { | |
console.log('show', person); | |
return `<li data-id="${person.id}"> | |
${person.name} | |
<input type="color" value="${person.color}" disabled /> | |
</li>`; | |
}) | |
.join('\n'); | |
}; | |
}, | |
openDB() { | |
let req = indexedDB.open('colorDB'); | |
req.onsuccess = (ev) => { | |
APP.DB = ev.target.result; | |
APP.showPeople(); | |
}; | |
req.onerror = (err) => { | |
console.warn(err); | |
//NOT calling showPeople() | |
}; | |
}, | |
}; | |
document.addEventListener('DOMContentLoaded', APP.init); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>Intro to Service Workers</title> | |
<!-- import google fonts --> | |
<link rel="preconnect" href="https://fonts.gstatic.com" /> | |
<link | |
href="https://fonts.googleapis.com/css2?family=Lato&family=Montserrat:wght@400;700&display=swap" | |
rel="stylesheet" | |
/> | |
<link rel="stylesheet" href="css/main.css" /> | |
</head> | |
<body> | |
<header> | |
<h1>Intro to Service Workers</h1> | |
<h2>Integrating with IndexedDB</h2> | |
</header> | |
<main> | |
<form id="colorForm" name="colorForm"> | |
<p> | |
<label for="name">Character Name</label> | |
<input type="text" id="name" name="name" /> | |
</p> | |
<p> | |
<label for="color">Favourite Colour</label> | |
<input type="color" id="color" name="color" /> | |
</p> | |
<p> | |
<button id="btnSave">Save</button> | |
</p> | |
</form> | |
<ul id="people"> | |
<!-- TODO: list of people and their colors --> | |
</ul> | |
</main> | |
<script defer src="./js/app.js"></script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
html { | |
font-size: 20px; | |
font-family: 'Montserrat', sans-serif; | |
line-height: 1.5; | |
background-color: #222; | |
color: #eee; | |
} | |
body { | |
min-height: 100vh; | |
background-color: inherit; | |
color: inherit; | |
} | |
header, | |
main { | |
margin: 1rem 2rem; | |
} | |
main { | |
display: flex; | |
flex-direction: row; | |
flex-wrap: wrap; | |
gap: 1rem; | |
justify-content: space-around; | |
} | |
h1 { | |
color: orangered; | |
font-weight: 700; | |
} | |
h2 { | |
color: orange; | |
font-weight: 700; | |
} | |
p { | |
font-family: 'Lato', sans-serif; | |
font-weight: 400; | |
} | |
form { | |
outline: 1px solid #999; | |
} | |
form p { | |
display: flex; | |
flex-direction: column; | |
justify-content: flex-start; | |
align-items: flex-start; | |
padding: 0.5rem 1rem; | |
} | |
label, | |
input { | |
font-size: 1rem; | |
margin: 0.5rem 0; | |
} | |
main img { | |
width: clamp(200px, 400px, 600px); | |
} | |
main a { | |
color: orange; | |
text-decoration-style: wavy; | |
} | |
button { | |
font-size: 1rem; | |
background-color: cornflowerblue; | |
color: white; | |
padding: 0.25rem 2rem; | |
border: none; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const version = 3; | |
let staticName = `staticCache-${version}`; | |
let dynamicName = `dynamicCache`; | |
let imageName = `imageCache-${version}`; | |
let options = { | |
ignoreSearch: false, | |
ignoreMethod: false, | |
ignoreVary: false, | |
}; | |
//starter html and css and js files | |
let assets = ['/', '/index.html', '/css/main.css', '/js/app.js', '/404.html']; | |
//starter images | |
let imageAssets = ['/img/1011-800x600.jpg', '/img/distracted-boyfriend.jpg']; | |
//TODO: | |
let DB = null; | |
self.addEventListener('install', (ev) => { | |
// service worker has been installed. | |
//Extendable Event | |
console.log(`Version ${version} installed`); | |
// build a cache | |
ev.waitUntil( | |
caches | |
.open(staticName) | |
.then((cache) => { | |
cache.addAll(assets).then( | |
() => { | |
//addAll == fetch() + put() | |
// console.log(`${staticName} has been updated.`); | |
}, | |
(err) => { | |
console.warn(`failed to update ${staticName}.`); | |
} | |
); | |
}) | |
.then(() => { | |
caches.open(imageName).then((cache) => { | |
cache.addAll(imageAssets).then( | |
() => { | |
console.log(`${imageName} has been updated.`); | |
}, | |
(err) => { | |
console.warn(`failed to update ${staticName}.`); | |
} | |
); | |
}); | |
}) | |
); | |
}); | |
self.addEventListener('activate', (ev) => { | |
// when the service worker has been activated to replace an old one. | |
//Extendable Event | |
console.log('activated'); | |
// delete old versions of caches. | |
ev.waitUntil( | |
caches.keys().then((keys) => { | |
return Promise.all( | |
keys | |
.filter((key) => { | |
if (key != staticName && key != imageName) { | |
return true; | |
} | |
}) | |
.map((key) => caches.delete(key)) | |
).then((empties) => { | |
//empties is an Array of boolean values. | |
//one for each cache deleted | |
//TODO: | |
openDB(); | |
}); | |
}) | |
); | |
}); | |
self.addEventListener('fetch', (ev) => { | |
// Extendable Event. | |
ev.respondWith( | |
caches.match(ev.request).then((cacheRes) => { | |
return ( | |
cacheRes || | |
Promise.resolve().then(() => { | |
let opts = { | |
mode: ev.request.mode, //cors, no-cors, same-origin, navigate | |
cache: 'no-cache', | |
}; | |
if (!ev.request.url.startsWith(location.origin)) { | |
//not on the same domain as my html file | |
opts.mode = 'cors'; | |
opts.credentials = 'omit'; | |
} | |
return fetch(ev.request.url, opts).then( | |
(fetchResponse) => { | |
//we got a response from the server. | |
if (fetchResponse.ok) { | |
return handleFetchResponse(fetchResponse, ev.request); | |
} | |
//not ok 404 error | |
if (fetchResponse.status == 404) { | |
if (ev.request.url.match(/\.html/i)) { | |
return caches.open(staticName).then((cache) => { | |
return cache.match('/404.html'); | |
}); | |
} | |
if ( | |
ev.request.url.match(/\.jpg$/i) || | |
ev.request.url.match(/\.png$/i) | |
) { | |
return caches.open(imageName).then((cache) => { | |
return cache.match('/img/distracted-boyfriend.jpg'); | |
}); | |
} | |
} | |
}, | |
(err) => { | |
//this is the network failure | |
//return the 404.html file if it is a request for an html file | |
if (ev.request.url.match(/\.html/i)) { | |
return caches.open(staticName).then((cache) => { | |
return cache.match('/404.html'); | |
}); | |
} | |
} | |
); | |
}) | |
); | |
}) //end of match().then() | |
); //end of respondWith | |
}); //end of fetch listener | |
const handleFetchResponse = (fetchResponse, request) => { | |
let type = fetchResponse.headers.get('content-type'); | |
// console.log('handle request for', type, request.url); | |
if (type && type.match(/^image\//i)) { | |
//save the image in image cache | |
// console.log(`SAVE ${request.url} in image cache`); | |
return caches.open(imageName).then((cache) => { | |
cache.put(request, fetchResponse.clone()); | |
return fetchResponse; | |
}); | |
} else { | |
//save in dynamic cache - html, css, fonts, js, etc | |
// console.log(`SAVE ${request.url} in dynamic cache`); | |
return caches.open(dynamicName).then((cache) => { | |
cache.put(request, fetchResponse.clone()); | |
return fetchResponse; | |
}); | |
} | |
}; | |
self.addEventListener('message', (ev) => { | |
let data = ev.data; | |
//console.log({ ev }); | |
let clientId = ev.source.id; | |
// console.log('Service Worker received', data, clientId); | |
if ('addPerson' in data) { | |
//TODO: really do something with the data | |
//TODO: open the database and wait for success | |
//TODO: start a transaction | |
//TODO: put() the data | |
//console.log({ DB }); | |
//TODO: check for db then openDB or savePerson | |
if (DB) { | |
savePerson(data.addPerson, clientId); | |
} else { | |
openDB(() => { | |
savePerson(data.addPerson, clientId); | |
}); | |
} | |
} | |
if ('otherAction' in data) { | |
let msg = 'Hola'; | |
sendMessage({ | |
code: 0, | |
message: msg, | |
}); | |
} | |
}); | |
const savePerson = (person, clientId) => { | |
if (person && DB) { | |
let tx = DB.transaction('colorStore', 'readwrite'); | |
tx.onerror = (err) => { | |
//failed transaction | |
}; | |
tx.oncomplete = (ev) => { | |
//finished saving... send the message | |
let msg = 'Thanks. The data was saved.'; | |
sendMessage( | |
{ | |
code: 0, | |
message: msg, | |
savedPerson: person, | |
}, | |
clientId | |
); | |
}; | |
let store = tx.objectStore('colorStore'); | |
let req = store.put(person); | |
req.onsuccess = (ev) => { | |
//saved the person | |
//tx.commit() will be called automatically | |
//and will trigger tx.oncomplete next | |
}; | |
} else { | |
let msg = 'No data was provided.'; | |
sendMessage( | |
{ | |
code: 0, | |
message: msg, | |
}, | |
clientId | |
); | |
} | |
}; | |
const sendMessage = async (msg, clientId) => { | |
let allClients = []; | |
if (clientId) { | |
let client = await clients.get(clientId); | |
allClients.push(client); | |
} else { | |
allClients = await clients.matchAll({ includeUncontrolled: true }); | |
} | |
return Promise.all( | |
allClients.map((client) => { | |
// console.log('postMessage', msg, 'to', client.id); | |
return client.postMessage(msg); | |
}) | |
); | |
}; | |
const openDB = (callback) => { | |
let req = indexedDB.open('colorDB', version); | |
req.onerror = (err) => { | |
//could not open db | |
console.warn(err); | |
DB = null; | |
}; | |
req.onupgradeneeded = (ev) => { | |
let db = ev.target.result; | |
if (!db.objectStoreNames.contains('colorStore')) { | |
db.createObjectStore('colorStore', { | |
keyPath: 'id', | |
}); | |
} | |
}; | |
req.onsuccess = (ev) => { | |
DB = ev.target.result; | |
console.log('db opened and upgraded as needed'); | |
if (callback) { | |
callback(); | |
} | |
}; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
thanks for making a great tutorial.