fix(web): harden route transitions and monitor memory
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { IconAlertTriangle, IconHome, IconRefresh } from '@douyinfe/semi-icons';
|
||||
import { Component, type ElementType, type ErrorInfo, type ReactNode, Suspense } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { PageLoading } from '../shared/AsyncState';
|
||||
|
||||
const CHUNK_RELOAD_KEY = 'vehicle-platform:route-chunk-reload';
|
||||
const CHUNK_RELOAD_COOLDOWN_MS = 2 * 60_000;
|
||||
|
||||
export function isRouteChunkError(error: unknown) {
|
||||
const message = error instanceof Error ? `${error.name} ${error.message}` : String(error);
|
||||
return /ChunkLoadError|Loading chunk .+ failed|Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module/i.test(message);
|
||||
}
|
||||
|
||||
function canAutoReload(routeKey: string) {
|
||||
try {
|
||||
const marker = JSON.parse(window.sessionStorage.getItem(CHUNK_RELOAD_KEY) ?? 'null') as { routeKey?: string; at?: number } | null;
|
||||
return !marker || marker.routeKey !== routeKey || Date.now() - (marker.at ?? 0) > CHUNK_RELOAD_COOLDOWN_MS;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function rememberAutoReload(routeKey: string) {
|
||||
try {
|
||||
window.sessionStorage.setItem(CHUNK_RELOAD_KEY, JSON.stringify({ routeKey, at: Date.now() }));
|
||||
} catch {
|
||||
// Recovery must still work when storage is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
class RecoverableRouteBoundary extends Component<{ children: ReactNode; routeKey: string }, { error?: Error }> {
|
||||
state: { error?: Error } = {};
|
||||
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, _info: ErrorInfo) {
|
||||
if (isRouteChunkError(error) && canAutoReload(this.props.routeKey)) {
|
||||
rememberAutoReload(this.props.routeKey);
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { error } = this.state;
|
||||
if (!error) return this.props.children;
|
||||
const chunkError = isRouteChunkError(error);
|
||||
return <section className="v2-route-error" role="alert">
|
||||
<span><IconAlertTriangle /></span>
|
||||
<div>
|
||||
<small>{chunkError ? '检测到页面版本更新' : '页面运行异常'}</small>
|
||||
<h2>{chunkError ? '正在等待加载最新页面资源' : '当前模块暂时无法显示'}</h2>
|
||||
<p>{chunkError ? '通常是发布后旧标签页仍引用上一版本资源。刷新后会恢复,不会丢失服务端数据。' : '页面已被安全隔离,侧栏和其他模块仍可使用。可刷新当前页面重试。'}</p>
|
||||
<code>{error.message || error.name}</code>
|
||||
<footer>
|
||||
<button type="button" onClick={() => window.location.reload()}><IconRefresh />刷新当前页面</button>
|
||||
<a href="/monitor"><IconHome />返回全局监控</a>
|
||||
</footer>
|
||||
</div>
|
||||
</section>;
|
||||
}
|
||||
}
|
||||
|
||||
export function RoutePage({ page: Page, label }: { page: ElementType; label: string }) {
|
||||
const location = useLocation();
|
||||
const routeKey = `${location.pathname}${location.search}`;
|
||||
return <RecoverableRouteBoundary key={routeKey} routeKey={routeKey}>
|
||||
<Suspense fallback={<PageLoading label={`正在加载${label}`} />}><Page /></Suspense>
|
||||
</RecoverableRouteBoundary>;
|
||||
}
|
||||
Reference in New Issue
Block a user