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 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}>
|
||||
|
||||
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