Sfoglia il codice sorgente

自动更新代码

Zimo 1 settimana fa
parent
commit
09dc9d1c37
4 ha cambiato i file con 194 aggiunte e 0 eliminazioni
  1. 25 0
      build/vite/index.ts
  2. 5 0
      src/main.ts
  3. 110 0
      src/utils/appVersion.ts
  4. 54 0
      src/utils/reloadOnChunkError.ts

+ 25 - 0
build/vite/index.ts

@@ -15,6 +15,30 @@ import topLevelAwait from 'vite-plugin-top-level-await'
 import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
 import { createSvgIconsPlugin } from 'vite-plugin-svg-icons-ng'
 import UnoCSS from 'unocss/vite'
+import type { Plugin } from 'vite'
+
+const createAppVersionPlugin = (): Plugin => {
+  return {
+    name: 'app-version',
+    apply: 'build',
+    generateBundle() {
+      const buildTime = new Date().toISOString()
+
+      this.emitFile({
+        type: 'asset',
+        fileName: 'version.json',
+        source: JSON.stringify(
+          {
+            version: buildTime,
+            buildTime
+          },
+          null,
+          2
+        )
+      })
+    }
+  }
+}
 
 export function createVitePlugins() {
   const root = process.cwd()
@@ -88,6 +112,7 @@ export function createVitePlugins() {
       deleteOriginFile: false //压缩后是否删除源文件
     }),
     ViteEjsPlugin(),
+    createAppVersionPlugin(),
     topLevelAwait({
       // https://juejin.cn/post/7152191742513512485
       // The export name of top-level await promise for each chunk module

+ 5 - 0
src/main.ts

@@ -42,11 +42,16 @@ import './permission'
 import '@/plugins/tongji' // 百度统计
 import Logger from '@/utils/Logger'
 import mqttTool from '@/utils/mqttTool' // Mqtt工具
+import { setupAppVersionCheck } from '@/utils/appVersion'
+import { setupReloadOnChunkError } from '@/utils/reloadOnChunkError'
 
 import VueDOMPurifyHTML from 'vue-dompurify-html' // 解决v-html 的安全隐患
 
 import DataVVue3 from '@kjgl77/datav-vue3'
 
+setupReloadOnChunkError(router)
+setupAppVersionCheck(router)
+
 // 创建实例
 const setupAll = async () => {
   console.time('app:init')

+ 110 - 0
src/utils/appVersion.ts

@@ -0,0 +1,110 @@
+import { ElMessageBox } from 'element-plus'
+import type { Router } from 'vue-router'
+
+type AppVersion = {
+  version?: string
+  buildTime?: string
+}
+
+const CHECK_INTERVAL = 60 * 1000
+const VERSION_FILE = `${import.meta.env.BASE_URL}version.json`
+
+let currentVersion = ''
+let lastCheckAt = 0
+let checking = false
+let updateNotified = false
+let checkTimer: number | undefined
+
+const getVersionUrl = () => {
+  const versionUrl = new URL(VERSION_FILE, window.location.origin)
+  versionUrl.searchParams.set('t', String(Date.now()))
+  return versionUrl.toString()
+}
+
+const fetchAppVersion = async (): Promise<string> => {
+  const response = await fetch(getVersionUrl(), {
+    cache: 'no-store',
+    headers: {
+      'Cache-Control': 'no-cache'
+    }
+  })
+
+  if (!response.ok) return ''
+
+  const data = (await response.json()) as AppVersion
+  return data.version || data.buildTime || ''
+}
+
+const reloadPage = () => {
+  console.log('111', 111)
+  window.location.reload()
+}
+
+const notifyUpdate = () => {
+  if (updateNotified) return
+
+  updateNotified = true
+  ElMessageBox.confirm('系统已发布新版本,点击确定后将刷新页面。', '发现新版本', {
+    confirmButtonText: '确定',
+    cancelButtonText: '稍后',
+    type: 'warning',
+    closeOnClickModal: false,
+    closeOnPressEscape: false
+  })
+    .then(reloadPage)
+    .catch(() => {
+      updateNotified = false
+    })
+}
+
+const checkAppVersion = async (force = false) => {
+  if (import.meta.env.DEV || checking) return
+
+  const now = Date.now()
+  if (!force && now - lastCheckAt < CHECK_INTERVAL) return
+
+  checking = true
+  lastCheckAt = now
+
+  try {
+    const latestVersion = await fetchAppVersion()
+    if (!latestVersion) return
+
+    if (!currentVersion) {
+      currentVersion = latestVersion
+      return
+    }
+
+    if (latestVersion !== currentVersion) {
+      notifyUpdate()
+    }
+  } catch (error) {
+    console.warn('Failed to check app version:', error)
+  } finally {
+    checking = false
+  }
+}
+
+export const setupAppVersionCheck = (router: Router) => {
+  checkAppVersion(true)
+
+  router.beforeResolve(() => {
+    checkAppVersion()
+  })
+
+  window.addEventListener('focus', () => {
+    checkAppVersion(true)
+  })
+
+  document.addEventListener('visibilitychange', () => {
+    if (document.visibilityState === 'visible') {
+      checkAppVersion(true)
+    }
+  })
+
+  if (!checkTimer) {
+    checkTimer = window.setInterval(() => {
+      checkAppVersion()
+    }, CHECK_INTERVAL)
+  }
+}

+ 54 - 0
src/utils/reloadOnChunkError.ts

@@ -0,0 +1,54 @@
+import type { Router } from 'vue-router'
+
+const RELOAD_STORAGE_KEY = 'app:chunk-load-reload'
+const RELOAD_INTERVAL = 30 * 1000
+
+const chunkLoadErrorPatterns = [
+  'Failed to fetch dynamically imported module',
+  'Importing a module script failed',
+  'Loading CSS chunk',
+  'Unable to preload CSS',
+  'ChunkLoadError',
+  'CSS_CHUNK_LOAD_FAILED'
+]
+
+const getErrorMessage = (error: unknown): string => {
+  if (typeof error === 'string') return error
+  if (error instanceof Error) return error.message
+
+  const maybeError = error as { message?: unknown; reason?: unknown }
+  if (typeof maybeError?.message === 'string') return maybeError.message
+  if (typeof maybeError?.reason === 'string') return maybeError.reason
+  if (maybeError?.reason instanceof Error) return maybeError.reason.message
+
+  return ''
+}
+
+const isChunkLoadError = (error: unknown): boolean => {
+  const message = getErrorMessage(error)
+  return chunkLoadErrorPatterns.some((pattern) => message.includes(pattern))
+}
+
+const reloadPageOnce = () => {
+  const now = Date.now()
+  const lastReloadAt = Number(sessionStorage.getItem(RELOAD_STORAGE_KEY) || 0)
+
+  if (now - lastReloadAt < RELOAD_INTERVAL) return
+
+  sessionStorage.setItem(RELOAD_STORAGE_KEY, String(now))
+  console.log('222', 222)
+  window.location.reload()
+}
+
+export const setupReloadOnChunkError = (router: Router) => {
+  window.addEventListener('vite:preloadError', (event) => {
+    event.preventDefault()
+    reloadPageOnce()
+  })
+
+  router.onError((error) => {
+    if (isChunkLoadError(error)) {
+      reloadPageOnce()
+    }
+  })
+}