- feedData.ts 聚合首页/板块/帖子详情数据,缺失内容 throw render(404) - Go 侧 spa_meta/seo 的 head 生成逻辑移植到 src/ssr/head.ts - _error 页复用站点壳渲染 404,+data 从 abortReason 恢复壳层数据 - linkedom 模拟服务端 DOM,DOMPurify 封装为 SSR 透传的 sanitizeHtml - 新增 /api/home-boot 聚合端点(handler/home_boot.go)供 SSR 冷启动灌缓存
94 lines
2.9 KiB
TypeScript
94 lines
2.9 KiB
TypeScript
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 { beginHomeHydrate } from '../src/utils/homeHydrate';
|
||
import { reloadForStaleChunk } from '../src/utils/chunkLoad';
|
||
import { seedFeedData, type FeedData } from '../src/ssr/feedData';
|
||
import { ensureColdBootReady, isMainLayoutPath } from '../src/utils/prefetchRoute';
|
||
|
||
export { onRenderClient };
|
||
|
||
type PageComponent = ComponentType<{ url?: string }>;
|
||
|
||
let root: ReturnType<typeof createRoot> | null = null;
|
||
|
||
function waitForStylesheets(): Promise<void> {
|
||
const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));
|
||
if (links.length === 0) return Promise.resolve();
|
||
return Promise.all(
|
||
links.map(
|
||
(node) =>
|
||
new Promise<void>((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());
|
||
// 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;
|
||
|
||
await waitForStylesheets();
|
||
try {
|
||
if (document.fonts?.ready) await document.fonts.ready;
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
|
||
const path = `${window.location.pathname}${window.location.search}`;
|
||
if (isMainLayoutPath(window.location.pathname)) {
|
||
await ensureColdBootReady(path);
|
||
}
|
||
|
||
// 有 SSR 正文 → hydrate 接管同一 DOM(首帧 hydrate 同构变体);SPA 壳 → 整树挂载
|
||
const hydrating = pageContext.isHydration && el.innerHTML.trim() !== '';
|
||
if (hydrating) beginHomeHydrate();
|
||
|
||
const Page = pageContext.Page as PageComponent;
|
||
const app = (
|
||
<StrictMode>
|
||
<Page />
|
||
</StrictMode>
|
||
);
|
||
|
||
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);
|
||
}
|
||
}
|