news 2026/9/14 2:43:25

如何用 EmailNode 的 renderToReactEmail 为 React Email Editor 创建自定义邮件节点

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
如何用 EmailNode 的 renderToReactEmail 为 React Email Editor 创建自定义邮件节点

如何用 EmailNode 的 renderToReactEmail 为 React Email Editor 创建自定义邮件节点

【免费下载链接】react-email💌 Build and send emails using React项目地址: https://gitcode.com/GitHub_Trending/re/react-email

这篇文章解决的问题是:在 react-email 的@react-email/editor(基于 TipTap 的邮件编辑器)中,内置的StarterKit只覆盖了一组邮件场景需要的节点;当你需要一种内置节点之外的结构——比如带高亮底色的 Callout 提示块——并且希望它在编辑器里能输入、能粘贴识别、还能在导出时序列化成邮件 HTML,就需要用EmailNode创建一个自定义扩展,并实现其中唯一的新增必选方法renderToReactEmail()

完成后的结果是:编辑器里可以插入自定义节点,调用composeReactEmail导出时,该节点会走你的renderToReactEmail渲染逻辑,出现在最终html输出中。

适用前提(来自编辑器文档的 Prerequisites 说明):

  • React 18+;
  • 使用支持 package exports 的打包器(Vite、Next.js、Webpack 5 等);
  • @react-email/editor包要求 Node >= 20(见 packages/editor/package.json 的engines字段)。

安装编辑器包

npm install @react-email/editor

文档同时给出 yarn / pnpm / bun 的等价命令(yarn add/pnpm add/bun add @react-email/editor)。本文后续都基于低层级路径(EditorProvider+ 扩展数组),因为自定义节点必须显式加入 extensions 数组;如果你使用独立的EmailEditor组件,文档建议改走低层级路径来获得对扩展列表的完全控制(见 getting-started.mdx 的 "Lower-level setup")。

EmailNode 与 renderToReactEmail 的工作机制

EmailNode扩展了 TipTap 的Node类,额外要求一个renderToReactEmail()方法,它决定节点在通过composeReactEmail导出为邮件 HTML 时如何被序列化(见 email-node.mdx)。

创建扩展的配置对象接受所有标准 TipTap Node 选项,加上renderToReactEmail。文档中三个方法的分工:

  • parseHTML():识别粘贴或导入的 HTML,让已有内容能被解析回该节点;
  • renderHTML():控制节点在编辑器内的外观;
  • renderToReactEmail():控制导出时该节点渲染成什么 React Email 结构。

renderToReactEmail收到的 props(来自 email-node.mdx 的 props 表):

PropType说明
childrenReact.ReactNode该节点序列化后的子内容
styleReact.CSSProperties该节点解析后的主题样式(无主题时为空对象)
nodeNodeProseMirror 节点实例
extensionEmailNode扩展实例,可用于访问options

导出的样式解析优先级(高者胜出,来自 compose-react-email.mdx):

  1. 节点属性上的内联样式;
  2. 当前主题通过getNodeStyles()提供的主题样式;
  3. 各扩展renderToReactEmail()中写死的默认样式。

因此在渲染器中通常是{...style}展开主题样式,再叠加固定样式。

一个关键限制:composeReactEmail遍历文档时,未注册、或不是EmailNode的节点类型会渲染为null。所以自定义节点必须用EmailNode.create(或EmailNode.from)创建并注册,否则导出时会直接消失。

创建自定义 Callout 节点

下面是文档给出的完整示例:一个渲染为高亮块的 Callout 节点(示例代码来自 custom-extensions.mdx):

import { EmailNode } from '@react-email/editor/core'; import { mergeAttributes } from '@tiptap/core'; const Callout = EmailNode.create({ name: 'callout', group: 'block', content: 'inline*', parseHTML() { return [{ tag: 'div[data-callout]' }]; }, renderHTML({ HTMLAttributes }) { return [ 'div', mergeAttributes(HTMLAttributes, { 'data-callout': '', style: 'padding: 12px 16px; background: #f4f4f5; border-left: 3px solid #1c1c1c; border-radius: 4px; margin: 8px 0;', }), 0, ]; }, renderToReactEmail({ children, style }) { return ( <div style={{ ...style, padding: '12px 16px', backgroundColor: '#f4f4f5', borderLeft: '3px solid #1c1c1c', borderRadius: '4px', margin: '8px 0', }} > {children} </div> ); }, });

三个方法要分别保持一致:parseHTMLdiv[data-callout]作为导入识别特征,renderHTML输出相同的data-callout属性和编辑器内样式,renderToReactEmail则把同样的视觉效果用 React 样式对象写出,供render()生成邮件 HTML。

注册扩展并插入自定义节点

把自定义扩展加入 extensions 数组,与StarterKit并列:

const extensions = [StarterKit, Callout];

程序化插入使用编辑器的insertContent命令。文档示例是一个工具栏按钮:

import { useCurrentEditor } from '@tiptap/react'; function Toolbar() { const { editor } = useCurrentEditor(); if (!editor) return null; return ( <button onClick={() => editor .chain() .focus() .insertContent({ type: 'callout', content: [{ type: 'text', text: 'New callout' }], }) .run() } > Insert Callout </button> ); }

完整编辑器组合

把节点、工具栏和 BubbleMenu 组合进EditorProvider(同样来自 custom-extensions.mdx):

import { EmailNode } from '@react-email/editor/core'; import { StarterKit } from '@react-email/editor/extensions'; import { BubbleMenu } from '@react-email/editor/ui'; import { mergeAttributes } from '@tiptap/core'; import { EditorProvider, useCurrentEditor } from '@tiptap/react'; import { Info } from 'lucide-react'; const Callout = EmailNode.create({ name: 'callout', group: 'block', content: 'inline*', parseHTML() { return [{ tag: 'div[data-callout]' }]; }, renderHTML({ HTMLAttributes }) { return [ 'div', mergeAttributes(HTMLAttributes, { 'data-callout': '', style: 'padding: 12px 16px; background: #f4f4f5; border-left: 3px solid #1c1c1c; border-radius: 4px; margin: 8px 0;', }), 0, ]; }, renderToReactEmail({ children, style }) { return ( <div style={{ ...style, padding: '12px 16px', backgroundColor: '#f4f4f5', borderLeft: '3px solid #1c1c1c', borderRadius: '4px', margin: '8px 0', }} > {children} </div> ); }, }); const extensions = [StarterKit, Callout]; const content = { type: 'doc', content: [ { type: 'paragraph', content: [ { type: 'text', text: 'This editor includes a custom Callout node. Use the toolbar to insert one.', }, ], }, { type: 'callout', content: [ { type: 'text', text: 'This is a callout block — a custom extension!' }, ], }, ], }; function Toolbar() { const { editor } = useCurrentEditor(); if (!editor) return null; return ( <button onClick={() => editor .chain() .focus() .insertContent({ type: 'callout', content: [{ type: 'text', text: 'New callout' }], }) .run() } > <Info size={16} /> Insert Callout </button> ); } export function MyEditor() { return ( <EditorProvider extensions={extensions} content={content} slotBefore={<Toolbar />} > <BubbleMenu /> </EditorProvider> ); }

注意content用的是 TipTap JSON 格式,编辑器同时支持 HTML 字符串与 TipTap JSON 两种初始内容格式(见 getting-started.mdx 的 "Content format")。使用低层级 UI 组件时记得导入主题 CSS,例如import '@react-email/editor/themes/default.css';,否则编辑器无样式。

验证导出结果

自定义节点是否真正参与邮件序列化,以导出输出为准。composeReactEmail的签名与返回值(来自 compose-react-email.mdx):

import { composeReactEmail } from '@react-email/editor/core'; async function composeReactEmail(params: { editor: Editor; preview: string | null; }): Promise<{ html: string; text: string }>;

它读取editor.getJSON(),遍历每个节点和 mark,调用各扩展的renderToReactEmail(),应用主题样式,包上邮件模板(BaseTemplate),最后用 react-email 的render()同时产出html与纯文本text

文档给出的带导出面板的验证方式(示例结果见 email-export.mdx):

import { composeReactEmail } from '@react-email/editor/core'; import { useCurrentEditor } from '@tiptap/react'; import { useState } from 'react'; function ExportPanel() { const { editor } = useCurrentEditor(); const [html, setHtml] = useState(''); const [exporting, setExporting] = useState(false); const handleExport = async () => { if (!editor) return; setExporting(true); const result = await composeReactEmail({ editor, preview: null }); setHtml(result.html); setExporting(false); }; return ( <div> <button onClick={handleExport} disabled={exporting}> {exporting ? 'Exporting...' : 'Export HTML'} </button> {html && ( <textarea readOnly value={html} rows={16} style={{ width: '100%', fontFamily: 'monospace' }} /> )} </div> ); }

ExportPanel作为EditorProvider的子组件(与Toolbar一样需要在 provider 内使用useCurrentEditor)。导出后的检查点:

  • html中应出现 Callout 节点renderToReactEmail写出的结构与内联样式(如padding: 12px 16pxborder-left: 3px solid #1c1c1c),并且内容被BaseTemplate包裹(默认模板包含 viewport meta 与Preview占位);
  • 若文档中存在未注册的节点类型,它们在html中不会有任何输出(渲染为null),这是排查"导出后内容缺失"的依据;
  • 如果 extensions 数组里配置了EmailTheming,主题样式会自动注入到每个节点;没有SerializerPlugin时,style默认为空对象、使用内置DefaultBaseTemplate

复用已有 TipTap 扩展(可选分支)

不想从零写节点时,EmailNode.from()可以把现有 TipTap 节点包一层邮件序列化能力,第二个参数就是renderToReactEmail渲染器(示例同样来自 custom-extensions.mdx):

import { EmailNode } from '@react-email/editor/core'; import { Node } from '@tiptap/core'; const MyTipTapNode = Node.create({ /* ... */ }); const MyEmailNode = EmailNode.from(MyTipTapNode, ({ children, style }) => { return <div style={style}>{children}</div>; });

EmailMark.from是 mark(行内样式,如高亮)的等价方法。EmailNodeEmailMark同时支持 TipTap 标准的.configure().extend(),而且.extend时也可以直接覆盖renderToReactEmail,例如文档示例中给Paragraph加键盘快捷键的同时重写其导出渲染:

const CustomParagraph = Paragraph.extend({ renderToReactEmail({ children, style }) { return <p style={{ ...style, lineHeight: '1.8' }}>{children}</p>; }, });

边界与注意事项

  • 自定义节点必须走EditorProvider+ 显式 extensions 数组这条路;renderToReactEmail只在composeReactEmail(及EmailEditorref 的getEmailHTML/getEmailText/getEmail,三者底层都是它)导出时被调用。
  • styleprop 在无主题时是空对象,主题样式、内联样式与扩展默认样式的合并优先级以第 3 节列表为准,渲染器里先展开...style再叠加固定样式是文档给出的合并方式。
  • depth只在列表节点(bulletListorderedList)内递增,用于区分嵌套段落(listParagraph)与顶层段落(paragraph)的主题键;如果你的自定义节点只在列表外使用,这一点不影响其渲染。
  • 文档中的 EmailNode 参考 与 composeReactEmail 参考 给出了create/from/configure/extend的完整 API 细节,可作为扩展开发时的查阅入口。

【免费下载链接】react-email💌 Build and send emails using React项目地址: https://gitcode.com/GitHub_Trending/re/react-email

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 2:39:39

如何用 marimo islands 把交互式笔记本内容嵌入静态网页?

如何用 marimo islands 把交互式笔记本内容嵌入静态网页&#xff1f; 【免费下载链接】marimo A reactive notebook for Python — run reproducible experiments, query with SQL, execute as a script, deploy as an app, and version with git. Stored as pure Python. All …

作者头像 李华
网站建设 2026/9/14 2:36:36

工业级可燃气体变送器原理与实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 2:34:33

把小爱音箱接入 ChatGPT:MiGPT 语音助手 10 分钟部署教程

把小爱音箱接入 ChatGPT&#xff1a;MiGPT 语音助手 10 分钟部署教程 【免费下载链接】mi-gpt &#x1f3e0; 将小爱音箱接入 ChatGPT 和豆包&#xff0c;改造成你的专属语音助手。 项目地址: https://gitcode.com/GitHub_Trending/mi/mi-gpt MiGPT 是一个把小米小爱音箱…

作者头像 李华
网站建设 2026/9/14 2:33:43

Obsidian多端同步难题破解:五大方案实测与选型指南

我在Obsidian上折腾同步已经有8年了&#xff0c;从最早的移动硬盘手动拷贝&#xff0c;到后来的各种插件、网盘、Git仓库&#xff0c;几乎把市面上能用的方案都试了一遍。写这篇东西的起因很简单&#xff1a;前几天帮我朋友从Notion迁到Obsidian&#xff0c;第一句话就问“多端…

作者头像 李华