Handle asset preload failures with auto reload

This commit is contained in:
zhx 2026-07-02 14:49:34 +08:00
parent 18ad13563b
commit c836d0f702
2 changed files with 129 additions and 5 deletions

View File

@ -1,15 +1,107 @@
import { Component } from "react";
import styles from "./ErrorBoundary.module.css";
export class ErrorBoundary extends Component {
state = { error: null };
const ASSET_RELOAD_STORAGE_PREFIX = "hyapp-admin:asset-reload:";
const ASSET_RELOAD_RETRY_MS = 5 * 60 * 1000;
const ASSET_ERROR_PATTERNS = [
/Unable to preload CSS/i,
/Failed to fetch dynamically imported module/i,
/error loading dynamically imported module/i,
/Importing a module script failed/i,
/Load failed for the module/i,
/ChunkLoadError/i,
/CSS_CHUNK_LOAD_FAILED/i,
];
static getDerivedStateFromError(error) {
return { error };
export function isRecoverableAssetLoadError(error) {
const message = errorMessage(error);
return ASSET_ERROR_PATTERNS.some((pattern) => pattern.test(message));
}
export function assetReloadStorageKey(error, location = window.location) {
const message = errorMessage(error);
const assetMatch = message.match(/\/assets\/[^\s"')]+/);
const asset = assetMatch?.[0] || message.slice(0, 160) || "unknown";
return `${ASSET_RELOAD_STORAGE_PREFIX}${location.pathname}:${asset}`;
}
function errorMessage(error) {
if (!error) {
return "";
}
if (typeof error === "string") {
return error;
}
return [error.name, error.message, error.stack].filter(Boolean).join("\n");
}
function canRetryAssetReload(error) {
const key = assetReloadStorageKey(error);
const now = Date.now();
try {
const previous = Number(window.sessionStorage.getItem(key) || "0");
if (previous && now - previous < ASSET_RELOAD_RETRY_MS) {
return false;
}
window.sessionStorage.setItem(key, String(now));
return true;
} catch {
if (window.__hyappAssetReloadAttempted) {
return false;
}
window.__hyappAssetReloadAttempted = true;
return true;
}
}
function scheduleAssetReload(error) {
if (!isRecoverableAssetLoadError(error) || !canRetryAssetReload(error)) {
return false;
}
window.setTimeout(() => {
window.location.reload();
}, 50);
return true;
}
export class ErrorBoundary extends Component {
state = { error: null, recovering: false };
static getDerivedStateFromError(error) {
return { error, recovering: isRecoverableAssetLoadError(error) };
}
componentDidMount() {
window.addEventListener("vite:preloadError", this.handlePreloadError);
}
componentWillUnmount() {
window.removeEventListener("vite:preloadError", this.handlePreloadError);
}
componentDidCatch(error) {
if (!scheduleAssetReload(error) && isRecoverableAssetLoadError(error)) {
this.setState({ recovering: false });
}
}
handlePreloadError = (event) => {
const error = event.payload || event.reason || event.error;
if (!scheduleAssetReload(error)) {
return;
}
event.preventDefault();
this.setState({ error, recovering: true });
};
reset = () => {
this.setState({ error: null });
this.setState({ error: null, recovering: false });
};
reload = () => {
@ -21,6 +113,17 @@ export class ErrorBoundary extends Component {
return this.props.children;
}
if (this.state.recovering) {
return (
<main className={styles.root}>
<section className={styles.panel}>
<h1 className={styles.title}>正在刷新页面</h1>
<p className={styles.message}>正在加载最新版本</p>
</section>
</main>
);
}
return (
<main className={styles.root}>
<section className={styles.panel}>

View File

@ -0,0 +1,21 @@
import { expect, test } from "vitest";
import { assetReloadStorageKey, isRecoverableAssetLoadError } from "./ErrorBoundary.jsx";
test("detects Vite CSS preload failures as recoverable asset errors", () => {
expect(isRecoverableAssetLoadError(new Error("Unable to preload CSS for /assets/HostOrgTable-DWK6sgCN.css"))).toBe(
true,
);
expect(isRecoverableAssetLoadError(new Error("Failed to fetch dynamically imported module"))).toBe(true);
});
test("keeps ordinary runtime errors visible", () => {
expect(isRecoverableAssetLoadError(new Error("Cannot read properties of undefined"))).toBe(false);
});
test("scopes asset reload attempts by route and asset", () => {
const key = assetReloadStorageKey(new Error("Unable to preload CSS for /assets/HostOrgTable-DWK6sgCN.css"), {
pathname: "/host/managers",
});
expect(key).toContain("/host/managers:/assets/HostOrgTable-DWK6sgCN.css");
});