renderToStaticMarkup

将 React 树渲染为静态 HTML - 类似于 renderToString,但不添加额外的 React 内部属性

核心概述

renderToStaticMarkuprenderToString 类似, 但有一个关键区别:它不会创建额外的 DOM 属性 (如 data-reactroot), 生成的 HTML 更干净、更轻量。这使它非常适合用于纯静态页面, 不需要客户端 hydration 的场景。

核心特点:

  • 不添加 React 内部属性 (data-reactroot 等)
  • 生成的 HTML 更小、更干净
  • 不支持客户端 hydration (因为没有内部属性)
  • 适用于纯静态内容生成

适用场景:

  • 生成电子邮件 HTML (邮件客户端不支持 JavaScript)
  • 生成 RSS/Atom feeds
  • 静态网站生成器 (SSG) 的非交互部分
  • 生成可嵌入其他网站的 HTML 片段
  • 预览/打印版本的内容

📧 经典用例:电子邮件

电子邮件客户端不支持 JavaScript,因此不需要 hydration。 使用 renderToStaticMarkup 生成的 HTML 更小,兼容性更好。

JavaScript
// 生成电子邮件 HTML
const emailHTML = renderToStaticMarkup(<EmailTemplate {...data} />);

技术规格

导入方式

TypeScript
import { renderToStaticMarkup } from 'react-dom/server';

函数签名

TypeScript
function renderToStaticMarkup(element: ReactNode): string

参数说明

参数类型说明
elementReactNode要渲染的 React 元素

返回值

返回一个 HTML 字符串,不包含 React 内部属性。

实战演练

1. 生成电子邮件 HTML

最常见的使用场景 - 生成电子邮件内容:

TypeScript
import { renderToStaticMarkup } from 'react-dom/server';
import EmailTemplate from './EmailTemplate';

interface EmailData {
  to: string;
  subject: string;
  userName: string;
  verificationUrl: string;
}

async function sendVerificationEmail(data: EmailData) {
  // 渲染电子邮件模板
  const emailHTML = renderToStaticMarkup(
    <EmailTemplate
      userName={data.userName}
      verificationUrl={data.verificationUrl}
    />
  );

  // 发送电子邮件
  await emailService.send({
    to: data.to,
    subject: data.subject,
    html: emailHTML, // 纯 HTML,无 JavaScript
  });
}

// EmailTemplate.jsx
function EmailTemplate({ userName, verificationUrl }) {
  return (
    <div style={{ fontFamily: 'Arial, sans-serif' }}>
      <h1>欢迎, {userName}!</h1>
      <p>请点击下面的链接验证您的邮箱:</p>
      <a href={verificationUrl}>验证邮箱</a>
      <p>如果您没有注册,请忽略此邮件。</p>
    </div>
  );
}

2. 生成 RSS Feed

为博客生成 RSS/Atom feeds:

TypeScript
import { renderToStaticMarkup } from 'react-dom/server';

function RSSFeed({ posts }) {
  return (
    <rss version="2.0">
      <channel>
        <title>My Blog</title>
        <link>https://example.com</link>
        <description>My awesome blog</description>
        {posts.map(post => (
          <item key={post.id}>
            <title>{post.title}</title>
            <link>{`https://example.com/posts/${post.slug}`}</link>
            <description>{post.excerpt}</description>
            <pubDate>{post.publishedAt.toUTCString()}</pubDate>
          </item>
        ))}
      </channel>
    </rss>
  );
}

// API 路由
app.get('/rss', async (req, res) => {
  const posts = await fetchPosts();
  const rss = renderToStaticMarkup(<RSSFeed posts={posts} />);

  res.set('Content-Type', 'application/rss+xml');
  res.send(rss);
});

3. 生成可嵌入的 HTML 片段

生成可嵌入其他网站的 HTML widget:

TypeScript
import { renderToStaticMarkup } from 'react-dom/server';

function EmbedWidget({ productId }) {
  return (
    <div className="product-widget">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>{product.price}</p>
      <a href={product.url}>查看详情</a>
    </div>
  );
}

// 生成嵌入代码
app.get('/embed/:productId', async (req, res) => {
  const product = await fetchProduct(req.params.productId);
  const widgetHTML = renderToStaticMarkup(<EmbedWidget product={product} />);

  // 用户可以复制这段 HTML 嵌入到他们的网站
  const embedCode = `
    <script src="https://your-domain.com/widget.js"></script>
    <div id="widget-root">${widgetHTML}</div>
  `;

  res.json({ embedCode });
});

4. 静态网站生成

在构建时生成静态 HTML 页面:

TypeScript
import { renderToStaticMarkup } from 'react-dom/server';
import fs from 'fs';
import path from 'path';

async function buildStaticPages() {
  const posts = await fetchAllPosts();

  for (const post of posts) {
    // 生成静态 HTML
    const html = renderToStaticMarkup(
      <html>
        <head>
          <title>{post.title}</title>
          <meta name="description" content={post.excerpt} />
        </head>
        <body>
          <article>
            <h1>{post.title}</h1>
            <div dangerouslySetInnerHTML={{ __html: post.content }} />
          </article>
        </body>
      </html>
    );

    // 写入文件
    const filePath = path.join('dist', `${post.slug}.html`);
    fs.writeFileSync(filePath, `<!DOCTYPE html>${html}`);
  }
}

// 运行构建
buildStaticPages();

renderToString vs renderToStaticMarkup

选择合适的 API 取决于你的使用场景:

特性renderToStringrenderToStaticMarkup
React 内部属性✅ 添加❌ 不添加
支持 Hydration
HTML 大小较大 (有额外属性)较小 (干净 HTML)
使用场景需要 hydration 的 SSR (已被流式 API 取代)静态 HTML,电子邮件,RSS feeds

输出对比

HTML
// renderToString 输出
<div data-reactroot="">
  <h1>Hello World</h1>
</div>

// renderToStaticMarkup 输出 (更干净)
<div>
  <h1>Hello World</h1>
</div>

避坑指南

陷阱 1: 试图进行 Hydration

问题:renderToStaticMarkup 生成的 HTML 无法进行 hydration。

TypeScript
// ❌ 错误:试图对 renderToStaticMarkup 的输出进行 hydration
const html = renderToStaticMarkup(<App />);
document.getElementById('root').innerHTML = html;
hydrateRoot(document.getElementById('root'), <App />); // 无法匹配!

// ✅ 正确:如果需要 hydration,使用 renderToString 或流式 API
const html = renderToString(<App />);
document.getElementById('root').innerHTML = html;
hydrateRoot(document.getElementById('root'), <App />);

陷阱 2: 在交互式页面中使用

问题:用于需要 JavaScript 交互的页面。

TypeScript
// ❌ 错误:用于交互式应用
const html = renderToStaticMarkup(<InteractiveApp />);
// 用户点击按钮不会有任何反应

// ✅ 正确:使用 renderToString 或流式 API
const html = renderToString(<InteractiveApp />);
hydrateRoot(document, <InteractiveApp />);

延伸阅读