sw.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import { cacheNames, clientsClaim } from 'workbox-core'
  2. import type { ManifestEntry } from 'workbox-build'
  3. declare let self: ServiceWorkerGlobalScope & {
  4. __WB_MANIFEST: ManifestEntry[]
  5. }
  6. const manifest = self.__WB_MANIFEST
  7. const cacheName = cacheNames.runtime
  8. const defaultLang = manifest.some(item => {
  9. return item.url.includes(navigator.language)
  10. })
  11. ? navigator.language
  12. : 'en-US'
  13. let userPreferredLang = ''
  14. let cacheEntries: RequestInfo[] = []
  15. let cacheManifestURLs: string[] = []
  16. let manifestURLs: string[] = []
  17. class LangDB {
  18. private db: IDBDatabase | undefined
  19. private databaseName = 'PWA_DB'
  20. private version = 1
  21. private storeNames = 'lang'
  22. constructor() {
  23. this.initDB()
  24. }
  25. private initDB() {
  26. return new Promise<boolean>(resolve => {
  27. const request = indexedDB.open(this.databaseName, this.version)
  28. request.onsuccess = event => {
  29. this.db = (event.target as IDBOpenDBRequest).result
  30. resolve(true)
  31. }
  32. request.onupgradeneeded = event => {
  33. this.db = (event.target as IDBOpenDBRequest).result
  34. if (!this.db.objectStoreNames.contains(this.storeNames)) {
  35. this.db.createObjectStore(this.storeNames, { keyPath: 'id' })
  36. }
  37. }
  38. })
  39. }
  40. private async initLang() {
  41. this.db!.transaction(this.storeNames, 'readwrite').objectStore(this.storeNames).add({ id: 1, lang: defaultLang })
  42. }
  43. async getLang() {
  44. if (!this.db) await this.initDB()
  45. return new Promise<string>(resolve => {
  46. const request = this.db!.transaction(this.storeNames).objectStore(this.storeNames).get(1)
  47. request.onsuccess = () => {
  48. if (request.result) {
  49. resolve(request.result.lang)
  50. } else {
  51. this.initLang()
  52. resolve(defaultLang)
  53. }
  54. }
  55. request.onerror = () => {
  56. resolve(defaultLang)
  57. }
  58. })
  59. }
  60. async setLang(lang: string) {
  61. if (userPreferredLang !== lang) {
  62. userPreferredLang = lang
  63. cacheEntries = []
  64. cacheManifestURLs = []
  65. manifestURLs = []
  66. if (!this.db) await this.initDB()
  67. this.db!.transaction(this.storeNames, 'readwrite').objectStore(this.storeNames).put({ id: 1, lang })
  68. }
  69. }
  70. }
  71. async function initManifest() {
  72. userPreferredLang = userPreferredLang || (await langDB.getLang())
  73. // match the data that needs to be cached
  74. // NOTE: When the structure of the document dist files changes, it needs to be changed here
  75. const cacheList = [userPreferredLang, `assets/(${userPreferredLang}|app|index|style|chunks)`, 'images', 'android-chrome', 'apple-touch-icon', 'manifest.webmanifest']
  76. const regExp = new RegExp(`^(${cacheList.join('|')})`)
  77. for (const item of manifest) {
  78. const url = new URL(item.url, self.location.origin)
  79. manifestURLs.push(url.href)
  80. if (regExp.test(item.url) || /^\/$/.test(item.url)) {
  81. const request = new Request(url.href, { credentials: 'same-origin' })
  82. cacheEntries.push(request)
  83. cacheManifestURLs.push(url.href)
  84. }
  85. }
  86. }
  87. const langDB = new LangDB()
  88. self.addEventListener('install', event => {
  89. event.waitUntil(
  90. caches.open(cacheName).then(async cache => {
  91. if (!cacheEntries.length) await initManifest()
  92. return cache.addAll(cacheEntries)
  93. })
  94. )
  95. })
  96. self.addEventListener('activate', (event: ExtendableEvent) => {
  97. // clean up outdated runtime cache
  98. event.waitUntil(
  99. caches.open(cacheName).then(async cache => {
  100. if (!cacheManifestURLs.length) await initManifest()
  101. cache.keys().then(keys => {
  102. keys.forEach(request => {
  103. // clean up those who are not listed in cacheManifestURLs
  104. !cacheManifestURLs.includes(request.url) && cache.delete(request)
  105. })
  106. })
  107. })
  108. )
  109. })
  110. self.addEventListener('fetch', event => {
  111. event.respondWith(
  112. caches.match(event.request).then(async response => {
  113. // when the cache is hit, it returns directly to the cache
  114. if (response) return response
  115. if (!manifestURLs.length) await initManifest()
  116. const requestClone = event.request.clone()
  117. // otherwise create a new fetch request
  118. return fetch(requestClone)
  119. .then(response => {
  120. const responseClone = response.clone()
  121. if (response.type !== 'basic' && response.status !== 200) {
  122. return response
  123. }
  124. // cache the data contained in the manifestURLs list
  125. manifestURLs.includes(requestClone.url) &&
  126. caches.open(cacheName).then(cache => {
  127. cache.put(requestClone, responseClone)
  128. })
  129. return response
  130. })
  131. .catch(err => {
  132. throw new Error(`Failed to load resource ${requestClone.url}, ${err}`)
  133. })
  134. })
  135. )
  136. })
  137. self.addEventListener('message', event => {
  138. if (event.data) {
  139. if (event.data.type === 'SKIP_WAITING') {
  140. self.skipWaiting()
  141. } else if (event.data.type === 'LANG') {
  142. langDB.setLang(event.data.lang)
  143. }
  144. }
  145. })
  146. clientsClaim()