首页
/ 使用 @novu/maily-core 构建邮件编辑器:Slash 命令、变量注入与扩展机制详解

使用 @novu/maily-core 构建邮件编辑器:Slash 命令、变量注入与扩展机制详解

2026-09-09 20:15:12作者:钟日瑜

@novu/maily-core 是 Novu 开源通信基础设施中用于创建邮件内容的 WYSIWYG 编辑器核心包,它基于 Tiptap(ProseMirror)构建,为邮件模板提供"预设计、移动端友好"的编辑体验。本文以 libs/maily-core/readme.md 为骨架,结合 libs/maily-core/src 下的源码实现,系统讲解其安装接入、Slash 命令定制、变量注入、扩展机制与图片上传等核心能力,并给出与之配套的 @novu/maily-render 渲染方案,帮助你在自己的 React 应用中快速集成一套可定制、可扩展、可输出 HTML 邮件的可视化编辑能力。

安装与前置条件

该包位于仓库的 libs/maily-core 目录,包名为 @novu/maily-core,当前版本 0.2.7-novu.19-core(见 package.json)。使用 pnpm 安装:

pnpm add @novu/maily-core

# 类型依赖
pnpm add -D @tiptap/core

几点环境说明(均以仓库实际配置为准):

  • Node 版本engines.node 要求 >=18.0.0
  • React 版本:peerDependencies 为 react / react-dom ^18.0.0 || ^19.0.0,其中 react-dom 标记为可选。
  • 核心依赖:底层基于 @tiptap/core@tiptap/react@tiptap/suggestion@tiptap/starter-kit 等 2.11.5 系列扩展,同时依赖 tippy.js(变量建议浮层)、sanitize-html(HTML 消毒)、tailwindcss(内置样式)等。
  • 样式导入:包通过子路径 @novu/maily-core/style.css 暴露编译后的样式(对应源码中的 styles/index.cssstyles/preflight.cssstyles/tailwind.css),使用前必须引入。
  • 子路径导出package.jsonexports 提供了三个入口:根入口 .(Editor 组件)、./blocks(预置块)、./extensions(扩展),以及 ./style.css

快速上手:接入 Editor 组件

最基础的用法是导入样式与 Editor 组件,传入 JSON 格式的初始内容,并通过 onCreate / onUpdate 拿到编辑器实例:

import '@novu/maily-core/style.css';

import { useState } from 'react';
import { Editor } from '@novu/maily-core';
import type { Editor as TiptapEditor, JSONContent } from '@tiptap/core';

type AppProps = {
  contentJson: JSONContent;
};

function App(props: AppProps) {
  const { contentJson: defaultContentJson } = props;
  const [editor, setEditor] = useState<TiptapEditor>();

  return (
    <Editor
      contentJson={defaultContentJson}
      onCreate={setEditor}
      onUpdate={setEditor}
    />
  );
}

Editor 组件实现 可以看到更多内置行为与可配置项:

  • 初始内容优先级contentJson 优先于 contentHtml;若两者均未提供,则默认创建一个空的 doc > paragraph 文档。传入的 contentJson 若顶层不是 doc,会被自动包装为 { type: 'doc', content }
  • config 选项:支持 hasMenuBar(默认 true)、spellCheck(默认 false)、autofocus(默认 'end')、immediatelyRender(默认 false),以及 wrapClassName / toolbarClassName / contentClassName / bodyClassName 等样式类名覆盖。
  • editable:默认 true;组件内部通过 useEffect 同步 editor.setEditable(editable, false),因此运行时切换只读/编辑状态是安全的。
  • 默认扩展:编辑器会默认装配 MailyKitSlashCommandExtensionVariableExtensionHTMLCodeBlockExtensionInlineImageExtensionPlaceholderExtension(见 extensions/index.tsx),并对同名扩展做去重,避免重复注册。
  • 占位符placeholder.ts 定义了分级占位提示——标题显示 Heading {level},HTML 代码块显示 Type your HTML code...,普通段落显示 Write something or / to see commands

Slash Commands:按分组组织命令面板

Slash 命令让你在编辑器中输入 / 加命令名即可与编辑器交互。命令按组(group)组织,每组是一个包含 titlecommands 数组的对象,数组内的每一项是一个 BlockItem(见 blocks/types.ts)。

基本分组示例

将若干基础块(如文本、标题)归为一组:

// omitting imports
import { text, heading1 } from '@novu/maily-core/blocks';

<Editor
  blocks={[
    {
      title: 'Basic Blocks',
      commands: [text, heading1],
    },
  ]}
/>

注意:分组的顺序以及组内命令的顺序,决定它们在编辑器命令面板中的展示顺序。例如 default-slash-commands.tsx 中默认先注册 Blocks 组(text、heading1~3、列表、图片、logo、columns、section、repeat、divider、spacer、button、linkCard、hardBreak、blockquote、footer、clearLine),再注册 Components 组(预置 headers、footers、htmlCodeBlock)。

带子命令的分组命令块

有时你希望一个命令展开为二级命令列表。此时定义一个带 idcommands 数组的命令即可——id 用于 slash 命令的查询过滤,例如输入 /headers. 会展示其子命令:

// omitting imports
<Editor
  blocks={[
    {
      title: 'Formatting',
      commands: [
        {
          title: 'Headers',
          // id 用于过滤命令,例如输入 `/headers.` 展示这些子命令
          id: 'headers',
          searchTerms: ['header', 'title'],
          commands: [
            {
              title: 'Heading 1',
              searchTerms: ['h1', 'heading1'],
              command: ({ editor, range }) => {
                // 将当前块转换为 Heading 1
              },
            },
            {
              title: 'Heading 2',
              searchTerms: ['h2', 'heading2'],
              command: ({ editor, range }) => {
                // 将当前块转换为 Heading 2
              },
            },
            // 继续添加更多子命令
          ],
        },
      ],
    },
  ]}
/>

注意:目前子命令只支持一层嵌套深度。

BlockItem 类型定义(blocks/types.ts)看,一个命令项除了 titlesearchTerms,还可选提供 descriptioniconpreview(预览图 URL 或函数)与 render 函数。默认命令的实际实现可参考 typography.tsx(如 heading1 执行 setNode('heading', { level: 1 }))与 layout.tsx(如 repeat 执行 setRepeat()spacer 执行 setSpacer({ height: 'sm' }))。

自定义渲染块

若要完全自定义命令的渲染结果,可给块对象传入 render 函数,它接收编辑器实例作为参数;基于编辑器状态你甚至可以返回 null 来表示不渲染任何内容:

// omitting imports
<Editor
  blocks={[
    {
      title: 'Custom Blocks',
      commands: [
        {
          title: 'Custom Block',
          searchTerms: ['custom'],
          render: (editor) => {
            return <div>Custom Block</div>;
          },
        },
      ],
    },
  ]}
/>

Slash 命令本身是一个 Tiptap 扩展:SlashCommandExtension 默认配置触发字符 char: '/',并通过 @tiptap/suggestion 的 ProseMirror 插件监听输入、弹出建议面板。

Variables:变量注入与动态建议

变量默认是必填的;将其 required 设为 false 即可变为可选。可选变量在未提供值时,编辑器会在其位置展示占位内容。

向编辑器传入变量有两种方式。

方式一:对象数组(静态建议)

以对象数组形式传入 variables 时,编辑器中输入 @ 即可触发变量的自动建议:

// (Omitted repeated imports)
import { VariableExtension, getVariableSuggestions } from '@novu/maily-core/extensions';

<Editor
  extensions={[
    VariableExtension.configure({
      suggestions: getVariableSuggestions('@'),
      variables: [{
         name: 'currentTime',
         required: false,
      }],
    }),
  ]}
/>

方式二:函数(动态建议)

当变量需要根据编辑器状态或其他输入动态生成时,将 variables 传为函数。函数签名(见 variable.tsVariableFunctionOptions)提供 query(触发字符后的文本)、from(变量请求来源,取值为 content-variable | bubble-variable | repeat-variable)与 editor

// (Omitted repeated imports)
import { VariableExtension, getVariableSuggestions } from '@novu/maily-core/extensions';

<Editor
  extensions={[
    VariableExtension.configure({
      suggestions: getVariableSuggestions('@'),
      variables: ({ query, from, editor }) => {
        // query: 触发字符之后的文本
        // from: 变量请求的上下文(repeat / variable 等)
        // editor: 编辑器实例
        if (from === 'repeat-variable') {
          // 返回 Repeat 块 `each` 键可用的变量
          return [
            { name: 'notifications' },
            { name: 'comments' },
          ];
        }

        return [
          { name: 'currentDate' },
          { name: 'currentTime', required: false },
        ];
      },
    }),
  ]}
/>

关键差异:传入对象数组时,Maily 会根据 query 自动完成过滤;而传入函数时,过滤逻辑需要你自己实现

从源码看,变量扩展是一个 atom 内联节点(variable.ts),其属性包括 idlabelfallbackrequired(默认 true),通过 data-id / data-label / data-fallback / data-required 属性与 HTML 双向序列化。建议浮层由 variable-suggestions.tsx 实现,使用 tippy.js 弹出、支持键盘上下键与 Enter 选择、Esc 关闭;getVariableSuggestions(char = '@') 返回的 items 会调用 processVariables 结合查询文本生成候选。此外变量节点还注册了 Backspace 快捷键:当光标紧贴变量时按 Backspace,会还原为触发字符 @ 以便重新输入。

Extensions:扩展编辑器能力

扩展是增强编辑器功能的主要手段,你可以通过扩展添加自定义块(block)、标记(mark),或直接扩展已有能力。

使用 MailyKit 一键装配邮件块

MailyKit 是邮件编辑的核心扩展集合。通过 MailyKit.configure(...) 可按需禁用某类节点,例如关闭 link card 节点:

// (Omitted repeated imports)
import { MailyKit, VariableExtension, getVariableSuggestions } from '@novu/maily-core/extensions';

<Editor
  extensions={[
    MailyKit.configure({
      // 禁用 link card 节点
      linkCard: false,
    }),
    // 扩展 Variable 扩展并自定义变量视图
    VariableExtension.extend({
      addNodeView() {
        // 用自定义 VariableView 替换默认视图
        return ReactNodeViewRenderer(VariableView, {
          className: 'mly-relative mly-inline-block',
          as: 'div',
        });
      },
    }).configure({
      suggestions: getVariableSuggestions(variableTriggerCharacter),
    }),
  ]}
/>

MailyKitOptions 支持的可关闭项包括:linkCardrepeatsectioncolumnscolumnbuttonspacerlogoimagelink,每一项都可传 false 或对应配置对象。MailyKit 内部还做了大量邮件友好的默认配置,例如:

  • Document 内容模型被约束为 (block|columns)+
  • StarterKit 被精细裁剪(禁用自带 heading、paragraph、horizontalRule、dropcursor、document,改用自研版本),并给 code、blockquote、列表注入 mly-* 样式类;
  • 链接默认 target='_blank'rel='noopener noreferrer nofollow'openOnClick: false
  • 标题仅支持 h1~h3;TextAlign 作用于 paragraph、heading、footer。

注册自定义扩展

你也可以直接传入自己的 Tiptap 扩展,写法与普通 Tiptap 扩展完全一致:

// (Omitted repeated imports)
import { CustomExtension } from './extensions/custom-extension';

<Editor
  extensions={[
    CustomExtension.configure({
      // your configuration
    }),
  ]}
/>

注意 extensions/index.tsx 中的同名去重逻辑:若你传入的扩展与默认扩展同名,默认版本会被你的版本替换,例如 Dashboard 中正是通过重新注册 image 扩展来覆盖 MailyKit 的图片节点配置。

Image Upload:图片上传

要启用图片上传,需要向编辑器传入 ImageUploadExtension 扩展。onImageUpload 会在图片上传时被调用,你可以借此把图片上传到自己的服务器并返回最终 URL:

// (Omitted repeated imports)
import { ImageUploadExtension } from '@novu/maily-core/extensions';

<Editor
  extensions={[
    ImageUploadExtension.configure({
      onImageUpload: async (file) => {
        // 上传图片到任意位置
        const url = await uploadImage(file);
        return url;
      },
    }),
  ]}
/>

其实现(image-upload.ts)说明了两点细节:

  • 默认允许的 MIME 类型image/jpegimage/pngimage/gifimage/webpimage/svg+xml,可通过 allowedMimeTypes 覆盖;
  • 未配置 onImageUpload 时不注册任何 ProseMirror 插件addProseMirrorPlugins 直接返回空数组),因此上传行为完全由你提供。

将编辑内容渲染为 HTML 邮件

编辑器中产生的是 Tiptap JSONContent,真正投递邮件前需要将其渲染为 HTML。这与 @novu/maily-render 包配合使用,渲染包在仓库中的入口见 maily-render/src/index.ts

核心用法(render.ts):

import { render } from '@novu/maily-render';

const html = await render(contentJson, {
  theme: {
    colors: { heading: '#111827' },
  },
  preview: '邮件预览文本',
  pretty: true,
});

Maily 类实现 可以了解其渲染管线:

  • 内容被映射为 React Email 组件(Html / Body / Container / Text / Heading / Button / Section / Row / Column 等),再通过 @react-email/renderrenderAsync 输出 HTML;
  • 变量替换:默认情况下变量按 {{name,fallback=...}} 格式输出(Liquid 风格);调用 maily.setVariableValue(name, value)setVariableValues({...}) 后进入"替换模式",将变量替换为真实值。仓库测试 render.test.ts 验证了:设置变量值后 {{name}} 被替换为实际字符串;未设置时输出默认格式化文本;还支持通过 setVariableFormatter 自定义变量输出格式;
  • 主题定制theme.colors 支持 heading、paragraph、horizontal、footer、blockquoteBorder、codeBackground、codeText 以及 linkCard 系列颜色;theme.fontSize 支持 paragraph 与 footer 的 fontSize / fontStyle / fontWeight / lineHeight
  • 渲染选项pretty(美化缩进)、plainText(纯文本输出)、noHtmlWrappingTags(不输出 Html 包裹标签),默认均为 false
  • 此外还提供 setLinkValue / setLinkValues 批量替换链接、setPayloadValue 注入 payload、setOpenTrackingPixel 插入 1x1 打开跟踪像素、setMetaTags / setHtmlProps 定制 meta 与 html 属性等能力。

在 Novu Dashboard 中的真实集成

除了独立使用,@novu/maily-core 也是 Novu Dashboard 邮件工作流编辑器的底层实现。在 apps/dashboard/src/components/maily/maily.tsx 中可以看到生产级集成方式:

  • 通过 createVariableNodeView 传入自定义变量节点视图,把变量渲染成校验过的 pill 样式;
  • 通过 extensions 传入 RepeatExtension.extend(...) 等扩展,自动为 Repeat 块挑选 payload 数组作为 each 默认值(见 maily-config.tsxpayload. 前缀变量的扫描逻辑);
  • 通过 imageExtensionOptions 区分聊天场景与邮件场景的图片默认对齐方式与尺寸限制;
  • 通过 onCreateNewVariable 在用户选择未存在于 payload schema 中的变量时动态创建新变量。

这些用法说明,Maily 编辑器在设计上充分考虑了"被宿主应用深度定制"的场景:变量、块、菜单、扩展、节点视图均可按需替换。

小结

本文从安装、基础用法、Slash 命令、变量注入、扩展机制、图片上传到 HTML 渲染,完整覆盖了 @novu/maily-core 的核心 API。实践要点可归纳为:

  1. 记得先 import '@novu/maily-core/style.css',并通过 ./blocks./extensions 子路径按需引入块与扩展;
  2. Slash 命令按"组 → 命令 →(可选)子命令"组织,子命令仅支持一层,searchTerms 影响搜索命中;
  3. 变量数组形式由 Maily 自动过滤,函数形式需自行过滤;required: false 可在缺失时展示占位;
  4. MailyKit.configure({ xxx: false }) 是裁剪默认节点集的快捷开关;
  5. 编辑内容(JSONContent)交给 @novu/maily-render 即可得到兼容邮件客户端的 HTML,并支持主题、预览文本、变量与链接替换、跟踪像素等高级配置。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
900
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
927
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.94 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
603
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
396
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
527