docs: only cache the user preferred language pages in the PWA (#7568)

* docs: cache the preferred lang pages in the PWA

* chore: remove index.html in manifest

* fix: no cache the '/' page

* perf: optimize code

* docs: add always refresh from PWA
This commit is contained in:
qiang 2022-05-12 22:48:38 +08:00 committed by GitHub
parent 325f84d44f
commit 0c92e8a8d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 233 additions and 3 deletions

View File

@ -1,5 +1,6 @@
{
"message": "New content available, click on refresh button to update.",
"refresh": "Refresh",
"always-refresh": "Always Refresh",
"close": "Close"
}

View File

@ -25,4 +25,10 @@
? toPath
: toPath.concat('/')
}
if (navigator && navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
type: 'LANG',
lang: userPreferredLang,
})
}
})()

189
docs/.vitepress/sw.ts Normal file
View File

@ -0,0 +1,189 @@
import { cacheNames, clientsClaim } from 'workbox-core'
import type { ManifestEntry } from 'workbox-build'
declare let self: ServiceWorkerGlobalScope & {
__WB_MANIFEST: ManifestEntry[]
}
const manifest = self.__WB_MANIFEST
const cacheName = cacheNames.runtime
const defaultLang = manifest.some((item) => {
return item.url.includes(navigator.language)
})
? navigator.language
: 'en-US'
let userPreferredLang = ''
let cacheEntries: RequestInfo[] = []
let cacheManifestURLs: string[] = []
let manifestURLs: string[] = []
class LangDB {
private db: IDBDatabase | undefined
private databaseName = 'PWA_DB'
private version = 1
private storeNames = 'lang'
constructor() {
this.initDB()
}
private initDB() {
return new Promise<boolean>((resolve) => {
const request = indexedDB.open(this.databaseName, this.version)
request.onsuccess = (event) => {
this.db = (event.target as IDBOpenDBRequest).result
resolve(true)
}
request.onupgradeneeded = (event) => {
this.db = (event.target as IDBOpenDBRequest).result
if (!this.db.objectStoreNames.contains(this.storeNames)) {
this.db.createObjectStore(this.storeNames, { keyPath: 'id' })
}
}
})
}
private async initLang() {
this.db!.transaction(this.storeNames, 'readwrite')
.objectStore(this.storeNames)
.add({ id: 1, lang: defaultLang })
}
async getLang() {
if (!this.db) await this.initDB()
return new Promise<string>((resolve) => {
const request = this.db!.transaction(this.storeNames)
.objectStore(this.storeNames)
.get(1)
request.onsuccess = () => {
if (request.result) {
resolve(request.result.lang)
} else {
this.initLang()
resolve(defaultLang)
}
}
request.onerror = () => {
resolve(defaultLang)
}
})
}
async setLang(lang: string) {
if (userPreferredLang !== lang) {
userPreferredLang = lang
cacheEntries = []
cacheManifestURLs = []
manifestURLs = []
if (!this.db) await this.initDB()
this.db!.transaction(this.storeNames, 'readwrite')
.objectStore(this.storeNames)
.put({ id: 1, lang })
}
}
}
async function initManifest() {
userPreferredLang = userPreferredLang || (await langDB.getLang())
// match the data that needs to be cached
// NOTE: When the structure of the document dist files changes, it needs to be changed here
const cacheList = [
userPreferredLang,
`assets/(${userPreferredLang}|app|index|style|chunks)`,
'images',
'android-chrome',
'apple-touch-icon',
'manifest.webmanifest',
]
const regExp = new RegExp(`^(${cacheList.join('|')})`)
for (const item of manifest) {
const url = new URL(item.url, self.location.origin)
manifestURLs.push(url.href)
if (regExp.test(item.url) || /^\/$/.test(item.url)) {
const request = new Request(url.href, { credentials: 'same-origin' })
cacheEntries.push(request)
cacheManifestURLs.push(url.href)
}
}
}
const langDB = new LangDB()
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(cacheName).then(async (cache) => {
if (!cacheEntries.length) await initManifest()
return cache.addAll(cacheEntries)
})
)
})
self.addEventListener('activate', (event: ExtendableEvent) => {
// clean up outdated runtime cache
event.waitUntil(
caches.open(cacheName).then(async (cache) => {
if (!cacheManifestURLs.length) await initManifest()
cache.keys().then((keys) => {
keys.forEach((request) => {
// clean up those who are not listed in cacheManifestURLs
!cacheManifestURLs.includes(request.url) && cache.delete(request)
})
})
})
)
})
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(async (response) => {
// when the cache is hit, it returns directly to the cache
if (response) return response
if (!manifestURLs.length) await initManifest()
const requestClone = event.request.clone()
// otherwise create a new fetch request
return fetch(requestClone)
.then((response) => {
const responseClone = response.clone()
if (response.type !== 'basic' && response.status !== 200) {
return response
}
// cache the data contained in the manifestURLs list
manifestURLs.includes(requestClone.url) &&
caches.open(cacheName).then((cache) => {
cache.put(requestClone, responseClone)
})
return response
})
.catch((err) => {
throw new Error(`Failed to load resource ${requestClone.url}, ${err}`)
})
})
)
})
self.addEventListener('message', (event) => {
if (event.data) {
if (event.data.type === 'SKIP_WAITING') {
self.skipWaiting()
} else if (event.data.type === 'LANG') {
langDB.setLang(event.data.lang)
}
}
})
clientsClaim()

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import { computed, watch } from 'vue'
import { useStorage } from '@vueuse/core'
import { useRegisterSW } from 'virtual:pwa-register/vue'
import { useLang } from '../composables/lang'
import pwaLocale from '../../i18n/component/pwa.json'
@ -7,15 +8,23 @@ import pwaLocale from '../../i18n/component/pwa.json'
const lang = useLang()
const locale = computed(() => pwaLocale[lang.value])
const { needRefresh, updateServiceWorker } = useRegisterSW()
const alwaysRefresh = useStorage('PWA_Always_Refresh', false)
watch(needRefresh, (value) => {
value && alwaysRefresh.value && updateServiceWorker()
})
</script>
<template>
<transition name="pwa-popup">
<el-card v-if="needRefresh" class="pwa-card" role="alert">
<el-card v-if="!alwaysRefresh && needRefresh" class="pwa-card" role="alert">
<p class="pwa-card-text">{{ locale.message }}</p>
<el-button type="primary" plain @click="updateServiceWorker()">
{{ locale.refresh }}
</el-button>
<el-button plain @click="alwaysRefresh = true">
{{ locale['always-refresh'] }}
</el-button>
<el-button plain @click="needRefresh = false">
{{ locale.close }}
</el-button>

View File

@ -1,6 +1,6 @@
import { computed } from 'vue'
import { useRoute, useRouter } from 'vitepress'
import { useStorage } from '@vueuse/core'
import { isClient, useStorage } from '@vueuse/core'
import { PREFERRED_LANG_KEY } from '../constant'
import langs from '../../i18n/lang.json'
@ -50,6 +50,13 @@ export const useTranslation = () => {
const goTo = `/${targetLang}/${route.path.slice(firstSlash + 1)}`
router.go(goTo)
if (isClient) {
navigator?.serviceWorker.controller?.postMessage({
type: 'LANG',
lang: targetLang,
})
}
}
return {

View File

@ -10,6 +10,7 @@
"strict": true,
"noImplicitAny": false,
"skipLibCheck": true,
"lib": ["WebWorker"],
"paths": {
"element-plus": ["../packages/element-plus"],
"~/*": ["./.vitepress/vitepress/*"]

View File

@ -99,8 +99,25 @@ export default defineConfig(async ({ mode }) => {
Inspect(),
mkcert(),
VitePWA({
strategies: 'injectManifest',
srcDir: '.vitepress',
filename: 'sw.ts',
outDir: '.vitepress/dist',
includeAssets: ['images/**'],
injectManifest: {
manifestTransforms: [
(manifest) => {
for (const item of manifest) {
if (item.url.endsWith('index.html')) {
const url = item.url.replace('index.html', '')
item.url = url ? url : '/'
}
}
return { manifest }
},
],
},
manifest: {
id: '/',
name: 'Element Plus',