fix(web): preserve route state and cancel stale reads

This commit is contained in:
lingniu
2026-07-16 00:29:47 +08:00
parent 53e1b57e86
commit 6506c0f1f0
21 changed files with 166 additions and 62 deletions

View File

@@ -1,7 +1,9 @@
import { cleanup, render, screen } from '@testing-library/react';
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { MemoryRouter } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { MemoryRouter, useSearchParams } from 'react-router-dom';
import { isRouteChunkError, RoutePage } from './RouteBoundary';
import { ROUTER_FUTURE } from './routerConfig';
afterEach(() => {
cleanup();
@@ -19,7 +21,7 @@ describe('route recovery boundary', () => {
it('isolates a page render failure and keeps a visible recovery action', () => {
vi.spyOn(console, 'error').mockImplementation(() => undefined);
const BrokenPage = () => { throw new Error('render exploded'); };
render(<MemoryRouter initialEntries={['/history']}><RoutePage page={BrokenPage} label="历史数据" /></MemoryRouter>);
render(<MemoryRouter future={ROUTER_FUTURE} initialEntries={['/history']}><RoutePage page={BrokenPage} label="历史数据" /></MemoryRouter>);
expect(screen.getByRole('alert')).toHaveTextContent('当前模块暂时无法显示');
expect(screen.getByRole('button', { name: /刷新当前页面/ })).toBeInTheDocument();
expect(screen.getByRole('link', { name: /返回全局监控/ })).toHaveAttribute('href', '/monitor');
@@ -29,8 +31,23 @@ describe('route recovery boundary', () => {
vi.spyOn(console, 'error').mockImplementation(() => undefined);
window.sessionStorage.setItem('vehicle-platform:route-chunk-reload', JSON.stringify({ routeKey: '/statistics', at: Date.now() }));
const StaleChunkPage = () => { throw new TypeError('Failed to fetch dynamically imported module: /assets/Statistics-old.js'); };
render(<MemoryRouter initialEntries={['/statistics']}><RoutePage page={StaleChunkPage} label="里程查询" /></MemoryRouter>);
render(<MemoryRouter future={ROUTER_FUTURE} initialEntries={['/statistics']}><RoutePage page={StaleChunkPage} label="里程查询" /></MemoryRouter>);
expect(screen.getByRole('alert')).toHaveTextContent('检测到页面版本更新');
expect(screen.getByText(/旧标签页仍引用上一版本资源/)).toBeInTheDocument();
});
it('keeps page state mounted when only URL search parameters change', () => {
let mounts = 0;
function FilterablePage() {
const [count, setCount] = useState(0);
const [, setSearchParams] = useSearchParams();
useEffect(() => { mounts += 1; }, []);
return <><strong> {count}</strong><button onClick={() => setCount((value) => value + 1)}></button><button onClick={() => setSearchParams({ page: '2' })}></button></>;
}
render(<MemoryRouter future={ROUTER_FUTURE} initialEntries={['/alerts']}><RoutePage page={FilterablePage} label="告警中心" /></MemoryRouter>);
fireEvent.click(screen.getByRole('button', { name: '修改本地状态' }));
fireEvent.click(screen.getByRole('button', { name: '修改查询参数' }));
expect(screen.getByText('本地状态 1')).toBeInTheDocument();
expect(mounts).toBe(1);
});
});

View File

@@ -65,7 +65,7 @@ class RecoverableRouteBoundary extends Component<{ children: ReactNode; routeKey
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}>
return <RecoverableRouteBoundary key={location.pathname} routeKey={routeKey}>
<Suspense fallback={<PageLoading label={`正在加载${label}`} />}><Page /></Suspense>
</RecoverableRouteBoundary>;
}

View File

@@ -0,0 +1,4 @@
export const ROUTER_FUTURE = {
v7_startTransition: true,
v7_relativeSplatPath: true
} as const;