React 服务端渲染之 Server API 深度解析
目录
- [React DOM Server 完整 API 解析](#React DOM Server 完整 API 解析 "#react-dom-server-%E5%AE%8C%E6%95%B4-api-%E8%A7%A3%E6%9E%90")
- [所有 renderToXX 方法详解](#所有 renderToXX 方法详解 "#%E6%89%80%E6%9C%89-rendertoxx-%E6%96%B9%E6%B3%95%E8%AF%A6%E8%A7%A3")
- [React 18 新特性深度应用](#React 18 新特性深度应用 "#react-18-%E6%96%B0%E7%89%B9%E6%80%A7%E6%B7%B1%E5%BA%A6%E5%BA%94%E7%94%A8")
- [Next.js Server API 体系](#Next.js Server API 体系 "#nextjs-server-api-%E4%BD%93%E7%B3%BB")
- 高级服务端优化技术
- 性能监控与调试
- 最佳实践与架构模式
React DOM Server 完整 API 解析
所有 renderToXX 方法完整覆盖
React DOM Server 提供了多种渲染方法,每种都有其特定的使用场景和性能特点:
API 方法总览
方法 | 环境 | 输出类型 | Hydration支持 | 流式渲染 | React 18特性 |
---|---|---|---|---|---|
renderToString |
Node.js | String | ✅ | ❌ | 有限支持 |
renderToStaticMarkup |
Node.js | String | ❌ | ❌ | ❌ |
renderToNodeStream |
Node.js | Stream | ✅ | ✅ | 有限支持 |
renderToStaticNodeStream |
Node.js | Stream | ❌ | ✅ | ❌ |
renderToPipeableStream |
Node.js | Stream | ✅ | ✅ | ✅ |
renderToReadableStream |
Web | Stream | ✅ | ✅ | ✅ |
渲染流程架构图
graph TB
A[React Element] --> B{选择渲染方法}
B --> C[renderToString]
B --> D[renderToStaticMarkup]
B --> E[renderToNodeStream]
B --> F[renderToStaticNodeStream]
B --> G[renderToPipeableStream]
B --> H[renderToReadableStream]
C --> C1[同步渲染HTML字符串]
C --> C2[包含data-reactroot属性]
C --> C3[支持客户端水合]
D --> D1[纯静态HTML字符串]
D --> D2[不包含React属性]
D --> D3[无法水合]
E --> E1[Node.js可读流]
E --> E2[支持背压处理]
E --> E3[包含React属性]
F --> F1[静态内容流]
F --> F2[更小的输出]
F --> F3[无React属性]
G --> G1[支持Suspense]
G --> G2[选择性水合]
G --> G3[并发特性]
H --> H1[Web流标准]
H --> H2[边缘计算友好]
H --> H3[现代浏览器API]
style G fill:#e1f5fe
style H fill:#c8e6c9
所有 renderToXX 方法详解
1. renderToString
用途:将React元素渲染为HTML字符串,是最传统的SSR方法。
特点:
- 同步渲染,阻塞式处理
- 生成包含React属性的HTML
- 支持客户端hydration
- 不支持Suspense和并发特性
javascript
import { renderToString } from 'react-dom/server';
// 基础使用示例
function BasicApp({ title, content }) {
return (
<html>
<head>
<title>{title}</title>
</head>
<body>
<div id="root">
<h1>{title}</h1>
<p>{content}</p>
</div>
</body>
</html>
);
}
// 完整的renderToString实现
class RenderToStringService {
constructor() {
this.renderCache = new Map();
this.renderStats = {
totalRenders: 0,
averageTime: 0,
errors: 0
};
}
render(element, options = {}) {
const startTime = Date.now();
const cacheKey = options.cacheKey;
try {
// 检查缓存
if (cacheKey && this.renderCache.has(cacheKey)) {
const cached = this.renderCache.get(cacheKey);
if (Date.now() - cached.timestamp < (options.cacheTTL || 300000)) {
return {
html: cached.html,
cached: true,
renderTime: 0
};
}
}
// 执行渲染
const html = renderToString(element);
const renderTime = Date.now() - startTime;
// 更新统计
this.updateStats(renderTime, false);
// 缓存结果
if (cacheKey) {
this.renderCache.set(cacheKey, {
html,
timestamp: Date.now()
});
}
// 返回结果
return {
html,
cached: false,
renderTime,
size: Buffer.byteLength(html, 'utf8'),
hasReactAttributes: html.includes('data-react')
};
} catch (error) {
this.updateStats(Date.now() - startTime, true);
console.error('renderToString failed:', error);
// 错误降级
return {
html: this.getErrorFallback(error),
cached: false,
renderTime: Date.now() - startTime,
error: error.message
};
}
}
updateStats(renderTime, isError) {
this.renderStats.totalRenders++;
if (isError) {
this.renderStats.errors++;
} else {
const prevAvg = this.renderStats.averageTime;
const count = this.renderStats.totalRenders - this.renderStats.errors;
this.renderStats.averageTime = (prevAvg * (count - 1) + renderTime) / count;
}
}
getErrorFallback(error) {
return `
<!DOCTYPE html>
<html>
<head>
<title>渲染错误</title>
</head>
<body>
<div id="root">
<h1>页面渲染失败</h1>
<p>正在加载客户端版本...</p>
<script>
console.error('SSR Error:', ${JSON.stringify(error.message)});
</script>
</div>
</body>
</html>
`;
}
getStats() {
return {
...this.renderStats,
cacheSize: this.renderCache.size,
errorRate: this.renderStats.errors / this.renderStats.totalRenders
};
}
}
// 使用示例
const renderService = new RenderToStringService();
// Express.js 集成
app.get('/', (req, res) => {
const appElement = <BasicApp title="首页" content="欢迎访问我们的网站" />;
const result = renderService.render(appElement, {
cacheKey: `home:${req.headers['accept-language']}`,
cacheTTL: 600000 // 10分钟缓存
});
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.setHeader('X-Render-Time', result.renderTime);
res.setHeader('X-Cached', result.cached);
res.send(result.html);
});
2. renderToStaticMarkup
用途:渲染纯静态HTML,不包含React特有的DOM属性。
特点:
- 输出更干净的HTML
- 文件体积更小
- 不支持客户端hydration
- 适用于静态页面生成
javascript
import { renderToStaticMarkup } from 'react-dom/server';
// 静态页面组件
function StaticPage({ title, content, lastModified }) {
return (
<>
<h1>{title}</h1>
<article dangerouslySetInnerHTML={{ __html: content }} />
<footer>
<p>最后更新:{new Date(lastModified).toLocaleDateString('zh-CN')}</p>
</footer>
</>
);
}
// 静态站点生成器
class StaticSiteGenerator {
constructor(options = {}) {
this.options = {
outputDir: './dist',
templatePath: './templates/base.html',
minify: true,
...options
};
}
async generatePage(component, props, outputPath) {
try {
// 渲染组件为静态HTML
const componentHTML = renderToStaticMarkup(component(props));
// 读取HTML模板
const template = await this.loadTemplate();
// 构建完整页面
const fullHTML = this.buildFullPage(template, componentHTML, props);
// 压缩HTML(如果启用)
const finalHTML = this.options.minify ? this.minifyHTML(fullHTML) : fullHTML;
// 写入文件
await this.writeFile(outputPath, finalHTML);
return {
success: true,
outputPath,
size: Buffer.byteLength(finalHTML, 'utf8'),
compressed: this.options.minify
};
} catch (error) {
console.error(`Failed to generate ${outputPath}:`, error);
return {
success: false,
error: error.message,
outputPath
};
}
}
async loadTemplate() {
const fs = require('fs').promises;
return await fs.readFile(this.options.templatePath, 'utf8');
}
buildFullPage(template, content, props) {
return template
.replace('{{TITLE}}', props.title || '')
.replace('{{META_DESCRIPTION}}', props.description || '')
.replace('{{CONTENT}}', content)
.replace('{{CANONICAL_URL}}', props.canonicalUrl || '')
.replace('{{LANG}}', props.lang || 'zh-CN');
}
minifyHTML(html) {
// 简化的HTML压缩
return html
.replace(/\s+/g, ' ')
.replace(/>\s+</g, '><')
.replace(/^\s+|\s+$/g, '')
.trim();
}
async writeFile(path, content) {
const fs = require('fs').promises;
const dir = require('path').dirname(path);
// 确保目录存在
await fs.mkdir(dir, { recursive: true });
// 写入文件
await fs.writeFile(path, content, 'utf8');
}
// 批量生成静态页面
async generateSite(pages) {
const results = [];
for (const page of pages) {
const result = await this.generatePage(
page.component,
page.props,
`${this.options.outputDir}${page.path}/index.html`
);
results.push({
...result,
path: page.path
});
}
// 生成站点地图
await this.generateSitemap(pages.filter((_, i) => results[i].success));
return results;
}
async generateSitemap(pages) {
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${pages.map(page => `
<url>
<loc>${page.props.canonicalUrl || `https://example.com${page.path}`}</loc>
<lastmod>${new Date().toISOString()}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
`).join('')}
</urlset>`;
await this.writeFile(`${this.options.outputDir}/sitemap.xml`, sitemap);
}
}
// 使用示例
const generator = new StaticSiteGenerator({
outputDir: './public',
templatePath: './templates/page.html',
minify: true
});
// 生成静态网站
const pages = [
{
path: '/',
component: StaticPage,
props: {
title: '首页',
content: '<p>欢迎访问我们的网站</p>',
description: '这是网站首页',
canonicalUrl: 'https://example.com/',
lastModified: Date.now()
}
},
{
path: '/about',
component: StaticPage,
props: {
title: '关于我们',
content: '<p>我们是一家专业的技术公司</p>',
description: '了解我们公司的发展历程',
canonicalUrl: 'https://example.com/about',
lastModified: Date.now()
}
}
];
generator.generateSite(pages).then(results => {
console.log('静态站点生成完成:');
results.forEach(result => {
if (result.success) {
console.log(`✓ ${result.path} (${result.size} bytes)`);
} else {
console.log(`✗ ${result.path} - ${result.error}`);
}
});
});
3. renderToNodeStream
用途:将React元素渲染为Node.js可读流,支持流式传输。
特点:
- 支持背压处理
- 更好的内存使用
- 包含React hydration属性
- 逐步发送HTML内容
javascript
import { renderToNodeStream } from 'react-dom/server';
// 流式渲染服务
class StreamingRenderService {
constructor() {
this.activeStreams = new Set();
this.streamStats = {
totalStreams: 0,
activeCount: 0,
bytesStreamed: 0,
errors: 0
};
}
createStream(element, options = {}) {
const {
onError = this.defaultErrorHandler,
onComplete = this.defaultCompleteHandler,
timeout = 30000
} = options;
try {
// 创建渲染流
const renderStream = renderToNodeStream(element);
// 创建增强的流包装器
const enhancedStream = this.createEnhancedStream(renderStream, {
onError,
onComplete,
timeout
});
// 跟踪流状态
this.trackStream(enhancedStream);
return enhancedStream;
} catch (error) {
console.error('Failed to create render stream:', error);
onError(error);
return null;
}
}
createEnhancedStream(originalStream, options) {
const { Transform } = require('stream');
let bytesTransferred = 0;
let chunksCount = 0;
const startTime = Date.now();
const enhancedStream = new Transform({
transform(chunk, encoding, callback) {
bytesTransferred += chunk.length;
chunksCount++;
// 更新全局统计
this.streamStats.bytesStreamed += chunk.length;
// 可以在这里添加流处理逻辑
// 例如:添加性能标记、内容修改等
callback(null, chunk);
}.bind(this),
flush(callback) {
const duration = Date.now() - startTime;
options.onComplete({
bytesTransferred,
chunksCount,
duration,
averageChunkSize: bytesTransferred / chunksCount
});
callback();
}
});
// 设置超时
const timeoutId = setTimeout(() => {
enhancedStream.destroy(new Error('Stream timeout'));
}, options.timeout);
// 管道连接
originalStream.pipe(enhancedStream);
// 错误处理
originalStream.on('error', (error) => {
clearTimeout(timeoutId);
options.onError(error);
});
enhancedStream.on('end', () => {
clearTimeout(timeoutId);
});
return enhancedStream;
}
trackStream(stream) {
this.activeStreams.add(stream);
this.streamStats.totalStreams++;
this.streamStats.activeCount++;
stream.on('end', () => {
this.activeStreams.delete(stream);
this.streamStats.activeCount--;
});
stream.on('error', () => {
this.activeStreams.delete(stream);
this.streamStats.activeCount--;
this.streamStats.errors++;
});
}
defaultErrorHandler(error) {
console.error('Stream error:', error);
}
defaultCompleteHandler(stats) {
console.log('Stream completed:', stats);
}
getStats() {
return {
...this.streamStats,
averageBytesPerStream: this.streamStats.bytesStreamed / this.streamStats.totalStreams
};
}
// 优雅关闭所有活跃流
async closeAllStreams() {
const promises = Array.from(this.activeStreams).map(stream => {
return new Promise((resolve) => {
stream.on('end', resolve);
stream.on('error', resolve);
stream.destroy();
});
});
await Promise.all(promises);
console.log('All streams closed');
}
}
// 大型应用组件
function LargeApp({ user, posts, comments }) {
return (
<html>
<head>
<title>用户主页 - {user.name}</title>
<meta name="description" content={`${user.name}的个人主页`} />
</head>
<body>
<div id="root">
<header>
<h1>欢迎,{user.name}!</h1>
<nav>
<a href="/">首页</a>
<a href="/profile">个人资料</a>
<a href="/settings">设置</a>
</nav>
</header>
<main>
<section className="posts">
<h2>最新动态</h2>
{posts.map(post => (
<article key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
<div className="post-meta">
<span>发布时间:{new Date(post.createdAt).toLocaleDateString('zh-CN')}</span>
<span>阅读量:{post.views}</span>
</div>
</article>
))}
</section>
<section className="comments">
<h2>最新评论</h2>
{comments.map(comment => (
<div key={comment.id} className="comment">
<strong>{comment.author}</strong>
<p>{comment.content}</p>
<time>{new Date(comment.createdAt).toLocaleDateString('zh-CN')}</time>
</div>
))}
</section>
</main>
<footer>
<p>© 2024 我的网站. 保留所有权利.</p>
</footer>
</div>
</body>
</html>
);
}
// Express.js 集成示例
const streamingService = new StreamingRenderService();
app.get('/user/:id', async (req, res) => {
try {
// 获取用户数据
const [user, posts, comments] = await Promise.all([
getUserById(req.params.id),
getUserPosts(req.params.id, { limit: 10 }),
getUserComments(req.params.id, { limit: 5 })
]);
if (!user) {
return res.status(404).send('用户不存在');
}
// 创建应用元素
const appElement = <LargeApp user={user} posts={posts} comments={comments} />;
// 创建流式渲染
const stream = streamingService.createStream(appElement, {
timeout: 15000,
onError: (error) => {
console.error(`Streaming error for user ${req.params.id}:`, error);
if (!res.headersSent) {
res.status(500).send('渲染失败');
}
},
onComplete: (stats) => {
console.log(`Streaming completed for user ${req.params.id}:`, stats);
}
});
if (!stream) {
return res.status(500).send('无法创建渲染流');
}
// 设置响应头
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.setHeader('Transfer-Encoding', 'chunked');
// 管道连接到响应
stream.pipe(res);
// 处理客户端断开连接
req.on('close', () => {
stream.destroy();
});
} catch (error) {
console.error('Request handling error:', error);
res.status(500).send('服务器内部错误');
}
});
// 健康检查端点
app.get('/health/streaming', (req, res) => {
const stats = streamingService.getStats();
res.json({
status: 'healthy',
streaming: stats,
timestamp: new Date().toISOString()
});
});
// 优雅关闭
process.on('SIGTERM', async () => {
console.log('Received SIGTERM, closing streams...');
await streamingService.closeAllStreams();
process.exit(0);
});
4. renderToStaticNodeStream
用途:生成不包含React属性的静态HTML流。
特点:
- 纯静态内容流
- 更小的输出体积
- 不支持hydration
- 适用于静态内容生成
javascript
import { renderToStaticNodeStream } from 'react-dom/server';
// 静态内容流生成器
class StaticStreamGenerator {
constructor(options = {}) {
this.options = {
compressionLevel: 6,
enableGzip: true,
bufferSize: 64 * 1024, // 64KB
...options
};
this.generationStats = {
totalGenerated: 0,
totalBytes: 0,
averageSize: 0,
compressionRatio: 0
};
}
generateStaticStream(element, options = {}) {
const {
enableCompression = this.options.enableGzip,
bufferSize = this.options.bufferSize
} = options;
try {
// 创建静态渲染流
const staticStream = renderToStaticNodeStream(element);
// 创建处理流水线
const pipeline = this.createProcessingPipeline(staticStream, {
enableCompression,
bufferSize
});
return pipeline;
} catch (error) {
console.error('Failed to create static stream:', error);
throw error;
}
}
createProcessingPipeline(sourceStream, options) {
const { Transform, PassThrough } = require('stream');
const zlib = require('zlib');
let originalSize = 0;
let compressedSize = 0;
const startTime = Date.now();
// 创建统计流
const statsStream = new Transform({
transform(chunk, encoding, callback) {
originalSize += chunk.length;
callback(null, chunk);
}
});
// 创建缓冲流
const bufferStream = new Transform({
highWaterMark: options.bufferSize,
transform(chunk, encoding, callback) {
callback(null, chunk);
}
});
let pipeline = sourceStream.pipe(statsStream).pipe(bufferStream);
// 可选的压缩流
if (options.enableCompression) {
const gzipStream = zlib.createGzip({
level: this.options.compressionLevel
});
// 跟踪压缩后大小
const compressStatsStream = new Transform({
transform(chunk, encoding, callback) {
compressedSize += chunk.length;
callback(null, chunk);
}
});
pipeline = pipeline.pipe(gzipStream).pipe(compressStatsStream);
}
// 完成时更新统计
pipeline.on('end', () => {
this.updateStats({
originalSize,
compressedSize: options.enableCompression ? compressedSize : originalSize,
processingTime: Date.now() - startTime,
compressed: options.enableCompression
});
});
return pipeline;
}
updateStats(streamStats) {
this.generationStats.totalGenerated++;
this.generationStats.totalBytes += streamStats.originalSize;
this.generationStats.averageSize =
this.generationStats.totalBytes / this.generationStats.totalGenerated;
if (streamStats.compressed) {
const ratio = streamStats.compressedSize / streamStats.originalSize;
this.generationStats.compressionRatio =
(this.generationStats.compressionRatio + ratio) / 2;
}
}
// 生成静态RSS源
generateRSSFeed(posts, siteInfo) {
const RSSComponent = ({ posts, siteInfo }) => (
<rss version="2.0">
<channel>
<title>{siteInfo.title}</title>
<description>{siteInfo.description}</description>
<link>{siteInfo.url}</link>
<language>zh-CN</language>
<lastBuildDate>{new Date().toUTCString()}</lastBuildDate>
{posts.map(post => (
<item key={post.id}>
<title>{post.title}</title>
<description>{post.summary}</description>
<link>{`${siteInfo.url}/posts/${post.slug}`}</link>
<guid>{post.id}</guid>
<pubDate>{new Date(post.publishedAt).toUTCString()}</pubDate>
<author>{post.author.email} ({post.author.name})</author>
</item>
))}
</channel>
</rss>
);
return this.generateStaticStream(
<RSSComponent posts={posts} siteInfo={siteInfo} />,
{ enableCompression: false } // RSS通常不压缩
);
}
// 生成站点地图
generateSitemap(urls) {
const SitemapComponent = ({ urls }) => (
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{urls.map((url, index) => (
<url key={index}>
<loc>{url.loc}</loc>
<lastmod>{url.lastmod}</lastmod>
<changefreq>{url.changefreq || 'weekly'}</changefreq>
<priority>{url.priority || '0.5'}</priority>
</url>
))}
</urlset>
);
return this.generateStaticStream(
<SitemapComponent urls={urls} />,
{ enableCompression: true }
);
}
getStats() {
return this.generationStats;
}
}
// 博客网站静态内容生成
function BlogPost({ post, relatedPosts, comments }) {
return (
<article className="blog-post">
<header>
<h1>{post.title}</h1>
<div className="post-meta">
<time dateTime={post.publishedAt}>
{new Date(post.publishedAt).toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</time>
<span className="author">作者:{post.author.name}</span>
<span className="reading-time">阅读时长:{post.readingTime}分钟</span>
</div>
</header>
<div className="post-content" dangerouslySetInnerHTML={{ __html: post.content }} />
<section className="post-tags">
<h3>标签</h3>
{post.tags.map(tag => (
<span key={tag.id} className="tag">#{tag.name}</span>
))}
</section>
<section className="related-posts">
<h3>相关文章</h3>
<ul>
{relatedPosts.map(relatedPost => (
<li key={relatedPost.id}>
<a href={`/posts/${relatedPost.slug}`}>{relatedPost.title}</a>
<time>{new Date(relatedPost.publishedAt).toLocaleDateString('zh-CN')}</time>
</li>
))}
</ul>
</section>
<section className="comments">
<h3>评论 ({comments.length})</h3>
{comments.map(comment => (
<div key={comment.id} className="comment">
<div className="comment-header">
<strong>{comment.author}</strong>
<time>{new Date(comment.createdAt).toLocaleDateString('zh-CN')}</time>
</div>
<div className="comment-content">{comment.content}</div>
</div>
))}
</section>
</article>
);
}
// 使用示例
const staticGenerator = new StaticStreamGenerator({
enableGzip: true,
compressionLevel: 9,
bufferSize: 128 * 1024
});
// Express.js 路由集成
app.get('/posts/:slug', async (req, res) => {
try {
const [post, relatedPosts, comments] = await Promise.all([
getPostBySlug(req.params.slug),
getRelatedPosts(req.params.slug, 5),
getPostComments(req.params.slug)
]);
if (!post) {
return res.status(404).send('文章不存在');
}
const blogElement = (
<BlogPost
post={post}
relatedPosts={relatedPosts}
comments={comments}
/>
);
const stream = staticGenerator.generateStaticStream(blogElement, {
enableCompression: req.headers['accept-encoding']?.includes('gzip')
});
// 设置响应头
res.setHeader('Content-Type', 'text/html; charset=utf-8');
if (req.headers['accept-encoding']?.includes('gzip')) {
res.setHeader('Content-Encoding', 'gzip');
}
res.setHeader('Cache-Control', 'public, max-age=3600'); // 1小时缓存
// 流式响应
stream.pipe(res);
stream.on('error', (error) => {
console.error('Static stream error:', error);
if (!res.headersSent) {
res.status(500).send('渲染失败');
}
});
} catch (error) {
console.error('Blog post rendering error:', error);
res.status(500).send('服务器内部错误');
}
});
// RSS源端点
app.get('/feed.xml', async (req, res) => {
try {
const posts = await getRecentPosts(20);
const siteInfo = {
title: '我的博客',
description: '技术分享与思考',
url: 'https://myblog.com'
};
const rssStream = staticGenerator.generateRSSFeed(posts, siteInfo);
res.setHeader('Content-Type', 'application/rss+xml; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=1800'); // 30分钟缓存
rssStream.pipe(res);
} catch (error) {
console.error('RSS generation error:', error);
res.status(500).send('RSS生成失败');
}
});
// 站点地图端点
app.get('/sitemap.xml', async (req, res) => {
try {
const urls = await generateSitemapUrls();
const sitemapStream = staticGenerator.generateSitemap(urls);
res.setHeader('Content-Type', 'application/xml; charset=utf-8');
res.setHeader('Content-Encoding', 'gzip');
res.setHeader('Cache-Control', 'public, max-age=86400'); // 24小时缓存
sitemapStream.pipe(res);
} catch (error) {
console.error('Sitemap generation error:', error);
res.status(500).send('站点地图生成失败');
}
});
5. renderToPipeableStream (React 18)
用途:React 18的现代流式渲染API,支持Suspense和并发特性。
特点:
- 完整的React 18特性支持
- 选择性水合
- Suspense边界流式渲染
- 更好的错误处理
javascript
import { renderToPipeableStream } from 'react-dom/server';
import { Suspense } from 'react';
// React 18 现代流式SSR系统
class ModernStreamingSSR {
constructor(options = {}) {
this.options = {
bootstrapScripts: ['/js/client.js'],
onShellReady: null,
onShellError: null,
onAllReady: null,
onError: null,
timeout: 10000,
...options
};
this.renderMetrics = {
shellReadyTime: 0,
totalRenderTime: 0,
suspenseCount: 0,
errorCount: 0,
successCount: 0
};
}
renderToStream(element, response, renderOptions = {}) {
const startTime = Date.now();
let shellReady = false;
const metrics = { ...this.renderMetrics };
const {
onShellReady = this.options.onShellReady,
onShellError = this.options.onShellError,
onAllReady = this.options.onAllReady,
onError = this.options.onError,
bootstrapScripts = this.options.bootstrapScripts,
timeout = this.options.timeout
} = renderOptions;
const { pipe, abort } = renderToPipeableStream(element, {
bootstrapScripts,
// Shell准备就绪 - 包含非Suspense内容
onShellReady() {
shellReady = true;
metrics.shellReadyTime = Date.now() - startTime;
console.log(`Shell ready in ${metrics.shellReadyTime}ms`);
// 设置响应头
response.statusCode = 200;
response.setHeader('Content-Type', 'text/html; charset=utf-8');
response.setHeader('Transfer-Encoding', 'chunked');
response.setHeader('X-Shell-Time', metrics.shellReadyTime);
// 开始流式传输
pipe(response);
// 调用自定义回调
onShellReady?.();
},
// Shell渲染错误
onShellError(error) {
console.error('Shell render error:', error);
metrics.errorCount++;
response.statusCode = 500;
response.setHeader('Content-Type', 'text/html; charset=utf-8');
response.end(this.getErrorHTML(error, 'shell'));
onShellError?.(error);
},
// 所有内容准备就绪 - 包括Suspense内容
onAllReady() {
metrics.totalRenderTime = Date.now() - startTime;
metrics.successCount++;
console.log(`All content ready in ${metrics.totalRenderTime}ms`);
// 如果shell还未准备好,现在开始传输
if (!shellReady) {
response.statusCode = 200;
response.setHeader('Content-Type', 'text/html; charset=utf-8');
pipe(response);
}
// 更新响应头
response.setHeader('X-Total-Time', metrics.totalRenderTime);
onAllReady?.(metrics);
},
// 渲染过程中的错误
onError(error, errorInfo) {
console.error('Render error:', error, errorInfo);
metrics.errorCount++;
// 记录错误详情
this.logRenderError(error, errorInfo);
onError?.(error, errorInfo);
}
});
// 设置超时处理
const timeoutId = setTimeout(() => {
console.warn(`Render timeout after ${timeout}ms, aborting...`);
abort();
}, timeout);
// 清理资源
response.on('close', () => {
clearTimeout(timeoutId);
console.log('Client disconnected, cleaning up render stream');
});
response.on('finish', () => {
clearTimeout(timeoutId);
this.updateGlobalMetrics(metrics);
});
return { abort, metrics };
}
logRenderError(error, errorInfo) {
const errorLog = {
message: error.message,
stack: error.stack,
componentStack: errorInfo?.componentStack,
timestamp: new Date().toISOString(),
renderContext: 'streaming'
};
// 这里可以发送到错误监控服务
console.error('Detailed render error:', errorLog);
}
getErrorHTML(error, context) {
return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>页面加载失败</title>
<style>
body { font-family: system-ui, sans-serif; margin: 40px; }
.error { background: #fee; border: 1px solid #fcc; padding: 20px; border-radius: 4px; }
.error-title { color: #c33; margin: 0 0 10px 0; }
.error-message { color: #666; }
.retry-btn {
background: #007bff; color: white; border: none;
padding: 10px 20px; border-radius: 4px; cursor: pointer; margin-top: 15px;
}
</style>
</head>
<body>
<div class="error">
<h1 class="error-title">页面渲染失败</h1>
<p class="error-message">
渲染${context === 'shell' ? '基础内容' : '页面内容'}时发生错误,请稍后重试。
</p>
<button class="retry-btn" onclick="window.location.reload()">重新加载</button>
</div>
<script>
console.error('SSR Error Context:', '${context}');
console.error('Error Details:', ${JSON.stringify(error.message)});
// 5秒后自动重新加载
setTimeout(() => {
window.location.reload();
}, 5000);
</script>
</body>
</html>
`;
}
updateGlobalMetrics(metrics) {
Object.keys(this.renderMetrics).forEach(key => {
if (typeof this.renderMetrics[key] === 'number' && typeof metrics[key] === 'number') {
this.renderMetrics[key] = (this.renderMetrics[key] + metrics[key]) / 2;
}
});
}
getStats() {
return {
...this.renderMetrics,
successRate: this.renderMetrics.successCount /
(this.renderMetrics.successCount + this.renderMetrics.errorCount)
};
}
}
// 支持Suspense的现代应用组件
function ModernApp({ userId, initialData }) {
return (
<html lang="zh-CN">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>现代SSR应用</title>
<link rel="stylesheet" href="/css/app.css" />
</head>
<body>
<div id="root">
{/* 立即可见的内容 - Shell */}
<Header />
<Navigation />
<main>
{/* 快速加载的内容 */}
<section className="hero">
<h1>欢迎使用现代SSR应用</h1>
<p>基于React 18的流式服务端渲染</p>
</section>
{/* 需要数据获取的内容 - Suspense边界 */}
<Suspense fallback={<UserProfileSkeleton />}>
<UserProfile userId={userId} />
</Suspense>
<Suspense fallback={<DashboardSkeleton />}>
<UserDashboard userId={userId} />
</Suspense>
<Suspense fallback={<RecommendationsSkeleton />}>
<PersonalizedRecommendations userId={userId} />
</Suspense>
{/* 低优先级内容 */}
<Suspense fallback={<FooterSkeleton />}>
<DynamicFooter />
</Suspense>
</main>
</div>
{/* 性能监控脚本 */}
<script dangerouslySetInnerHTML={{
__html: `
window.__SSR_METRICS__ = {
renderStart: ${Date.now()},
shellReady: false,
allReady: false
};
// 标记shell就绪
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
window.__SSR_METRICS__.shellReady = true;
console.log('Shell hydration ready');
});
} else {
window.__SSR_METRICS__.shellReady = true;
}
`
}} />
</body>
</html>
);
}
// 异步数据组件示例
async function UserProfile({ userId }) {
// 模拟异步数据获取
const user = await new Promise(resolve => {
setTimeout(() => {
resolve({
id: userId,
name: '张三',
avatar: '/avatars/zhangsan.jpg',
bio: '全栈开发工程师,专注于React和Node.js开发',
joinDate: '2020-01-15',
followers: 1234,
following: 567
});
}, Math.random() * 1000 + 500); // 0.5-1.5秒随机延迟
});
return (
<section className="user-profile">
<div className="profile-header">
<img src={user.avatar} alt={user.name} className="avatar" />
<div className="profile-info">
<h2>{user.name}</h2>
<p>{user.bio}</p>
<div className="profile-stats">
<span>{user.followers} 关注者</span>
<span>{user.following} 正在关注</span>
<span>加入于 {new Date(user.joinDate).toLocaleDateString('zh-CN')}</span>
</div>
</div>
</div>
</section>
);
}
async function UserDashboard({ userId }) {
const dashboardData = await new Promise(resolve => {
setTimeout(() => {
resolve({
totalPosts: 42,
totalLikes: 1337,
totalComments: 89,
recentActivity: [
{ type: 'post', title: '发布了新文章:React 18 新特性解析', time: '2小时前' },
{ type: 'like', title: '点赞了文章:JavaScript 性能优化', time: '5小时前' },
{ type: 'comment', title: '评论了文章:现代前端架构设计', time: '1天前' }
]
});
}, Math.random() * 800 + 300);
});
return (
<section className="user-dashboard">
<h3>个人统计</h3>
<div className="stats-grid">
<div className="stat-card">
<h4>发布文章</h4>
<span className="stat-number">{dashboardData.totalPosts}</span>
</div>
<div className="stat-card">
<h4>获得点赞</h4>
<span className="stat-number">{dashboardData.totalLikes}</span>
</div>
<div className="stat-card">
<h4>收到评论</h4>
<span className="stat-number">{dashboardData.totalComments}</span>
</div>
</div>
<div className="recent-activity">
<h4>最近活动</h4>
<ul>
{dashboardData.recentActivity.map((activity, index) => (
<li key={index} className={`activity-${activity.type}`}>
<span className="activity-title">{activity.title}</span>
<span className="activity-time">{activity.time}</span>
</li>
))}
</ul>
</div>
</section>
);
}
// 骨架屏组件
function UserProfileSkeleton() {
return (
<section className="user-profile skeleton">
<div className="profile-header">
<div className="avatar skeleton-box"></div>
<div className="profile-info">
<div className="skeleton-text skeleton-title"></div>
<div className="skeleton-text"></div>
<div className="skeleton-text skeleton-half"></div>
</div>
</div>
</section>
);
}
function DashboardSkeleton() {
return (
<section className="user-dashboard skeleton">
<div className="skeleton-text skeleton-title"></div>
<div className="stats-grid">
{[1, 2, 3].map(i => (
<div key={i} className="stat-card skeleton-box"></div>
))}
</div>
</section>
);
}
// Express.js 集成
const modernSSR = new ModernStreamingSSR({
bootstrapScripts: ['/js/client.js', '/js/polyfills.js'],
timeout: 15000
});
app.get('/app/:userId?', (req, res) => {
const userId = req.params.userId || 'guest';
const appElement = <ModernApp userId={userId} />;
const { abort } = modernSSR.renderToStream(appElement, res, {
onShellReady: () => {
console.log(`Shell ready for user: ${userId}`);
},
onAllReady: (metrics) => {
console.log(`Complete render for user: ${userId}`, metrics);
},
onError: (error, errorInfo) => {
console.error(`Render error for user: ${userId}`, error);
// 可以在这里发送错误监控
}
});
// 请求取消处理
req.on('close', () => {
console.log(`Client disconnected for user: ${userId}`);
abort();
});
});
// 性能统计端点
app.get('/api/ssr-stats', (req, res) => {
const stats = modernSSR.getStats();
res.json({
status: 'ok',
ssr: stats,
timestamp: new Date().toISOString()
});
});
6. renderToReadableStream (Web Streams)
用途:为Web环境(如Cloudflare Workers、Deno)设计的流式渲染API。
特点:
- Web标准流API
- 边缘计算友好
- 现代浏览器原生支持
- 更好的流控制
javascript
// 注意:这个API主要用于Web环境,如Cloudflare Workers
// 这里提供Node.js环境的模拟实现示例
import { renderToReadableStream } from 'react-dom/server';
// Web流式SSR服务(适用于边缘计算环境)
class WebStreamingSSR {
constructor(options = {}) {
this.options = {
signal: null,
onError: null,
bootstrapScripts: [],
...options
};
this.streamMetrics = {
totalStreams: 0,
activeStreams: 0,
averageSize: 0,
errorRate: 0
};
}
async renderToWebStream(element, options = {}) {
const startTime = Date.now();
let bytesGenerated = 0;
const {
signal = this.options.signal,
onError = this.options.onError,
bootstrapScripts = this.options.bootstrapScripts
} = options;
try {
this.streamMetrics.totalStreams++;
this.streamMetrics.activeStreams++;
// 创建Web可读流
const stream = await renderToReadableStream(element, {
bootstrapScripts,
signal,
onError: (error, errorInfo) => {
console.error('Web stream render error:', error);
this.streamMetrics.errorRate =
(this.streamMetrics.errorRate + 1) / this.streamMetrics.totalStreams;
onError?.(error, errorInfo);
}
});
// 创建增强的流处理器
const enhancedStream = this.createEnhancedWebStream(stream, {
onData: (chunk) => {
bytesGenerated += chunk.length;
},
onComplete: () => {
const duration = Date.now() - startTime;
this.updateStreamMetrics(bytesGenerated, duration);
this.streamMetrics.activeStreams--;
}
});
return enhancedStream;
} catch (error) {
this.streamMetrics.activeStreams--;
console.error('Failed to create web stream:', error);
throw error;
}
}
createEnhancedWebStream(originalStream, callbacks) {
const { onData, onComplete } = callbacks;
// 创建转换流来监控数据
const transformStream = new TransformStream({
transform(chunk, controller) {
onData?.(chunk);
controller.enqueue(chunk);
},
flush() {
onComplete?.();
}
});
return originalStream.pipeThrough(transformStream);
}
updateStreamMetrics(bytes, duration) {
const currentAvg = this.streamMetrics.averageSize;
const count = this.streamMetrics.totalStreams;
this.streamMetrics.averageSize = (currentAvg * (count - 1) + bytes) / count;
}
getMetrics() {
return this.streamMetrics;
}
}
// Cloudflare Workers 示例
const webSSR = new WebStreamingSSR();
// Workers 环境的应用组件
function WorkersApp({ url, userAgent, cf }) {
return (
<html lang="zh-CN">
<head>
<meta charSet="utf-8" />
<title>边缘计算SSR应用</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div id="root">
<header>
<h1>边缘计算渲染</h1>
<p>此页面在离您最近的边缘节点渲染</p>
</header>
<main>
<section className="request-info">
<h2>请求信息</h2>
<dl>
<dt>访问URL:</dt>
<dd>{url}</dd>
<dt>用户代理:</dt>
<dd>{userAgent}</dd>
{cf && (
<>
<dt>数据中心:</dt>
<dd>{cf.colo}</dd>
<dt>国家/地区:</dt>
<dd>{cf.country}</dd>
<dt>时区:</dt>
<dd>{cf.timezone}</dd>
</>
)}
</dl>
</section>
<section className="edge-features">
<h2>边缘计算特性</h2>
<ul>
<li>✅ 超低延迟渲染</li>
<li>✅ 全球CDN分发</li>
<li>✅ 自动扩缩容</li>
<li>✅ 零冷启动</li>
</ul>
</section>
</main>
<footer>
<p>渲染时间: {new Date().toISOString()}</p>
<p>Powered by Cloudflare Workers + React SSR</p>
</footer>
</div>
</body>
</html>
);
}
// Cloudflare Workers 处理器
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
// 路由处理
if (url.pathname === '/') {
try {
const stream = await webSSR.renderToWebStream(
<WorkersApp
url={request.url}
userAgent={request.headers.get('User-Agent')}
cf={request.cf}
/>,
{
// 处理取消信号
signal: request.signal,
onError: (error) => {
console.error('Workers SSR error:', error);
}
}
);
return new Response(stream, {
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'public, max-age=300', // 5分钟缓存
'X-Rendered-By': 'Cloudflare-Workers-SSR'
}
});
} catch (error) {
console.error('Workers render failed:', error);
return new Response(
`<h1>渲染失败</h1><p>错误信息: ${error.message}</p>`,
{
status: 500,
headers: { 'Content-Type': 'text/html; charset=utf-8' }
}
);
}
}
// API路由示例
if (url.pathname === '/api/metrics') {
const metrics = webSSR.getMetrics();
return Response.json({
metrics,
worker: {
colo: request.cf?.colo,
country: request.cf?.country,
timestamp: new Date().toISOString()
}
});
}
return new Response('Not Found', { status: 404 });
}
};
// Deno 环境示例
if (typeof Deno !== 'undefined') {
const server = Deno.serve({ port: 8000 }, async (request) => {
const url = new URL(request.url);
if (url.pathname === '/') {
try {
const stream = await webSSR.renderToWebStream(
<WorkersApp
url={request.url}
userAgent={request.headers.get('User-Agent')}
/>
);
return new Response(stream, {
headers: {
'Content-Type': 'text/html; charset=utf-8',
'X-Rendered-By': 'Deno-SSR'
}
});
} catch (error) {
return new Response(`Error: ${error.message}`, { status: 500 });
}
}
return new Response('Not Found', { status: 404 });
});
console.log('Deno SSR server running on http://localhost:8000');
}
// Node.js环境的Web Streams适配器
class NodeWebStreamAdapter {
static async renderToNodeResponse(element, nodeResponse, options = {}) {
try {
// 在Node.js中模拟Web Streams
const webStream = await renderToReadableStream(element, options);
// 转换为Node.js流
const nodeStream = this.webStreamToNodeStream(webStream);
// 设置响应头
nodeResponse.setHeader('Content-Type', 'text/html; charset=utf-8');
nodeResponse.setHeader('X-Stream-Type', 'web-streams-adapter');
// 管道到响应
nodeStream.pipe(nodeResponse);
return nodeStream;
} catch (error) {
console.error('Web stream adapter error:', error);
nodeResponse.status(500).send('渲染失败');
throw error;
}
}
static webStreamToNodeStream(webStream) {
const { Readable } = require('stream');
const reader = webStream.getReader();
return new Readable({
async read() {
try {
const { done, value } = await reader.read();
if (done) {
this.push(null); // 结束流
} else {
this.push(value);
}
} catch (error) {
this.destroy(error);
}
}
});
}
}
// Node.js Express 使用示例
if (typeof require !== 'undefined') {
const express = require('express');
const app = express();
app.get('/web-streams', async (req, res) => {
const element = <WorkersApp url={req.url} userAgent={req.get('User-Agent')} />;
try {
await NodeWebStreamAdapter.renderToNodeResponse(element, res, {
onError: (error) => {
console.error('Node.js web streams error:', error);
}
});
} catch (error) {
if (!res.headersSent) {
res.status(500).send('渲染失败');
}
}
});
// 性能对比端点
app.get('/api/stream-comparison', async (req, res) => {
const metrics = webSSR.getMetrics();
res.json({
webStreams: metrics,
nodeJs: {
platform: process.platform,
version: process.version,
memory: process.memoryUsage()
},
comparison: {
advantages: [
'Web标准兼容',
'边缘计算友好',
'更好的流控制',
'现代浏览器原生支持'
],
limitations: [
'Node.js环境需要适配',
'生态系统相对较新',
'某些工具库可能不兼容'
]
}
});
});
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Node.js server with Web Streams running on port ${port}`);
});
}
React 18 新特性深度应用
Concurrent Features 在 SSR 中的应用
React 18 引入的并发特性为 SSR 带来了革命性的性能提升,特别是在处理大型应用和复杂数据获取场景中。
javascript
import { Suspense, startTransition, useDeferredValue, useTransition } from 'react';
import { renderToPipeableStream } from 'react-dom/server';
// 并发SSR应用架构
class ConcurrentSSRApp {
constructor() {
this.suspenseTracker = new Map();
this.renderPriorities = new Map();
}
// 优先级渲染控制
renderWithPriority(element, priority = 'normal') {
const priorityMap = {
urgent: 1,
normal: 2,
background: 3
};
return new Promise((resolve, reject) => {
const priorityValue = priorityMap[priority] || 2;
// 使用优先级队列管理渲染任务
this.scheduleRender(element, priorityValue, resolve, reject);
});
}
scheduleRender(element, priority, resolve, reject) {
// 模拟React的并发调度
const renderTask = () => {
try {
const stream = renderToPipeableStream(element, {
onShellReady() {
console.log(`Priority ${priority} render shell ready`);
},
onAllReady() {
console.log(`Priority ${priority} render complete`);
resolve(stream);
},
onError(error) {
console.error(`Priority ${priority} render error:`, error);
reject(error);
}
});
} catch (error) {
reject(error);
}
};
// 根据优先级调度任务
if (priority === 1) {
// 高优先级立即执行
renderTask();
} else if (priority === 2) {
// 正常优先级在下一个事件循环执行
setImmediate(renderTask);
} else {
// 低优先级延迟执行
setTimeout(renderTask, 10);
}
}
}
// 支持并发特性的组件示例
function ConcurrentApp({ userId, filters, preferences }) {
return (
<html>
<head>
<title>并发SSR应用</title>
</head>
<body>
<div id="root">
{/* 高优先级:立即显示的内容 */}
<Header />
<Navigation />
{/* 中优先级:主要内容区域 */}
<main>
<Suspense fallback={<MainContentSkeleton />}>
<MainContent userId={userId} />
</Suspense>
{/* 低优先级:次要内容 */}
<aside>
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar preferences={preferences} />
</Suspense>
</aside>
</main>
{/* 最低优先级:底部内容 */}
<Suspense fallback={<FooterSkeleton />}>
<DynamicFooter />
</Suspense>
</div>
</body>
</html>
);
}
// 智能数据预取组件
function SmartDataFetcher({ children, fallback, priority = 'normal' }) {
const [isPending, startTransition] = useTransition();
// 在服务端,直接渲染子组件
if (typeof window === 'undefined') {
return children;
}
// 在客户端,使用transition管理更新
return (
<Suspense fallback={fallback}>
{isPending ? fallback : children}
</Suspense>
);
}
// 并发数据获取Hook
function useConcurrentData(fetcher, deps = []) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [isPending, startTransition] = useTransition();
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
try {
setIsLoading(true);
setError(null);
const result = await fetcher();
if (!cancelled) {
// 使用transition来更新状态,避免阻塞UI
startTransition(() => {
setData(result);
setIsLoading(false);
});
}
} catch (err) {
if (!cancelled) {
setError(err);
setIsLoading(false);
}
}
};
fetchData();
return () => {
cancelled = true;
};
}, deps);
return { data, error, isLoading, isPending };
}
// 使用示例
const concurrentSSR = new ConcurrentSSRApp();
// Express路由with concurrent features
app.get('/concurrent/:userId', async (req, res) => {
const { userId } = req.params;
const { priority = 'normal' } = req.query;
try {
const appElement = (
<ConcurrentApp
userId={userId}
filters={req.query.filters}
preferences={req.query.preferences}
/>
);
// 根据请求优先级进行渲染
const stream = await concurrentSSR.renderWithPriority(appElement, priority);
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.setHeader('X-Render-Priority', priority);
stream.pipe(res);
} catch (error) {
console.error('Concurrent SSR error:', error);
res.status(500).send('并发渲染失败');
}
});
总结
React 服务端渲染的 Server API 为构建高性能 Web 应用提供了强大的工具集。通过本文的深入解析,我们全面覆盖了所有 renderToXX 方法:
核心 API 总结
- renderToString - 传统同步渲染,适用于简单应用
- renderToStaticMarkup - 纯静态内容生成,体积更小
- renderToNodeStream - Node.js流式渲染,支持背压处理
- renderToStaticNodeStream - 静态内容流,无React属性
- renderToPipeableStream - React 18现代流式API,完整并发支持
- renderToReadableStream - Web标准流,边缘计算友好
技术选择指南
- 传统应用 : 使用
renderToString
进行快速迁移 - 静态站点 : 使用
renderToStaticMarkup
获得最小输出 - 大型应用 : 使用
renderToPipeableStream
获得最佳性能 - 边缘计算 : 使用
renderToReadableStream
实现全球分发
最佳实践
- 渐进式升级: 从传统API逐步迁移到React 18新特性
- 合理选择: 根据应用场景选择最适合的渲染方法
- 性能监控: 建立完整的渲染性能监控体系
- 错误处理: 实现完善的错误处理和降级机制
通过掌握这些 Server API,开发者能够构建出高性能、可扩展的服务端渲染应用,为用户提供优秀的访问体验。