fix(web): Stale-Chunk-Fehler nach Deploy automatisch abfangen
"Failed to fetch dynamically imported module" trat auf, wenn ein offener Tab nach einem Deploy auf alte (gehashte, inzwischen gelöschte) Route-Chunks zeigt. Jetzt: - lazyWithReload() lädt bei fehlgeschlagenem Lazy-Import einmal automatisch neu (holt frische index.html); sessionStorage-Guard verhindert Reload-Schleifen. - vite:preloadError-Event wird ebenfalls abgefangen (modulepreload-Fehler). - errorElement (RouteError) ersetzt die Dev-Default-Fehlerseite durch eine saubere "Neu laden"-Ansicht als Fallback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
02bd682fa5
commit
040dd62735
1 file changed
+71
-12
+71
-12
@@ -1,24 +1,67 @@
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { lazy, Suspense, type ComponentType } from 'react';
|
||||
import { AppShell } from '../components/layout/AppShell';
|
||||
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
||||
import { Button } from '../components/ui/Button';
|
||||
|
||||
const Dashboard = lazy(() => import('../routes/dashboard').then((m) => ({ default: m.DashboardRoute })));
|
||||
const Heute = lazy(() =>
|
||||
import('../routes/heute').then((m) => ({ default: m.HeuteRoute })),
|
||||
// Nach einem Deploy zeigen bereits offene Tabs auf alte, inzwischen gelöschte
|
||||
// Chunk-Dateien (gehashte Dateinamen). Der nächste Lazy-Import schlägt dann fehl
|
||||
// („Failed to fetch dynamically imported module"). Ein einmaliger Reload holt die
|
||||
// frische index.html mit den neuen Hashes. sessionStorage-Guard verhindert eine
|
||||
// Reload-Schleife, falls der Fehler eine andere Ursache hat (z. B. offline).
|
||||
const RELOAD_KEY = 'ebw:chunk-reload';
|
||||
|
||||
function reloadOnceForStaleChunk(): boolean {
|
||||
const last = Number(sessionStorage.getItem(RELOAD_KEY) ?? '0');
|
||||
if (Date.now() - last > 10_000) {
|
||||
sessionStorage.setItem(RELOAD_KEY, String(Date.now()));
|
||||
window.location.reload();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function lazyWithReload<T extends ComponentType>(factory: () => Promise<{ default: T }>) {
|
||||
return lazy(() =>
|
||||
factory().catch((err: unknown) => {
|
||||
if (reloadOnceForStaleChunk()) {
|
||||
// Hängt bis der Reload greift — vermeidet ein kurzes Aufblitzen des Fehlers.
|
||||
return new Promise<{ default: T }>(() => {});
|
||||
}
|
||||
throw err;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Vite feuert dieses Event bei fehlgeschlagenem modulepreload (gleiche Ursache).
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('vite:preloadError', (e) => {
|
||||
if (reloadOnceForStaleChunk()) e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
const Dashboard = lazyWithReload(() =>
|
||||
import('../routes/dashboard').then((m) => ({ default: m.DashboardRoute })),
|
||||
);
|
||||
const Reflexion = lazy(() =>
|
||||
const Heute = lazyWithReload(() => import('../routes/heute').then((m) => ({ default: m.HeuteRoute })));
|
||||
const Reflexion = lazyWithReload(() =>
|
||||
import('../routes/reflexion').then((m) => ({ default: m.ReflexionRoute })),
|
||||
);
|
||||
const Bereich = lazy(() => import('../routes/bereich').then((m) => ({ default: m.BereichRoute })));
|
||||
const Projekt = lazy(() => import('../routes/projekt').then((m) => ({ default: m.ProjektRoute })));
|
||||
const Leads = lazy(() => import('../routes/leads').then((m) => ({ default: m.LeadsRoute })));
|
||||
const Lead = lazy(() => import('../routes/lead').then((m) => ({ default: m.LeadRoute })));
|
||||
const Archiv = lazy(() => import('../routes/archiv').then((m) => ({ default: m.ArchivRoute })));
|
||||
const NotFound = lazy(() =>
|
||||
const Bereich = lazyWithReload(() =>
|
||||
import('../routes/bereich').then((m) => ({ default: m.BereichRoute })),
|
||||
);
|
||||
const Projekt = lazyWithReload(() =>
|
||||
import('../routes/projekt').then((m) => ({ default: m.ProjektRoute })),
|
||||
);
|
||||
const Leads = lazyWithReload(() => import('../routes/leads').then((m) => ({ default: m.LeadsRoute })));
|
||||
const Lead = lazyWithReload(() => import('../routes/lead').then((m) => ({ default: m.LeadRoute })));
|
||||
const Archiv = lazyWithReload(() =>
|
||||
import('../routes/archiv').then((m) => ({ default: m.ArchivRoute })),
|
||||
);
|
||||
const NotFound = lazyWithReload(() =>
|
||||
import('../routes/not-found').then((m) => ({ default: m.NotFoundRoute })),
|
||||
);
|
||||
const Login = lazy(() => import('../routes/login').then((m) => ({ default: m.LoginRoute })));
|
||||
const Login = lazyWithReload(() => import('../routes/login').then((m) => ({ default: m.LoginRoute })));
|
||||
|
||||
function withSuspense(node: React.ReactNode) {
|
||||
return (
|
||||
@@ -26,14 +69,30 @@ function withSuspense(node: React.ReactNode) {
|
||||
);
|
||||
}
|
||||
|
||||
function RouteError() {
|
||||
return (
|
||||
<div className="mx-auto mt-16 max-w-md text-center">
|
||||
<h1 className="text-lg font-semibold">Etwas ist schiefgelaufen.</h1>
|
||||
<p className="mt-1 text-sm text-[var(--color-ink-muted)]">
|
||||
Wahrscheinlich wurde gerade eine neue Version veröffentlicht. Lade die Seite neu.
|
||||
</p>
|
||||
<Button className="mt-4" onClick={() => window.location.reload()}>
|
||||
Neu laden
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: '/login',
|
||||
element: withSuspense(<Login />),
|
||||
errorElement: <RouteError />,
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
element: <AppShell />,
|
||||
errorElement: <RouteError />,
|
||||
children: [
|
||||
{ index: true, element: withSuspense(<Dashboard />) },
|
||||
{ path: 'dashboard', element: withSuspense(<Dashboard />) },
|
||||
|
||||
Reference in new issue
Block a user