sw.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  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')
  42. .objectStore(this.storeNames)
  43. .add({ id: 1, lang: defaultLang })
  44. }
  45. async getLang() {
  46. if (!this.db) await this.initDB()
  47. return new Promise<string>((resolve) => {
  48. const request = this.db!.transaction(this.storeNames)
  49. .objectStore(this.storeNames)
  50. .get(1)
  51. request.onsuccess = () => {
  52. if (request.result) {
  53. resolve(request.result.lang)
  54. } else {
  55. this.initLang()
  56. resolve(defaultLang)
  57. }
  58. }
  59. request.onerror = () => {
  60. resolve(defaultLang)
  61. }
  62. })
  63. }
  64. async setLang(lang: string) {
  65. if (userPreferredLang !== lang) {
  66. userPreferredLang = lang
  67. cacheEntries = []
  68. cacheManifestURLs = []
  69. manifestURLs = []
  70. if (!this.db) await this.initDB()
  71. this.db!.transaction(this.storeNames, 'readwrite')
  72. .objectStore(this.storeNames)
  73. .put({ id: 1, lang })
  74. }
  75. }
  76. }
  77. async function initManifest() {
  78. userPreferredLang = userPreferredLang || (await langDB.getLang())
  79. // match the data that needs to be cached
  80. // NOTE: When the structure of the document dist files changes, it needs to be changed here
  81. const cacheList = [
  82. userPreferredLang,
  83. `assets/(${userPreferredLang}|app|index|style|chunks)`,
  84. 'images',
  85. 'android-chrome',
  86. 'apple-touch-icon',
  87. 'manifest.webmanifest',
  88. ]
  89. const regExp = new RegExp(`^(${cacheList.join('|')})`)
  90. for (const item of manifest) {
  91. const url = new URL(item.url, self.location.origin)
  92. manifestURLs.push(url.href)
  93. if (regExp.test(item.url) || /^\/$/.test(item.url)) {
  94. const request = new Request(url.href, { credentials: 'same-origin' })
  95. cacheEntries.push(request)
  96. cacheManifestURLs.push(url.href)
  97. }
  98. }
  99. }
  100. const langDB = new LangDB()
  101. self.addEventListener('install', (event) => {
  102. event.waitUntil(
  103. caches.open(cacheName).then(async (cache) => {
  104. if (!cacheEntries.length) await initManifest()
  105. return cache.addAll(cacheEntries)
  106. })
  107. )
  108. })
  109. self.addEventListener('activate', (event: ExtendableEvent) => {
  110. // clean up outdated runtime cache
  111. event.waitUntil(
  112. caches.open(cacheName).then(async (cache) => {
  113. if (!cacheManifestURLs.length) await initManifest()
  114. cache.keys().then((keys) => {
  115. keys.forEach((request) => {
  116. // clean up those who are not listed in cacheManifestURLs
  117. !cacheManifestURLs.includes(request.url) && cache.delete(request)
  118. })
  119. })
  120. })
  121. )
  122. })
  123. self.addEventListener('fetch', (event) => {
  124. event.respondWith(
  125. caches.match(event.request).then(async (response) => {
  126. // when the cache is hit, it returns directly to the cache
  127. if (response) return response
  128. if (!manifestURLs.length) await initManifest()
  129. const requestClone = event.request.clone()
  130. // otherwise create a new fetch request
  131. return fetch(requestClone)
  132. .then((response) => {
  133. const responseClone = response.clone()
  134. if (response.type !== 'basic' && response.status !== 200) {
  135. return response
  136. }
  137. // cache the data contained in the manifestURLs list
  138. manifestURLs.includes(requestClone.url) &&
  139. caches.open(cacheName).then((cache) => {
  140. cache.put(requestClone, responseClone)
  141. })
  142. return response
  143. })
  144. .catch((err) => {
  145. throw new Error(`Failed to load resource ${requestClone.url}, ${err}`)
  146. })
  147. })
  148. )
  149. })
  150. self.addEventListener('message', (event) => {
  151. if (event.data) {
  152. if (event.data.type === 'SKIP_WAITING') {
  153. self.skipWaiting()
  154. } else if (event.data.type === 'LANG') {
  155. langDB.setLang(event.data.lang)
  156. }
  157. }
  158. })
  159. clientsClaim()