diff --git a/apps/admin-web/nginx.conf b/apps/admin-web/nginx.conf index fa9b170..5dffb0b 100644 --- a/apps/admin-web/nginx.conf +++ b/apps/admin-web/nginx.conf @@ -22,6 +22,13 @@ server { try_files $uri $uri/ /index.html; } + # Never cache the SPA entrypoint — chunk hashes change every deploy and a + # stale index.html keeps users stuck on 404'd chunks even after reload. + location = /index.html { + add_header Cache-Control "no-cache, no-store, must-revalidate" always; + expires -1; + } + # Proxy API requests to the backend location /api { proxy_pass http://tenant-api:8080; diff --git a/apps/ops-web/nginx.conf b/apps/ops-web/nginx.conf index 1f1a8e7..a15e770 100644 --- a/apps/ops-web/nginx.conf +++ b/apps/ops-web/nginx.conf @@ -16,6 +16,13 @@ server { try_files $uri $uri/ /index.html; } + # Never cache the SPA entrypoint — chunk hashes change every deploy and a + # stale index.html keeps users stuck on 404'd chunks even after reload. + location = /index.html { + add_header Cache-Control "no-cache, no-store, must-revalidate" always; + expires -1; + } + location /api { proxy_pass http://ops-api:8090; proxy_http_version 1.1; diff --git a/apps/ops-web/src/lib/chunk-reload.ts b/apps/ops-web/src/lib/chunk-reload.ts new file mode 100644 index 0000000..d253664 --- /dev/null +++ b/apps/ops-web/src/lib/chunk-reload.ts @@ -0,0 +1,54 @@ +const RELOAD_FLAG_KEY = '__chunk_reload_at' +const RELOAD_COOLDOWN_MS = 10_000 + +function shouldReload(): boolean { + try { + const raw = sessionStorage.getItem(RELOAD_FLAG_KEY) + if (!raw) return true + const at = Number(raw) + if (!Number.isFinite(at)) return true + return Date.now() - at > RELOAD_COOLDOWN_MS + } catch { + return true + } +} + +function markReload(): void { + try { + sessionStorage.setItem(RELOAD_FLAG_KEY, String(Date.now())) + } catch { + // ignore storage failures (private mode, quota) + } +} + +export function reloadForStaleChunk(): boolean { + if (!shouldReload()) return false + markReload() + location.reload() + return true +} + +const CHUNK_ERROR_PATTERNS = [ + /Failed to fetch dynamically imported module/i, + /error loading dynamically imported module/i, + /Importing a module script failed/i, + /Unable to preload CSS/i, +] + +export function isChunkLoadError(err: unknown): boolean { + const msg = + err instanceof Error + ? err.message + : typeof err === 'string' + ? err + : '' + return CHUNK_ERROR_PATTERNS.some((re) => re.test(msg)) +} + +export function registerChunkReloadHandlers(): void { + window.addEventListener('vite:preloadError', (event) => { + if (reloadForStaleChunk()) { + event.preventDefault() + } + }) +} diff --git a/apps/ops-web/src/main.ts b/apps/ops-web/src/main.ts index 1d42906..d6b468a 100644 --- a/apps/ops-web/src/main.ts +++ b/apps/ops-web/src/main.ts @@ -36,6 +36,7 @@ import { } from 'ant-design-vue' import { createApp } from 'vue' +import { registerChunkReloadHandlers } from '@/lib/chunk-reload' import { bindUnauthorizedHandler } from '@/lib/http' import { router } from '@/router' import { useAuthStore } from '@/stores/auth' @@ -44,6 +45,8 @@ import 'ant-design-vue/dist/reset.css' import App from './App.vue' import './styles.css' +registerChunkReloadHandlers() + const queryClient = new QueryClient({ defaultOptions: { queries: { diff --git a/apps/ops-web/src/router/index.ts b/apps/ops-web/src/router/index.ts index 48b21f3..0a370f0 100644 --- a/apps/ops-web/src/router/index.ts +++ b/apps/ops-web/src/router/index.ts @@ -1,5 +1,6 @@ import { createRouter, createWebHistory } from 'vue-router' +import { isChunkLoadError, reloadForStaleChunk } from '@/lib/chunk-reload' import { useAuthStore } from '@/stores/auth' import { pinia } from '@/stores/pinia' @@ -143,3 +144,9 @@ router.afterEach((to) => { document.title = `${t} · 省心推` } }) + +router.onError((err) => { + if (isChunkLoadError(err)) { + reloadForStaleChunk() + } +})