Files
jiang13-forum/frontend/renderer/+onRenderHtml.tsx
freefire 3c6d8cb891 feat(ssr): 帖子页全量 SSR——作者卡/目录首帧服务端渲染,消除冗余首屏请求
- 帖子快照携带作者主页资料与统计(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 错误
2026-09-10 05:04:31 +08:00

79 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>`;
}