Refactor PWA

This commit is contained in:
Jeremy Likness 2019-12-03 15:28:57 -08:00
parent d13ccc0c32
commit e2d52d32e9
2 changed files with 74 additions and 169 deletions

241
pwa.js
View File

@ -1,13 +1,5 @@
// @ts-check // @ts-check
/**
* @typedef {object} CacheVersions
* @property {string} assets Cache name for base assets
* @property {string} content Cache for pages
* @property {string} offline Cache for offline content
* @property {string} notFound Cache for 404 content
*/
/** /**
* @typedef {function} AddEventListener * @typedef {function} AddEventListener
* @param {string} eventName Name of event to add * @param {string} eventName Name of event to add
@ -56,20 +48,6 @@ class Pwa {
'/images/logo.png', '/images/logo.png',
'/js/app.js', '/js/app.js',
]; ];
/**
* Files needed for offline mode
* @type {string[]}
*/
this.OFFLINE_CACHE_FILES = [
'/pwa/offline.html'
];
/**
* Files needed for 404 page
* @type {string[]}
*/
this.NOT_FOUND_CACHE_FILES = [
'/pwa/404.html'
];
/** /**
* Page to redirect to when offline * Page to redirect to when offline
* @type {string} * @type {string}
@ -81,48 +59,30 @@ class Pwa {
*/ */
this.NOT_FOUND_PAGE = '/pwa/404.html'; this.NOT_FOUND_PAGE = '/pwa/404.html';
/** /**
* Versioned caches * Versioned cache
* @type {CacheVersions} * @type {string}
*/ */
this.CACHE_VERSIONS = { this.CACHE_NAME = `content-v${this.CACHE_VERSION}`;
assets: `assets-v${this.CACHE_VERSION}`,
content: `content-v${this.CACHE_VERSION}`,
offline: `offline-v${this.CACHE_VERSION}`,
notFound: `404-v${this.CACHE_VERSION}`,
};
/** /**
* The time to live in cache * The time to live in cache
* @type {object} * @type {object}
*/ */
this.MAX_TTL = { this.MAX_TTL = 86400;
/** @type {number} Default time to live in seconds */
'/': 3600,
/** @type {number} Time to live for pages in seconds */
html: 43200,
/** @type {number} Time to live for JSON in seconds */
json: 43200,
/** @type {number} Time to live for scripts in seconds */
js: 86400,
/** @type {number} Time to live for stylesheets in seconds */
css: 86400,
};
/** /**
* Supported methods for HTTP requests * Extensions with no expiration in the cache
* @type {string[]} * @type {string[]}
*/ */
this.SUPPORTED_METHODS = [ this.TTL_EXCEPTIONS = ["jpg", "jpeg", "png", "gif", "mp4"];
'GET',
];
} }
/** /**
* Get the extension of a file from URL * Get the extension of a file from URL
* @param {string} url * @param {string} url
* @returns {string} * @returns {string} The extension
*/ */
getFileExtension(url) { getFileExtension(url) {
let extension = url.split('.').reverse()[0].split('?')[0]; const extension = url.split('.').reverse()[0].split('?')[0];
return (extension.endsWith('/')) ? '/' : extension; return (extension.endsWith('/')) ? '/' : extension;
} }
@ -134,32 +94,21 @@ class Pwa {
getTTL(url) { getTTL(url) {
if (typeof url === 'string') { if (typeof url === 'string') {
const extension = this.getFileExtension(url); const extension = this.getFileExtension(url);
if (typeof this.MAX_TTL[extension] === 'number') { return ~this.TTL_EXCEPTIONS.indexOf(extension) ?
return this.MAX_TTL[extension]; this.MAX_TTL : null;
} else {
return null;
}
} else {
return null;
} }
return null;
} }
async installServiceWorker() { async installServiceWorker() {
try { try {
await Promise.all([ await caches.open(this.CACHE_NAME).then((cache) => {
caches.open(this.CACHE_VERSIONS.assets).then((cache) => { return cache.addAll(this.BASE_CACHE_FILES);
return cache.addAll(this.BASE_CACHE_FILES); }, err => console.error(`Error with ${this.CACHE_NAME}`, err));
}, err => console.error(`Error with ${this.CACHE_VERSIONS.assets}`, err)),
caches.open(this.CACHE_VERSIONS.offline).then((cache_1) => {
return cache_1.addAll(this.OFFLINE_CACHE_FILES);
}, err_1 => console.error(`Error with ${this.CACHE_VERSIONS.offline}`, err_1)),
caches.open(this.CACHE_VERSIONS.notFound).then((cache_2) => {
return cache_2.addAll(this.NOT_FOUND_CACHE_FILES);
}, err_2 => console.error(`Error with ${this.CACHE_VERSIONS.notFound}`, err_2))]);
return this.scope.skipWaiting(); return this.scope.skipWaiting();
} }
catch (err_3) { catch (err) {
return console.error("Error with installation: ", err_3); return console.error("Error with installation: ", err);
} }
} }
@ -169,34 +118,26 @@ class Pwa {
*/ */
cleanupLegacyCache() { cleanupLegacyCache() {
const currentCaches = Object.keys(this.CACHE_VERSIONS).map((key) => { const currentCaches = [this.CACHE_NAME];
return this.CACHE_VERSIONS[key];
});
return new Promise( return new Promise(
(resolve, reject) => { (resolve, reject) => {
caches.keys().then((keys) => { caches.keys()
return keys.filter((key) => { .then((keys) => keys.filter((key) => !~currentCaches.indexOf(key)))
return !~currentCaches.indexOf(key); .then((legacy) => {
}); if (legacy.length) {
}).then((legacy) => { Promise.all(legacy.map((legacyKey) => caches.delete(legacyKey))
if (legacy.length) { ).then(() => resolve()).catch((err) => {
Promise.all(legacy.map((legacyKey) => { console.error("Error in legacy cleanup: ", err);
return caches.delete(legacyKey); reject(err);
}) });
).then(() => { } else {
resolve(); resolve();
}).catch((err) => { }
console.error("Error in legacy cleanup: ", err); }).catch((err) => {
reject(err); console.error("Error in legacy cleanup: ", err);
}); reject(err);
} else { });
resolve();
}
}).catch((err) => {
console.error("Error in legacy cleanup: ", err);
reject(err);
});
}); });
} }
@ -204,23 +145,13 @@ class Pwa {
* Pre-fetches URL to store to cache * Pre-fetches URL to store to cache
* @param {string} url * @param {string} url
*/ */
preCacheUrl(url) { async preCacheUrl(url) {
caches.open(this.CACHE_VERSIONS.content).then((cache) => { const cache = await caches.open(this.CACHE_NAME);
cache.match(url).then((response) => { const response = await cache.match(url);
if (!response) { if (!response) {
return fetch(url); return fetch(url).then(resp => cache.put(url, resp.clone()));
} else { }
// already in cache, nothing to do. return null;
return null;
}
}).then((response) => {
if (response) {
return cache.put(url, response.clone());
} else {
return null;
}
});
});
} }
/** /**
@ -249,70 +180,44 @@ class Pwa {
this.scope.addEventListener('fetch', event => { this.scope.addEventListener('fetch', event => {
event.respondWith( event.respondWith(
caches.open(this.CACHE_VERSIONS.content) caches.open(this.CACHE_NAME).then(async cache => {
.then(async (cache) => { const response = await cache.match(event.request);
try { if (response) {
const response = await cache.match(event.request); // found it, see if expired
if (response) { const headers = response.headers.entries();
const headers = response.headers.entries(); let date = null;
let date = null; for (let pair of headers) {
for (let pair of headers) { if (pair[0] === 'date') {
if (pair[0] === 'date') { date = new Date(pair[1]);
date = new Date(pair[1]); break;
}
}
if (date) {
let age = parseInt(((new Date().getTime() - date.getTime()) / 1000).toString());
let ttl = this.getTTL(event.request.url);
if (ttl && age > ttl) {
return new Promise(async (resolve) => {
try {
const updatedResponse = await fetch(event.request.clone());
if (updatedResponse) {
cache.put(event.request, updatedResponse.clone());
resolve(updatedResponse);
}
else {
resolve(response);
}
}
catch (e) {
resolve(response);
}
}).catch(() => response);
}
else {
return response;
}
}
else {
return response;
}
}
else {
return null;
} }
} }
catch (error) { if (!date) {
console.error('Error in fetch handler:', error); return response;
throw error;
} }
const age = parseInt(((new Date().getTime() - date.getTime()) / 1000).toString());
const ttl = this.getTTL(event.request.url);
if (ttl === null || (ttl && age < ttl)) {
return response;
}
}
// not found or expired, fresh request
return fetch(event.request.clone()).then(resp => {
if (resp.status < 400) {
// good to go
cache.put(event.request, resp.clone());
return resp;
}
else {
// not found
return cache.match(this.NOT_FOUND_PAGE);
}
}).catch(err => {
// offline
console.error("Error resulting in offline", err);
return cache.match(this.OFFLINE_PAGE);
}) })
); }));
});
this.scope.addEventListener('message', (event) => {
if (typeof event.data === 'object' &&
typeof event.data.action === 'string') {
switch (event.data.action) {
case 'cache':
this.preCacheUrl(event.data.url);
break;
default:
console.log(`Unknown action: ${event.data.action}`);
break;
}
}
}); });
} }
} }

View File

@ -8,7 +8,7 @@
"Differences in the DOM (i.e. jQuery normalization)", "Differences in the DOM (i.e. jQuery normalization)",
"Lack of built-in templates", "Lack of built-in templates",
"Need for SPA routing (journal)", "Need for SPA routing (journal)",
"Asynchronous module loading/manage the dependency graph", "Asynchronous module loading/dependency graph",
"Testability", "Testability",
"Databinding", "Databinding",
"Contacts (Types and Interfaces ➡ TypeScript)", "Contacts (Types and Interfaces ➡ TypeScript)",