import { StrictMode } from 'react'; import type { ComponentType } from 'react'; import { hydrateRoot, createRoot } from 'react-dom/client'; import type { PageContextClient } from 'vike/types'; import { applyTheme, getStoredTheme } from '../src/utils/theme'; import { installZoomGuard } from '../src/utils/zoomGuard'; import { beginHomeHydrate } from '../src/utils/homeHydrate'; import { reloadForStaleChunk } from '../src/utils/chunkLoad'; import { seedFeedData, type FeedData } from '../src/ssr/feedSeed'; import { ensureColdBootReady, isMainLayoutPath } from '../src/utils/prefetchRoute'; export { onRenderClient }; type PageComponent = ComponentType<{ url?: string }>; let root: ReturnType | null = null; function waitForStylesheets(): Promise { const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]')); if (links.length === 0) return Promise.resolve(); return Promise.all( links.map( (node) => new Promise((resolve) => { const link = node as HTMLLinkElement; try { if (link.sheet) { resolve(); return; } } catch { /* cross-origin */ } const finish = () => resolve(); link.addEventListener('load', finish, { once: true }); link.addEventListener('error', finish, { once: true }); }), ), ).then(() => undefined); } async function onRenderClient(pageContext: PageContextClient) { applyTheme(getStoredTheme()); installZoomGuard(); // hydrate 前把 +data 结果 seed 进客户端缓存(与服务端渲染前 seed 同构,保证首帧一致) if (pageContext.isHydration) { seedFeedData((pageContext as { data?: FeedData }).data); } // Vite modulepreload / 动态 import 失败(发版后旧 hashed URL 404) window.addEventListener('vite:preloadError', (event) => { event.preventDefault(); reloadForStaleChunk(); }); const el = document.getElementById('root'); if (!el) return; // 有 SSR 正文 → hydrate 接管同一 DOM(首帧数据已由 +data seed,无需冷启动预热); // SPA 壳 / SSR 降级空壳 → createRoot 挂载,挂载前先冷启动预热,避免空白首帧 const hydrating = pageContext.isHydration && el.innerHTML.trim() !== ''; // 仅非 hydrate 路径需要冷启动:SSR 页 seed 已灌入缓存,冷启动只会重复请求并阻塞 hydrate if (!hydrating && isMainLayoutPath(window.location.pathname)) { const path = `${window.location.pathname}${window.location.search}`; await ensureColdBootReady(path); } await waitForStylesheets(); try { if (document.fonts?.ready) await document.fonts.ready; } catch { /* ignore */ } if (hydrating) beginHomeHydrate(); const Page = pageContext.Page as PageComponent; const app = ( ); if (hydrating) { try { hydrateRoot(el, app); } catch (err) { console.warn('[j13] hydrateRoot 失败,回退 createRoot', err); el.innerHTML = ''; if (!root) root = createRoot(el); root.render(app); } } else { if (!root) root = createRoot(el); root.render(app); } }