Port admin-web's chunk-reload guard to ops-web (vite:preloadError + router.onError) so a stale tab landing on a lazy-loaded route triggers a single reload instead of a hard 404. Also force index.html through revalidation on both apps so the reload actually fetches a fresh entry referencing the new chunk hashes; hashed asset URLs keep their long immutable cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,13 @@ server {
|
|||||||
try_files $uri $uri/ /index.html;
|
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
|
# Proxy API requests to the backend
|
||||||
location /api {
|
location /api {
|
||||||
proxy_pass http://tenant-api:8080;
|
proxy_pass http://tenant-api:8080;
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ server {
|
|||||||
try_files $uri $uri/ /index.html;
|
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 {
|
location /api {
|
||||||
proxy_pass http://ops-api:8090;
|
proxy_pass http://ops-api:8090;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
} from 'ant-design-vue'
|
} from 'ant-design-vue'
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
|
|
||||||
|
import { registerChunkReloadHandlers } from '@/lib/chunk-reload'
|
||||||
import { bindUnauthorizedHandler } from '@/lib/http'
|
import { bindUnauthorizedHandler } from '@/lib/http'
|
||||||
import { router } from '@/router'
|
import { router } from '@/router'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
@@ -44,6 +45,8 @@ import 'ant-design-vue/dist/reset.css'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import './styles.css'
|
import './styles.css'
|
||||||
|
|
||||||
|
registerChunkReloadHandlers()
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
|
import { isChunkLoadError, reloadForStaleChunk } from '@/lib/chunk-reload'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { pinia } from '@/stores/pinia'
|
import { pinia } from '@/stores/pinia'
|
||||||
|
|
||||||
@@ -143,3 +144,9 @@ router.afterEach((to) => {
|
|||||||
document.title = `${t} · 省心推`
|
document.title = `${t} · 省心推`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.onError((err) => {
|
||||||
|
if (isChunkLoadError(err)) {
|
||||||
|
reloadForStaleChunk()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user