优化
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const recoverySource = readFileSync(new URL('./staticAssetRecovery.ts', import.meta.url), 'utf8')
|
||||
const mainSource = readFileSync(new URL('../main.ts', import.meta.url), 'utf8')
|
||||
|
||||
test('refreshes once when a deployment invalidates lazy chunks or same-origin scripts and styles', () => {
|
||||
assert.match(mainSource, /installStaticAssetRecovery\(router\)/)
|
||||
assert.match(recoverySource, /vite:preloadError/)
|
||||
assert.match(recoverySource, /router\.onError/)
|
||||
assert.match(recoverySource, /HTMLScriptElement/)
|
||||
assert.match(recoverySource, /HTMLLinkElement/)
|
||||
assert.match(recoverySource, /window\.location\.reload\(\)/)
|
||||
assert.match(recoverySource, /RECOVERY_COOLDOWN_MS/)
|
||||
assert.match(recoverySource, /previous\?\.href === window\.location\.href/)
|
||||
})
|
||||
|
||||
test('does not turn ordinary application errors or third-party asset failures into refresh loops', () => {
|
||||
assert.match(recoverySource, /isStaticAssetLoadError\(error\)/)
|
||||
assert.match(recoverySource, /origin !== window\.location\.origin/)
|
||||
assert.match(recoverySource, /sessionStorage/)
|
||||
assert.match(recoverySource, /setTimeout\(clearRecoveryRecord, RECOVERY_STABLE_MS\)/)
|
||||
})
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { Router } from 'vue-router'
|
||||
|
||||
const RECOVERY_KEY = 'client-static-asset-recovery'
|
||||
const RECOVERY_COOLDOWN_MS = 60_000
|
||||
const RECOVERY_STABLE_MS = 15_000
|
||||
|
||||
interface RecoveryRecord {
|
||||
href: string
|
||||
refreshedAt: number
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown) {
|
||||
if (error instanceof Error) return `${error.name}: ${error.message}`
|
||||
return typeof error === 'string' ? error : ''
|
||||
}
|
||||
|
||||
export function isStaticAssetLoadError(error: unknown) {
|
||||
return /ChunkLoadError|Loading chunk [\w-]+ failed|Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module|Unable to preload CSS/i.test(errorMessage(error))
|
||||
}
|
||||
|
||||
function readRecoveryRecord(): RecoveryRecord | null {
|
||||
try {
|
||||
const raw = window.sessionStorage.getItem(RECOVERY_KEY)
|
||||
if (!raw) return null
|
||||
const value = JSON.parse(raw) as Partial<RecoveryRecord>
|
||||
return typeof value.href === 'string' && typeof value.refreshedAt === 'number'
|
||||
? { href: value.href, refreshedAt: value.refreshedAt }
|
||||
: null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function writeRecoveryRecord(record: RecoveryRecord) {
|
||||
try {
|
||||
window.sessionStorage.setItem(RECOVERY_KEY, JSON.stringify(record))
|
||||
return true
|
||||
} catch {
|
||||
// Without a persistent guard, reloading could loop while the deployment is still unavailable.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function clearRecoveryRecord() {
|
||||
try {
|
||||
window.sessionStorage.removeItem(RECOVERY_KEY)
|
||||
} catch {
|
||||
// Storage availability must not affect application startup.
|
||||
}
|
||||
}
|
||||
|
||||
function recoverFromStaticAssetFailure(error: unknown, confirmedAssetFailure = false) {
|
||||
if (!confirmedAssetFailure && !isStaticAssetLoadError(error)) return false
|
||||
const now = Date.now()
|
||||
const previous = readRecoveryRecord()
|
||||
if (previous?.href === window.location.href && now - previous.refreshedAt < RECOVERY_COOLDOWN_MS) return false
|
||||
if (!writeRecoveryRecord({ href: window.location.href, refreshedAt: now })) return false
|
||||
window.location.reload()
|
||||
return true
|
||||
}
|
||||
|
||||
function failedApplicationAsset(event: Event) {
|
||||
const target = event.target
|
||||
if (target instanceof HTMLScriptElement) return target.src
|
||||
if (target instanceof HTMLLinkElement && ['stylesheet', 'modulepreload', 'preload'].includes(target.rel)) return target.href
|
||||
return ''
|
||||
}
|
||||
|
||||
export function installStaticAssetRecovery(router: Router) {
|
||||
window.addEventListener('vite:preloadError', (event) => {
|
||||
const preloadEvent = event as Event & { payload?: unknown }
|
||||
if (recoverFromStaticAssetFailure(preloadEvent.payload, true)) preloadEvent.preventDefault()
|
||||
})
|
||||
|
||||
window.addEventListener('error', (event) => {
|
||||
const assetUrl = failedApplicationAsset(event)
|
||||
if (!assetUrl) return
|
||||
try {
|
||||
if (new URL(assetUrl, window.location.href).origin !== window.location.origin) return
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
recoverFromStaticAssetFailure(assetUrl, true)
|
||||
}, true)
|
||||
|
||||
router.onError((error) => {
|
||||
recoverFromStaticAssetFailure(error)
|
||||
})
|
||||
|
||||
window.setTimeout(clearRecoveryRecord, RECOVERY_STABLE_MS)
|
||||
}
|
||||
Reference in New Issue
Block a user