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"