diff --git a/docs/default-theme-config/README.md b/docs/default-theme-config/README.md index ecb851b13a..cb26affbf3 100644 --- a/docs/default-theme-config/README.md +++ b/docs/default-theme-config/README.md @@ -356,6 +356,31 @@ Note that it's `off` by default. If given `string`, it will be displayed as a pr Since `lastUpdated` is based on `git`, so you can only use it in a `git` repository. ::: +## Service Workers + +The `themeConfig.serviceWorker` option allows you to configure about service workers. + +### Popup UI to refresh contents + +The `themeConfig.serviceWorker.updatePopup` option enables the popup to refresh contents. The popup will be shown when the site is updated (the service worker is updated). It provides `refresh` button to allow users to refresh contents immediately. + +::: tip NOTE +If without the `refresh` button, the new service worker will be active after all clients are closed. +This means that visitors cannot see new contents until they close all tabs of your site. + +But the `refresh` button activates the new service worker immediately. +::: + +``` js +module.exports = { + themeConfig: { + serviceWorker { + updatePopup: true | {message: "New content is available.", buttonText: "Refresh"} + } + } +} +``` + ## Prev / Next Links Prev and next links are automatically inferred based on the sidebar order of the active page. You can also explicitly overwrite or disable them using `YAML front matter`: diff --git a/lib/app/SWUpdateEvent.js b/lib/app/SWUpdateEvent.js new file mode 100644 index 0000000000..fe6ab31c33 --- /dev/null +++ b/lib/app/SWUpdateEvent.js @@ -0,0 +1,43 @@ +export default class SWUpdateEvent { + constructor (registration) { + Object.defineProperty(this, 'registration', { + value: registration, + configurable: true, + writable: true + }) + } + + /** + * Check if the new service worker exists or not. + */ + update () { + return this.registration.update() + } + + /** + * Activate new service worker to work 'location.reload()' with new data. + */ + skipWaiting () { + const worker = this.registration.waiting + if (!worker) { + return Promise.resolve() + } + + console.log('[vuepress:sw] Doing worker.skipWaiting().') + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + console.log('[vuepress:sw] Done worker.skipWaiting().') + if (event.data.error) { + reject(event.data.error) + } else { + resolve(event.data) + } + } + + worker.postMessage({ type: 'skip-waiting' }, [channel.port2]) + }) + } +} + diff --git a/lib/app/clientEntry.js b/lib/app/clientEntry.js index 5691528ebf..7b634e7fb1 100644 --- a/lib/app/clientEntry.js +++ b/lib/app/clientEntry.js @@ -1,6 +1,7 @@ /* global BASE_URL, GA_ID, ga, SW_ENABLED, VUEPRESS_VERSION, LAST_COMMIT_HASH*/ import { createApp } from './app' +import SWUpdateEvent from './SWUpdateEvent' import { register } from 'register-service-worker' const { app, router } = createApp() @@ -46,13 +47,13 @@ router.onReady(() => { console.log('[vuepress:sw] Service worker is active.') app.$refs.layout.$emit('sw-ready') }, - cached () { + cached (registration) { console.log('[vuepress:sw] Content has been cached for offline use.') - app.$refs.layout.$emit('sw-cached') + app.$refs.layout.$emit('sw-cached', new SWUpdateEvent(registration)) }, - updated () { + updated (registration) { console.log('[vuepress:sw] Content updated.') - app.$refs.layout.$emit('sw-updated') + app.$refs.layout.$emit('sw-updated', new SWUpdateEvent(registration)) }, offline () { console.log('[vuepress:sw] No internet connection found. App is running in offline mode.') diff --git a/lib/build.js b/lib/build.js index d43dbb5aa2..16cda677ed 100644 --- a/lib/build.js +++ b/lib/build.js @@ -82,11 +82,16 @@ module.exports = async function build (sourceDir, cliOptions = {}) { if (options.siteConfig.serviceWorker) { logger.wait('\nGenerating service worker...') const wbb = require('workbox-build') - wbb.generateSW({ + await wbb.generateSW({ swDest: path.resolve(outDir, 'service-worker.js'), globDirectory: outDir, globPatterns: ['**\/*.{js,css,html,png,jpg,jpeg,gif,svg,woff,woff2,eot,ttf,otf}'] }) + await fs.writeFile( + path.resolve(outDir, 'service-worker.js'), + await fs.readFile(path.resolve(__dirname, 'service-worker/skip-waiting.js'), 'utf8'), + { flag: 'a' } + ) } // DONE. diff --git a/lib/default-theme/Layout.vue b/lib/default-theme/Layout.vue index 7b422a41fa..ad65a47ce4 100644 --- a/lib/default-theme/Layout.vue +++ b/lib/default-theme/Layout.vue @@ -17,6 +17,7 @@ + @@ -27,13 +28,15 @@ import Home from './Home.vue' import Navbar from './Navbar.vue' import Page from './Page.vue' import Sidebar from './Sidebar.vue' +import SWUpdatePopup from './SWUpdatePopup.vue' import { resolveSidebarItems } from './util' export default { - components: { Home, Page, Sidebar, Navbar }, + components: { Home, Page, Sidebar, Navbar, SWUpdatePopup }, data () { return { - isSidebarOpen: false + isSidebarOpen: false, + swUpdateEvent: null } }, @@ -101,6 +104,8 @@ export default { nprogress.done() this.isSidebarOpen = false }) + + this.$on('sw-updated', this.onSWUpdated) }, methods: { @@ -124,6 +129,9 @@ export default { this.toggleSidebar(false) } } + }, + onSWUpdated (e) { + this.swUpdateEvent = e } } } diff --git a/lib/default-theme/SWUpdatePopup.vue b/lib/default-theme/SWUpdatePopup.vue new file mode 100644 index 0000000000..cbe5d76a85 --- /dev/null +++ b/lib/default-theme/SWUpdatePopup.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/lib/service-worker/skip-waiting.js b/lib/service-worker/skip-waiting.js new file mode 100644 index 0000000000..54fd8d37ce --- /dev/null +++ b/lib/service-worker/skip-waiting.js @@ -0,0 +1,12 @@ +addEventListener('message', event => { + const replyPort = event.ports[0] + const message = event.data + if (replyPort && message && message.type === 'skip-waiting') { + event.waitUntil( + self.skipWaiting().then( + () => replyPort.postMessage({ error: null }), + error => replyPort.postMessage({ error }) + ) + ) + } +}) diff --git a/package.json b/package.json index 659cdba98f..ed20f1a12b 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "portfinder": "^1.0.13", "postcss-loader": "^2.1.5", "prismjs": "^1.13.0", - "register-service-worker": "^1.2.0", + "register-service-worker": "^1.3.0", "semver": "^5.5.0", "stylus": "^0.54.5", "stylus-loader": "^3.0.2", diff --git a/yarn.lock b/yarn.lock index b573bba4ef..c4b8db62b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6941,9 +6941,9 @@ regexpu-core@^4.1.3, regexpu-core@^4.1.4: unicode-match-property-ecmascript "^1.0.4" unicode-match-property-value-ecmascript "^1.0.2" -register-service-worker@^1.2.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/register-service-worker/-/register-service-worker-1.4.1.tgz#4b4c9b4200fc697942c6ae7d611349587b992b2f" +register-service-worker@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/register-service-worker/-/register-service-worker-1.3.0.tgz#02a0b7c40413b3c5ed1d801d764deb3aab1c3397" registry-auth-token@^3.0.1: version "3.3.2"