Handle asset preload failures with auto reload
This commit is contained in:
parent
18ad13563b
commit
c836d0f702
@ -1,15 +1,107 @@
|
|||||||
import { Component } from "react";
|
import { Component } from "react";
|
||||||
import styles from "./ErrorBoundary.module.css";
|
import styles from "./ErrorBoundary.module.css";
|
||||||
|
|
||||||
export class ErrorBoundary extends Component {
|
const ASSET_RELOAD_STORAGE_PREFIX = "hyapp-admin:asset-reload:";
|
||||||
state = { error: null };
|
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) {
|
export function isRecoverableAssetLoadError(error) {
|
||||||
return { 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 = () => {
|
reset = () => {
|
||||||
this.setState({ error: null });
|
this.setState({ error: null, recovering: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
reload = () => {
|
reload = () => {
|
||||||
@ -21,6 +113,17 @@ export class ErrorBoundary extends Component {
|
|||||||
return this.props.children;
|
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 (
|
return (
|
||||||
<main className={styles.root}>
|
<main className={styles.root}>
|
||||||
<section className={styles.panel}>
|
<section className={styles.panel}>
|
||||||
|
|||||||
21
src/app/ErrorBoundary.test.jsx
Normal file
21
src/app/ErrorBoundary.test.jsx
Normal 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");
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user