Skip to content

Commit 14dbd1e

Browse files
mysticateaulivz
authored andcommitted
feat($pwa): add themeConfig.serviceWorker.updatePopup option (close: #453) (#533)
1 parent 887a0a6 commit 14dbd1e

File tree

9 files changed

+187
-11
lines changed

9 files changed

+187
-11
lines changed

docs/default-theme-config/README.md

+25
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,31 @@ Note that it's `off` by default. If given `string`, it will be displayed as a pr
356356
Since `lastUpdated` is based on `git`, so you can only use it in a `git` repository.
357357
:::
358358

359+
## Service Workers
360+
361+
The `themeConfig.serviceWorker` option allows you to configure about service workers.
362+
363+
### Popup UI to refresh contents
364+
365+
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.
366+
367+
::: tip NOTE
368+
If without the `refresh` button, the new service worker will be active after all clients are closed.
369+
This means that visitors cannot see new contents until they close all tabs of your site.
370+
371+
But the `refresh` button activates the new service worker immediately.
372+
:::
373+
374+
``` js
375+
module.exports = {
376+
themeConfig: {
377+
serviceWorker {
378+
updatePopup: true | {message: "New content is available.", buttonText: "Refresh"}
379+
}
380+
}
381+
}
382+
```
383+
359384
## Prev / Next Links
360385

361386
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`:

lib/app/SWUpdateEvent.js

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
export default class SWUpdateEvent {
2+
constructor (registration) {
3+
Object.defineProperty(this, 'registration', {
4+
value: registration,
5+
configurable: true,
6+
writable: true
7+
})
8+
}
9+
10+
/**
11+
* Check if the new service worker exists or not.
12+
*/
13+
update () {
14+
return this.registration.update()
15+
}
16+
17+
/**
18+
* Activate new service worker to work 'location.reload()' with new data.
19+
*/
20+
skipWaiting () {
21+
const worker = this.registration.waiting
22+
if (!worker) {
23+
return Promise.resolve()
24+
}
25+
26+
console.log('[vuepress:sw] Doing worker.skipWaiting().')
27+
return new Promise((resolve, reject) => {
28+
const channel = new MessageChannel()
29+
30+
channel.port1.onmessage = (event) => {
31+
console.log('[vuepress:sw] Done worker.skipWaiting().')
32+
if (event.data.error) {
33+
reject(event.data.error)
34+
} else {
35+
resolve(event.data)
36+
}
37+
}
38+
39+
worker.postMessage({ type: 'skip-waiting' }, [channel.port2])
40+
})
41+
}
42+
}
43+

lib/app/clientEntry.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* global BASE_URL, GA_ID, ga, SW_ENABLED, VUEPRESS_VERSION, LAST_COMMIT_HASH*/
22

33
import { createApp } from './app'
4+
import SWUpdateEvent from './SWUpdateEvent'
45
import { register } from 'register-service-worker'
56

67
const { app, router } = createApp()
@@ -46,13 +47,13 @@ router.onReady(() => {
4647
console.log('[vuepress:sw] Service worker is active.')
4748
app.$refs.layout.$emit('sw-ready')
4849
},
49-
cached () {
50+
cached (registration) {
5051
console.log('[vuepress:sw] Content has been cached for offline use.')
51-
app.$refs.layout.$emit('sw-cached')
52+
app.$refs.layout.$emit('sw-cached', new SWUpdateEvent(registration))
5253
},
53-
updated () {
54+
updated (registration) {
5455
console.log('[vuepress:sw] Content updated.')
55-
app.$refs.layout.$emit('sw-updated')
56+
app.$refs.layout.$emit('sw-updated', new SWUpdateEvent(registration))
5657
},
5758
offline () {
5859
console.log('[vuepress:sw] No internet connection found. App is running in offline mode.')

lib/build.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,16 @@ module.exports = async function build (sourceDir, cliOptions = {}) {
8282
if (options.siteConfig.serviceWorker) {
8383
logger.wait('\nGenerating service worker...')
8484
const wbb = require('workbox-build')
85-
wbb.generateSW({
85+
await wbb.generateSW({
8686
swDest: path.resolve(outDir, 'service-worker.js'),
8787
globDirectory: outDir,
8888
globPatterns: ['**\/*.{js,css,html,png,jpg,jpeg,gif,svg,woff,woff2,eot,ttf,otf}']
8989
})
90+
await fs.writeFile(
91+
path.resolve(outDir, 'service-worker.js'),
92+
await fs.readFile(path.resolve(__dirname, 'service-worker/skip-waiting.js'), 'utf8'),
93+
{ flag: 'a' }
94+
)
9095
}
9196

9297
// DONE.

lib/default-theme/Layout.vue

+10-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<slot name="page-top" slot="top"/>
1818
<slot name="page-bottom" slot="bottom"/>
1919
</Page>
20+
<SWUpdatePopup :updateEvent="swUpdateEvent" />
2021
</div>
2122
</template>
2223

@@ -27,13 +28,15 @@ import Home from './Home.vue'
2728
import Navbar from './Navbar.vue'
2829
import Page from './Page.vue'
2930
import Sidebar from './Sidebar.vue'
31+
import SWUpdatePopup from './SWUpdatePopup.vue'
3032
import { resolveSidebarItems } from './util'
3133
3234
export default {
33-
components: { Home, Page, Sidebar, Navbar },
35+
components: { Home, Page, Sidebar, Navbar, SWUpdatePopup },
3436
data () {
3537
return {
36-
isSidebarOpen: false
38+
isSidebarOpen: false,
39+
swUpdateEvent: null
3740
}
3841
},
3942
@@ -101,6 +104,8 @@ export default {
101104
nprogress.done()
102105
this.isSidebarOpen = false
103106
})
107+
108+
this.$on('sw-updated', this.onSWUpdated)
104109
},
105110
106111
methods: {
@@ -124,6 +129,9 @@ export default {
124129
this.toggleSidebar(false)
125130
}
126131
}
132+
},
133+
onSWUpdated (e) {
134+
this.swUpdateEvent = e
127135
}
128136
}
129137
}

lib/default-theme/SWUpdatePopup.vue

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<template>
2+
<transition name="sw-update-popup">
3+
<div v-if="enabled" class="sw-update-popup">
4+
{{message}}<br>
5+
<button @click="reload">{{buttonText}}</button>
6+
</div>
7+
</transition>
8+
</template>
9+
10+
<script>
11+
export default {
12+
props: {
13+
updateEvent: {
14+
type: Object,
15+
default: null
16+
}
17+
},
18+
19+
computed: {
20+
popupConfig () {
21+
for (const config of [this.$themeLocaleConfig, this.$site.themeConfig]) {
22+
const sw = config.serviceWorker
23+
if (sw && sw.updatePopup) {
24+
return typeof sw.updatePopup === 'object' ? sw.updatePopup : {}
25+
}
26+
}
27+
return null
28+
},
29+
30+
enabled () {
31+
return Boolean(this.popupConfig && this.updateEvent)
32+
},
33+
34+
message () {
35+
const c = this.popupConfig
36+
return (c && c.message) || 'New content is available.'
37+
},
38+
39+
buttonText () {
40+
const c = this.popupConfig
41+
return (c && c.buttonText) || 'Refresh'
42+
}
43+
},
44+
45+
methods: {
46+
reload () {
47+
if (this.updateEvent) {
48+
this.updateEvent.skipWaiting().then(() => {
49+
location.reload(true)
50+
})
51+
this.updateEvent = null
52+
}
53+
}
54+
}
55+
}
56+
</script>
57+
58+
<style lang="stylus">
59+
@import './styles/config.styl'
60+
61+
.sw-update-popup
62+
position fixed
63+
right 1em
64+
bottom 1em
65+
padding 1em
66+
border 1px solid $accentColor
67+
border-radius 3px
68+
background #fff
69+
box-shadow 0 4px 16px rgba(0, 0, 0, 0.5)
70+
text-align center
71+
72+
button
73+
margin-top 0.5em
74+
padding 0.25em 2em
75+
76+
.sw-update-popup-enter-active, .sw-update-popup-leave-active
77+
transition opacity 0.3s, transform 0.3s
78+
79+
.sw-update-popup-enter, .sw-update-popup-leave-to
80+
opacity 0
81+
transform translate(0, 50%) scale(0.5)
82+
</style>

lib/service-worker/skip-waiting.js

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
addEventListener('message', event => {
2+
const replyPort = event.ports[0]
3+
const message = event.data
4+
if (replyPort && message && message.type === 'skip-waiting') {
5+
event.waitUntil(
6+
self.skipWaiting().then(
7+
() => replyPort.postMessage({ error: null }),
8+
error => replyPort.postMessage({ error })
9+
)
10+
)
11+
}
12+
})

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
"portfinder": "^1.0.13",
7878
"postcss-loader": "^2.1.5",
7979
"prismjs": "^1.13.0",
80-
"register-service-worker": "^1.2.0",
80+
"register-service-worker": "^1.3.0",
8181
"semver": "^5.5.0",
8282
"stylus": "^0.54.5",
8383
"stylus-loader": "^3.0.2",

yarn.lock

+3-3
Original file line numberDiff line numberDiff line change
@@ -6941,9 +6941,9 @@ regexpu-core@^4.1.3, regexpu-core@^4.1.4:
69416941
unicode-match-property-ecmascript "^1.0.4"
69426942
unicode-match-property-value-ecmascript "^1.0.2"
69436943

6944-
register-service-worker@^1.2.0:
6945-
version "1.4.1"
6946-
resolved "https://registry.yarnpkg.com/register-service-worker/-/register-service-worker-1.4.1.tgz#4b4c9b4200fc697942c6ae7d611349587b992b2f"
6944+
register-service-worker@^1.3.0:
6945+
version "1.3.0"
6946+
resolved "https://registry.yarnpkg.com/register-service-worker/-/register-service-worker-1.3.0.tgz#02a0b7c40413b3c5ed1d801d764deb3aab1c3397"
69476947

69486948
registry-auth-token@^3.0.1:
69496949
version "3.3.2"

0 commit comments

Comments
 (0)