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/feedData'; import { renderHeadTags } from '../src/ssr/head'; import { installServerDom } from '../src/ssr/serverDom'; // 服务端 DOM polyfill:内容处理依赖 DOMParser/document,须在渲染前安装 installServerDom(); export { onRenderHtml }; type PageComponent = ComponentType<{ url?: string }>; /** 首帧主题脚本(localStorage j13-theme),与旧 index.html 保持一致 */ const THEME_INIT_SCRIPT = "(function(){var t=localStorage.getItem('j13-theme')||'light';document.documentElement.classList.toggle('dark',t==='dark');document.documentElement.style.colorScheme=t;})();"; /** 锁死手机/平板双指与手势缩放;头像裁剪 / 图片灯箱放行 pinch */ const ZOOM_BLOCK_SCRIPT = "(function(){function inZoomAllowArea(target){return !!(target&&target.closest&&target.closest('.avatar-crop-stage, .image-lightbox'));}function blockGesture(e){if(inZoomAllowArea(e.target))return;if(e.cancelable)e.preventDefault();}function blockMultiTouch(e){if(e.touches&&e.touches.length>1){if(inZoomAllowArea(e.target))return;if(e.cancelable)e.preventDefault();}}document.addEventListener('gesturestart',blockGesture,{passive:false});document.addEventListener('gesturechange',blockGesture,{passive:false});document.addEventListener('gestureend',blockGesture,{passive:false});document.addEventListener('touchstart',blockMultiTouch,{passive:false});document.addEventListener('touchmove',blockMultiTouch,{passive:false});})();"; /** 等 lazy 页面全部渲染完成(allReady)后缓冲为完整 HTML,首字节语义与旧 Go 文档级 SSR 一致 */ async function renderPageToString(element: ReactElement): Promise { 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 scope = createRequestScope(createHomeStore()); scope.hydrating = true; const { pathname, searchOriginal } = pageContext.urlParsed; const url = pathname + (searchOriginal ?? ''); try { pageHtml = await runWithScope(scope, () => { // 渲染前把 +data 取回的数据 seed 进请求作用域(缓存层按 scope 隔离) seedFeedData(data); return renderPageToString(); }); } catch (err) { console.error('[j13] SSR 渲染失败,降级 SPA 壳:', err); pageHtml = ''; } } const headTags = renderHeadTags(data?.head); // 404/301 等状态码由 Vike abort 机制(throw render/redirect)原生处理 return escapeInject` ${dangerouslySkipEscape(headTags)}
${dangerouslySkipEscape(pageHtml)}
`; }