Support PWA (#437)

* add PWA

* cleanup

* add offline cache
This commit is contained in:
wengtad
2021-05-28 00:48:59 +08:00
committed by GitHub
parent 8e7a17b1bb
commit 39baca4462
26 changed files with 1034 additions and 45 deletions

View File

@@ -9,6 +9,17 @@
</div>
</v-banner>
<GlobalSnackbar />
<v-snackbar v-model="snackWithButtons" bottom left timeout="-1">
{{ snackWithBtnText }}
<template v-slot:action="{ attrs }">
<v-btn text color="primary" v-bind="attrs" @click.stop="refreshApp">
{{ snackBtnText }}
</v-btn>
<v-btn icon class="ml-4" @click="snackWithButtons = false">
<v-icon>{{ $globals.icons.close }}</v-icon>
</v-btn>
</template>
</v-snackbar>
<router-view></router-view>
</v-main>
</v-app>
@@ -51,6 +62,29 @@ export default {
this.$store.dispatch("requestSiteSettings");
},
data() {
return {
refreshing: false,
registration: null,
snackBtnText: "",
snackWithBtnText: "",
snackWithButtons: false,
};
},
created() {
// Listen for swUpdated event and display refresh snackbar as required.
document.addEventListener("swUpdated", this.showRefreshUI, { once: true });
// Refresh all open app tabs when a new service worker is installed.
if (navigator.serviceWorker) {
navigator.serviceWorker.addEventListener("controllerchange", () => {
if (this.refreshing) return;
this.refreshing = true;
window.location.reload();
});
}
},
methods: {
// For Later!
@@ -70,6 +104,25 @@ export default {
this.darkModeSystemCheck();
});
},
showRefreshUI(e) {
// Display a snackbar inviting the user to refresh/reload the app due
// to an app update being available.
// The new service worker is installed, but not yet active.
// Store the ServiceWorkerRegistration instance for later use.
this.registration = e.detail;
this.snackBtnText = "Refresh";
this.snackWithBtnText = "New version available!";
this.snackWithButtons = true;
},
refreshApp() {
this.snackWithButtons = false;
// Protect against missing registration.waiting.
if (!this.registration || !this.registration.waiting) {
return;
}
this.registration.waiting.postMessage("skipWaiting");
},
},
};
</script>

View File

@@ -8,6 +8,7 @@ import { globals } from "@/utils/globals";
import i18n from "./i18n";
import "@mdi/font/css/materialdesignicons.css";
import "typeface-roboto/index.css";
import './registerServiceWorker'
Vue.config.productionTip = false;
Vue.use(VueRouter);

View File

@@ -0,0 +1,39 @@
/* eslint-disable no-console */
import { register } from "register-service-worker";
if (process.env.NODE_ENV === "production") {
register(`${process.env.BASE_URL}service-worker.js`, {
ready() {
console.log("Service worker is active.");
},
registered(registration) {
console.log("Service worker has been registered.");
// Routinely check for app updates by testing for a new service worker.
setInterval(() => {
registration.update();
}, 1000 * 60 * 60); // hourly checks
},
cached() {
console.log("Content has been cached for offline use.");
},
updatefound() {
console.log("New content is downloading.");
},
updated(registration) {
console.log("New content is available; please refresh.");
// Add a custom event and dispatch it.
// Used to display of a 'refresh' banner following a service worker update.
// Set the event payload to the service worker registration object.
document.dispatchEvent(new CustomEvent("swUpdated", { detail: registration }));
},
offline() {
console.log("No internet connection found. App is running in offline mode.");
},
error(error) {
console.error("Error during service worker registration:", error);
},
});
}

75
frontend/src/sw.js Normal file
View File

@@ -0,0 +1,75 @@
/* eslint-disable no-undef, no-underscore-dangle, no-restricted-globals */
self.addEventListener("install", event => {
event.waitUntil(preLoad());
});
var preLoad = async () => {
console.log("Installing web app");
const cache = await caches.open("offline");
console.log("caching index and important routes");
return await cache.addAll(["/", "/recipes/all"]);
};
self.addEventListener("fetch", event => {
event.respondWith(
checkResponse(event.request).catch(() => {
return returnFromCache(event.request);
})
);
event.waitUntil(addToCache(event.request));
});
var checkResponse = request => {
return new Promise(function(fulfill, reject) {
fetch(request).then(function(response) {
if (response.status !== 404) {
fulfill(response);
} else {
reject();
}
}, reject);
});
};
var addToCache = async request => {
const cache = await caches.open("offline");
const response = await fetch(request);
console.log(response.url + " was cached");
return await cache.put(request, response);
};
var returnFromCache = async request => {
const cache = await caches.open("offline");
const matching = await cache.match(request);
if (!matching || matching.status == 404) {
return cache.match("offline.html");
} else {
return matching;
}
};
// This is the code piece that GenerateSW mode can't provide for us.
// This code listens for the user's confirmation to update the app.
self.addEventListener("message", e => {
if (!e.data) {
return;
}
switch (e.data) {
case "skipWaiting":
self.skipWaiting();
break;
default:
// NOOP
break;
}
});
workbox.core.clientsClaim(); // Vue CLI 4 and Workbox v4, else
// workbox.clientsClaim(); // Vue CLI 3 and Workbox v3.
// The precaching code provided by Workbox.
self.__precacheManifest = [].concat(self.__precacheManifest || []);
// workbox.precaching.suppressWarnings(); // Only used with Vue CLI 3 and Workbox v3.
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});