- 帖子快照携带作者主页资料与统计(authorProfile/authorStats), SSR 端与客户端预热均取 /api/users/:id;PostAuthorCard 首帧直接渲染, 不再挂载后请求,统计数字不再显示 — 占位 - MainLayout 首帧从已 seed 的帖子快照派生右栏 outline(作者卡+文章目录), 修复 outline 仅靠 effect 上报导致 SSR 不渲染右栏的问题 - 壳层首屏请求门控:useAuth 有种子跳过 /api/me、branding 缓存命中不重拉、 侧栏三个区块首屏用 seed 缓存、未读数首屏跳过即时请求(轮询/事件刷新保留) - serverDom(linkedom) 修复:DOMParser 片段包裹、属性顺序、SVG 自闭合展开 - 验证:首页与帖子页 hydration 后仅剩 pageview 上报,0 hydration 错误
79 lines
3.3 KiB
TypeScript
79 lines
3.3 KiB
TypeScript
import type { ComponentType, ReactElement } from 'react';
|
||
import { renderToReadableStream } from 'react-dom/server';
|
||
import { escapeInject, dangerouslySkipEscape } from 'vike/server';
|
||
import type { PageContextServer } from 'vike/types';
|
||
import { createHomeStore } from '../src/store/homeStore';
|
||
import { createRequestScope } from '../src/ssr/scopeTypes';
|
||
import { runWithScope } from '../src/ssr/scope';
|
||
import { seedFeedData, type FeedData } from '../src/ssr/feedSeed';
|
||
import { renderHeadTags } from '../src/ssr/head';
|
||
import { installServerDom } from '../src/ssr/serverDom';
|
||
import { readThemeFromCookie } from '../src/utils/theme';
|
||
import { createSsrApp } from '../src/App';
|
||
|
||
// 服务端 DOM polyfill:内容处理依赖 DOMParser/document,须在渲染前安装
|
||
installServerDom();
|
||
|
||
export { onRenderHtml };
|
||
|
||
type PageComponent = ComponentType<{ url?: string }>;
|
||
|
||
/** 等 lazy 页面全部渲染完成(allReady)后缓冲为完整 HTML,首字节语义与旧 Go 文档级 SSR 一致 */
|
||
async function renderPageToString(element: ReactElement): Promise<string> {
|
||
const stream = await renderToReadableStream(element, {
|
||
onError(err) {
|
||
console.error('[j13] SSR 渲染错误:', err);
|
||
},
|
||
});
|
||
await stream.allReady;
|
||
return new Response(stream).text();
|
||
}
|
||
|
||
async function onRenderHtml(pageContext: PageContextServer) {
|
||
const Page = pageContext.Page as PageComponent | undefined;
|
||
const data = (pageContext as { data?: FeedData }).data;
|
||
|
||
let pageHtml = '';
|
||
if (Page) {
|
||
// SSR feed 页:per-request 作用域 + hydrate 同构变体;渲染失败降级空壳(客户端自行接管),不 500
|
||
const { pathname, searchOriginal } = pageContext.urlParsed;
|
||
const url = pathname + (searchOriginal ?? '');
|
||
const scope = createRequestScope(createHomeStore(), url);
|
||
scope.hydrating = true;
|
||
try {
|
||
pageHtml = await runWithScope(scope, async () => {
|
||
// 渲染前把 +data 取回的数据 seed 进请求作用域(缓存层按 scope 隔离)
|
||
seedFeedData(data);
|
||
// 官方 SSR 路径:await 首导航后 StaticRouterProvider 同步渲染(避免 RouterProvider suspend
|
||
// 导致内容落入 <div hidden>,禁 JS 爬虫不可见)
|
||
const app = await createSsrApp(url);
|
||
return renderPageToString(app);
|
||
});
|
||
} catch (err) {
|
||
console.error('[j13] SSR 渲染失败,降级 SPA 壳:', err);
|
||
pageHtml = '';
|
||
}
|
||
}
|
||
|
||
const headTags = renderHeadTags(data?.head);
|
||
|
||
// 主题从请求 Cookie 读取,首帧直接渲染到 <html> 上(深色用户无白屏闪烁,无需内联脚本)
|
||
// headersOriginal 经 Vike 归一化为 pageContext.headers(键全小写)
|
||
const cookieHeader = (pageContext as { headers?: Record<string, string> | null }).headers?.cookie;
|
||
const dark = readThemeFromCookie(cookieHeader) === 'dark';
|
||
const htmlAttrs = dark ? ' class="dark" style="color-scheme:dark"' : '';
|
||
|
||
// 404/301 等状态码由 Vike abort 机制(throw render/redirect)原生处理
|
||
return escapeInject`<!DOCTYPE html>
|
||
<html lang="zh-CN"${dangerouslySkipEscape(htmlAttrs)}>
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||
${dangerouslySkipEscape(headTags)}
|
||
</head>
|
||
<body>
|
||
<div id="root">${dangerouslySkipEscape(pageHtml)}</div>
|
||
</body>
|
||
</html>`;
|
||
}
|