@react-email/render 渲染引擎演进全解:从 render 到纯文本转换的完整实践指南
【免费下载链接】react-email💌 Build and send emails using React项目地址: https://gitcode.com/GitHub_Trending/re/react-email
@react-email/render是 React Email 生态中将 React 组件转换为可用于真实发送的 HTML 邮件模板的核心包。本文以该包在仓库中的 CHANGELOG.md 为主线,结合 packages/render 下的源码、测试与配置,系统梳理render函数的异步化演进、运行时分支、纯文本转换能力、输出后处理(preload 剥离与格式化)以及多字节字符处理等关键技术点,帮助你在 Next.js、Node.js、Edge 与浏览器等不同环境中正确使用并理解其底层行为。
一、包概览:一个包,三种运行时
@react-email/render的核心职责只有一个:把 React 组件渲染成邮件 HTML 字符串。在 package.json 中可以看到它面向不同运行时暴露了不同的构建产物:
node:Node.js 服务端渲染(默认导出);edge:Edge Runtime(含workerd、edge-light、convex条件导出);browser:浏览器端与 Deno、Worker 环境。
此外还通过exports字段中的deno、worker、browser等条件精确控制模块解析。其中convex条件在 2.0.7 中单独修复了与node条件的导出顺序问题(见 CHANGELOG 2.0.7),并在 1.2.3 中改为在 Convex 运行时使用 edge 导出,这些细节说明该包对不同部署平台的兼容性做了专门打磨。
版本要求方面:engines.node要求>=20.0.0,peerDependencies声明react与react-dom为^18.0 || ^19.0 || ^19.0.0-rc,即支持 React 18 与 React 19(含 RC)。从 CHANGELOG 可以看到,对 React 19 的兼容从 1.0.0 起便通过放宽 peer 依赖逐步引入。
二、核心 API:render 的异步化演进
2.1 从同步到异步:1.0.0 的重大变更
CHANGELOG 中 1.0.0 版本标记为Major Changes,将render从同步 API 改为始终返回 Promise,并同时弃用了renderAsync。CHANGELOG 给出的理由有三点:
- 更好地支持 Next.js 最新版本;
- 为未来 React API 的弃用做准备;
- 支持 Suspense,从而允许在组件内部使用 async 能力。
对升级用户而言,迁移成本被刻意压低:旧render的调用需要await结果;而原来使用renderAsync的代码可以直接替换为render,属于 drop-in 替换。2.0.0 则正式移除了已弃用的renderAsync,并顺带清理了不再使用的react-promise-suspense依赖,API 表面收敛为单一的render。
2.2 现代 render 的实现路径
在 src/node/render.tsx 中,render的签名是:
export const render = async (node: React.ReactNode, options?: Options): Promise<string>其内部实现要点如下:
- 动态导入
react-dom/server:通过import('react-dom/server')并做m.default回退处理(对应 2.0.1 的add fallback for m.default修复),兼容不同打包器与模块格式; - 优先使用
renderToReadableStream:当宿主环境存在WritableStream时走可读流路径,先await stream.allReady再读取完整输出(对应 2.0.6 的await stream.allReady before reading renderToReadableStream output修复); - 回退到
renderToPipeableStream:在WritableStream不可用的环境(如部分 Node 版本或老式容器)回退到管道流(对应 1.3.2 的fallback to renderToPipeableSream修复); - 包裹 Suspense 与 ErrorBoundary:所有模板在渲染前被包进
<Suspense>与自建的 ErrorBoundary(见 src/shared/error-boundary.tsx),1.0.1 引入包裹 Suspense,1.0.5 之前一系列修复解决了错误被吞掉、错误被写进输出、甚至进入客户端渲染(CSR)导致邮件静默损坏的问题; progressiveChunkSize: Number.POSITIVE_INFINITY:让 React 一次输出完整文档,避免渐进式刷新产生的分块噪声;onError立即 reject:防止错误发生时 React 退化为 CSR 回退,确保错误被抛出而不是被吞进 HTML(对应 2.0.3、2.0.5 的修复)。
2.3 流读取与多字节字符处理
渲染输出的流式读取集中在 src/node/read-stream.ts。它同时处理两类流:
- 可读流(
pipeTo到WritableStream); - 管道流(
pipe到 NodeWritable)。
关键实现是使用单一TextDecoder实例并以{ stream: true }模式解码,这是针对多字节字符(如 CJK、emoji 等高密度字符)在流分块时被截断问题的核心修复,对应 CHANGELOG 中的多项记录:
- 1.0.2:
Fix null characters in between chunks when using high-density characters; - 1.3.1:
fixed multi-byte characters causing problems during stream reading; - 2.0.8:
Strip nul bytes from React 18 renderToPipeableStream output to prevent emails with multi-byte characters from being truncated。
renderToPipeableStream路径还会额外执行replaceAll('\0', ''),这是对 React 18 已知问题(facebook/react#26228)。
三、Options 配置全解
render的第二个参数options类型定义在 src/shared/options.ts 中,是理解全部可选行为的钥匙:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
pretty | boolean | false | 是否用 Prettier 格式化输出 HTML(1.1.0 起弃用render上的 pretty 选项,改为独立的pretty函数,此处保留为便捷开关) |
plainText | boolean | false | 是否返回纯文本而非 HTML |
htmlToTextOptions | HtmlToTextOptions | 见下 | 透传给html-to-text库的选项,仅当plainText: true且unstableTextConversion为false时有效 |
unstableTextConversion | boolean | false | 2.1.0 新增;为true时使用包内自研的纯文本格式化器,忽略htmlToTextOptions |
类型层面通过联合类型约束了合法组合:plainText: false时不允许再传htmlToTextOptions或unstableTextConversion;unstableTextConversion: true时不再接受htmlToTextOptions。
在 src/node/render.tsx 中,render的返回逻辑按以下顺序处理:
if (options?.plainText) { return options.unstableTextConversion ? unstableToPlainText(html) : toPlainText(html, options.htmlToTextOptions); } // 否则拼上 XHTML 1.0 Transitional doctype 后返回 const doctype = '<!DOCTYPE html PUBLIC ...XHTML 1.0 Transitional...>'; const document = `${doctype}${html.replace(/<!DOCTYPE.*?>/, '')}`; if (options?.pretty) return pretty(document); return document;注意:render输出的 HTML 会自动剥离 React 注入的 doctype,并替换为邮件客户端兼容性最好的XHTML 1.0 Transitionaldoctype;开启pretty时整个文档(含 doctype)会经 Prettier 格式化。
四、纯文本转换:toPlainText 与 unstableToPlainText
邮件通常需要同时提供 HTML 与纯文本两种版本,纯文本转换因此是render的重要能力。
4.1 toPlainText:基于 html-to-text 的默认实现
src/shared/utils/to-plain-text.ts 基于html-to-text库的convert函数,内置了一套默认选择器规则:
export const plainTextSelectors: SelectorDefinition[] = [ { selector: 'img', format: 'skip' }, // 图片默认跳过 { selector: '[data-skip-in-text=true]', format: 'skip' }, // 显式标记跳过 { selector: 'a', options: { linkBrackets: false, hideLinkHrefIfSameAsText: true } }, { selector: '[data-text-format="dataTable"]', format: 'dataTable' }, ];对应 CHANGELOG 与测试(to-plain-text.spec.ts)可以确认以下行为:
img与alt文本默认不进入纯文本;- 任何元素可通过
data-skip-in-text="true"属性在纯文本中隐藏; - 链接不带方括号包裹(
linkBrackets: false),且当链接文字与 href 相同时只保留一份(hideLinkHrefIfSameAsText); - 1.3.0 修复了纯文本模式下链接重复输出的问题;
- 1.4.0 起默认关闭自动换行(
wordwrap: false); - 2.1.0 新增
data-text-format="dataTable":渲染为对齐的数据表格列,其行为由html-to-text的dataTableformat 提供(测试用例should render tables as aligned rows with># npm npm install @react-email/render -E # 或 yarn yarn add @react-email/render -E要求 Node.js >= 20,且项目中已安装
react/react-dom(18 或 19)。7.2 基础渲染
import { MyTemplate } from "../components/MyTemplate"; import { render } from "@react-email/render"; // render 始终返回 Promise,必须 await const html = await render(<MyTemplate firstName="Jim" />);7.3 纯文本与格式化
const text = await render(<MyTemplate />, { plainText: true, // htmlToTextOptions: { wordwrap: 80 }, // 默认路径可用 // unstableTextConversion: true, // 2.1.0+ 自研转换器 }); const formatted = await render(<MyTemplate />, { pretty: true }); // 也可以单独使用导出的工具函数 import { toPlainText, pretty } from "@react-email/render"; const text = toPlainText(html); const htmlPretty = pretty(html);7.4 实践建议
- 迁移注意:若你仍在用
renderAsync,直接替换为render并await即可;当前版本已彻底移除该 API; - 多字节内容:包含中文、日文、emoji 等内容的邮件无需特殊处理,流读取与
\0剥离逻辑已内置处理; - 纯文本表格:希望纯文本中以对齐列呈现的表格,给
<table>加上data-text-format="dataTable"属性; - 隐藏内容:不希望出现在纯文本中的元素(如装饰性图片)添加
data-skip-in-text="true",图片本身默认即被跳过; - 选择器的取舍:
toPlainText的稳定 API 与html-to-text生态兼容性更好;unstableToPlainText减少了第三方依赖体积,但行为仍在演进,投入生产前请结合自身模板验证输出。
结语
从 1.0.0 的
render异步化,到 2.1.0 引入自研纯文本转换器,@react-email/render的 CHANGELOG 记录了一条清晰的技术演进路线:围绕异步流式渲染、跨运行时兼容、多字节安全与输出净化持续打磨。理解这些变更背后的动机与实现,能帮助你在使用render、toPlainText、pretty等 API 时做出更合理的技术选型,也能在遇到渲染异常时更快定位问题根因。更多组件与用法可参考仓库根目录的 README.md 及 packages/react-email 的相关文档。【免费下载链接】react-email💌 Build and send emails using React
项目地址: https://gitcode.com/GitHub_Trending/re/react-email
- 迁移注意:若你仍在用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考