RedwoodJS Mailer 完整指南:从模板渲染、多通道发送到测试与 Studio 集成的端到端邮件体系
2026/9/23 20:50:47 网站建设 项目流程
  • 后端
  • 前端
  • Web框架
  • 开发工具

【免费下载链接】redwood

RedwoodGraphQL

项目地址:https://gitcode.com/gh_mirrors/re/redwood
点击查看免费下载

RedwoodJS 内置的 Mailer 是一个端到端的邮件设计与交付框架:它通过Renderer(渲染器)把 React 组件转成 HTML/纯文本,再通过Handler(处理器)交给 Nodemailer、Resend、SES 等具体服务发出,并针对开发、测试、生产三种环境自动切换发送通道,防止邮件泄漏。读完本文,你将掌握yarn rw setup mailer的完整初始化流程、api/src/lib/mailer.ts的每一项配置、如何在 Service 中发送模板邮件、如何用 InMemory 处理器做快照级单元测试,以及如何接入 Redwood Studio 的模板预览与本地收件箱。

本文以仓库中的 version-6.x/mailer.md 文档为主线,并结合packages/mailer下的核心源码、四个官方处理器、两个官方渲染器及 CLI 生成模板进行源码级印证。

设计目标:为什么 Mailer 不只是"发一封邮件"

RedwoodJS Mailer 的设计出发点,是把"发送邮件"这个动作拆成一条可组合、可测试、可切换的流水线。官方文档明确列出了它的设计约束,这些约束直接决定了下面要讲的架构形态:

  • 可对接主流第三方服务:如 Resend、SendGrid、Postmark、Amazon SES 等;
  • 可自托管:通过 Nodemailer 作为开源自建方案;
  • 按场景选通道:例如事务邮件走 Resend、摘要邮件走 SES,调用方可以在发信时按需指定 handler;
  • 开发/测试环境安全隔离:在 "sandbox" 中发送,绝不意外泄露真实邮件;
  • 支持 React 模板体系:基于 React Email 或 MJML 编写 HTML/纯文本邮件,并预留更多模板方案;
  • 可单元测试:断言 to、from、cc、subject、正文等字段;
  • 与 RedwoodJS Studio 深度集成:用于模板设计与预览。

如文档所说,RedwoodJS Mailer 是"一整套端到端的邮件设计、开发与测试工具包",其核心由handlersrenderers两类抽象组成,配合api/src/lib/mailer.ts配置文件完成装配。

总体架构:Handler × Renderer × 三种运行模式

官方文档给出了一张 Mailer Flow 示意图(见 docs/static/img/mailer/flow.svg),完整描绘了邮件从模板组件到最终投递的路径:

整条链路在源码 packages/mailer/core/src/mailer.ts 的Mailer类中实现。Mailer在构造时做三件事:

  1. 确定运行模式this.mode = this.isTest() ? 'test' : this.isDevelopment() ? 'development' : 'production',见 mailer.ts#L52-L56);
  2. 校验并装载 handlers / renderers,同时为测试与开发模式准备"回退处理器";
  3. 抽取默认发送参数extractDefaults)。

调用mailer.send(...)时,send方法根据当前 mode 决定实际使用哪个 handler:test 模式用测试 handler,development 模式用开发 handler,production 模式才使用发送选项中指定的(或默认的)生产 handler(见 mailer.ts#L164-L180)。这种模式驱动的路由机制,正是"开发/测试不泄露真实邮件"这一设计目标的技术基础。

两种核心抽象:Renderer 与 Handler

Renderer(渲染器)负责把 React 组件渲染成可供邮件客户端使用的字符串(HTML 与纯文本)。抽象基类AbstractMailRenderer定义在 packages/mailer/core/src/renderer.ts,其契约非常简单:

abstract render( template: unknown, options: MailRendererOptions<unknown>, utilities?: MailUtilities, ): MailRenderedContent abstract internal(): Record<string, unknown>

Handler(处理器)负责把渲染后的内容交给真实投递服务。抽象基类AbstractMailHandler定义在 packages/mailer/core/src/handler.ts:

abstract send( renderedContent: MailRenderedContent, sendOptions: MailSendOptionsComplete, handlerOptions?: Record<string | number | symbol, unknown>, utilities?: MailUtilities, ): Promise<MailResult> | MailResult abstract internal(): Record<string, unknown>

注意MailRenderedContent同时包含htmltext两个字段(见 packages/mailer/core/src/types.ts#L9-L12),因此任何官方渲染器都会尽量同时产出两种格式,任何官方 handler 也会把两种格式一并投递。

官方 Renderer 一览

Mailer 目前官方提供两个渲染器,目录均在 packages/mailer/renderers:

Renderer依赖技术源码位置
@redwoodjs/mailer-renderer-react-emailReact Email
@redwoodjs/mailer-renderer-mjml-reactMJML(Faire/mjml-react

以 React Email 渲染器为例,其实现位于 packages/mailer/renderers/react-email/src/index.ts:它支持outputFormat选项,取值'both' | 'html' | 'text',默认'both'html分支使用reactEmailRender(template, { pretty: true, plainText: false })text分支使用plainText: true,从而基于同一份组件同时产出两种正文格式。

官方文档特别强调:邮件客户端在 HTML 渲染上出了名的不一致,因此强烈建议使用 React Email、MJML 这类健壮的组件库来编写邮件模板,以保证跨客户端的视觉效果一致性。

官方 Handler 一览

Mailer 目前官方提供四个处理器,目录在 packages/mailer/handlers:

Handler用途源码位置
@redwoodjs/mailer-handler-in-memory内存收件箱,典型用于测试packages/mailer/handlers/in-memory
@redwoodjs/mailer-handler-nodemailer基于 Nodemailer 的通用 SMTP 发送packages/mailer/handlers/nodemailer
@redwoodjs/mailer-handler-studio把邮件送入 Redwood Studio(内部仍走 Nodemailer)packages/mailer/handlers/studio
@redwoodjs/mailer-handler-resend对接 Resend 云服务packages/mailer/handlers/resend

以 Nodemailer handler 为例(见 packages/mailer/handlers/nodemailer/src/index.ts),它的配置HandlerConfig接受transport(可以是SMTPTransportSMTPTransport.Options或连接字符串)与可选的defaults,构造时即调用nodemailer.createTransport(config.transport, config.defaults)send方法把MailSendOptionsComplete中的to/cc/bcc/from/replyTo/subject/headers/attachments与渲染出的text/html一起传给transporter.sendMail,并返回{ messageID: result.messageId, handlerInformation: result }

Resend handler(见 packages/mailer/handlers/resend/src/index.ts)则略有不同:它通过new Resend(apiToken)创建云客户端,发送时把replyTo映射为 Resend API 的reply_to,并把字符串形式的附件内容转成 UTF-8Buffer后随请求发出;同时支持 Resend 特有的tags选项用于邮件打标。

InMemoryMailHandler(见 packages/mailer/handlers/in-memory/src/index.ts)最为简单:它维护一个inbox数组,send时把完整发送参数、textContenthtmlContent以及utilities中携带的handlerrenderer信息一并压入 inbox,并返回形如in-memory-1messageID;同时提供clearInbox()方法便于测试间重置。这正是文档中测试断言的落点。

如果你的目标服务不在官方列表里,也可以参考以上实现自行编写 handler/renderer,接口契约就在@redwoodjs/mailer-core(packages/mailer/core)中,完成后可以开源回馈社区。

关键文件与目录约定

Mailer 的核心配置文件是api/src/lib/mailer.ts。文档给出了初始化后的默认形态,这与 CLI 生成的模板 packages/cli/src/commands/setup/mailer/templates/mailer.ts.template 完全一致:

import { Mailer } from '@redwoodjs/mailer-core' import { NodemailerMailHandler } from '@redwoodjs/mailer-handler-nodemailer' import { ReactEmailRenderer } from '@redwoodjs/mailer-renderer-react-email' import { logger } from 'src/lib/logger' export const mailer = new Mailer({ handling: { handlers: { // TODO: Update this handler config or switch it out for a different handler completely nodemailer: new NodemailerMailHandler({ transport: { host: 'localhost', port: 4319, secure: false, }, }), }, default: 'nodemailer', }, rendering: { renderers: { reactEmail: new ReactEmailRenderer(), }, default: 'reactEmail', }, logger, })

配置结构说明(类型定义见 packages/mailer/core/src/types.ts#L65-L85):

  • handling.handlers:一个以任意字符串为 key、handler 实例为 value 的映射表;
  • handling.default:生产模式下默认使用的 handler key,必须提供,否则构造时会抛出No default handler configured
  • handling.options:可选,为每个 handler 提供默认的 handlerOptions(发送时会与调用方的 handlerOptions 浅合并,见 mailer.ts#L218-L224);
  • rendering.renderersrendering.default:同理,渲染器的注册表与默认项;
  • defaults:可选,全局默认发送参数(tosubject除外,见下文"默认发送参数"小节);
  • development/test:可选,覆盖两种非生产模式的行为;
  • logger:可选,缺省时回退为console,且 Mailer 会为 logger 挂一个{ module: 'mailer' }的子上下文(见 mailer.ts#L47-L49)。

Mailer 还约定你的邮件模板组件放在api/src/mail目录下。例如欢迎邮件应位于api/src/mail/Welcome/Welcome.tsx。这保证了"配置在lib、模板在mail"的目录约定清晰可循。

初始化:yarn rw setup mailer

新建的 RedwoodJS 应用默认不包含 Mailer,但初始化非常简单,只需运行:

yarn rw setup mailer

该命令会完成两件事(对应 packages/cli/src/commands/setup/mailer/mailer.js 及其 handler mailerHandler.js):

  1. 安装必要依赖:包括@redwoodjs/mailer-core@redwoodjs/mailer-handler-nodemailer@redwoodjs/mailer-renderer-react-email,并且会把@redwoodjs/mailer-handler-in-memory作为devDependency自动加入,以便测试模式默认可用;
  2. 生成初始配置:即上文api/src/lib/mailer.ts模板。

该命令还支持两个可选参数:

  • --force(别名-f):覆盖已存在的配置文件;
  • --skip-examples:只生成必需文件,跳过示例模板。

初始化之后,邮件模板组件(如Welcome.tsx)需要你自己在api/src/mail下创建,命令本身不会替你生成业务模板。

发送邮件:一个完整的 Contact Us 实战

官方文档用一个博客站点的"联系我们"功能作为示例:表单提交的 name、email、message 落库后,同时向内部邮箱发送一封通知邮件。改造后的 Service 如下:

import { mailer } from 'src/lib/mailer' import { ContactUsEmail } from 'src/mail/Example/Example' // ... export const createContact: MutationResolvers['createContact'] = async ({ input, }) => { const contact = await db.contact.create({ data: input, }) // Send email await mailer.send( ContactUsEmail({ name: input.name, email: input.email, // Note the date is hardcoded here for the sake of test snapshot consistency when: new Date(0).toLocaleString(), }), { to: 'inbox@example.com', subject: 'New Contact Us Submission', replyTo: input.email, from: 'contact-us@example.com', } ) return contact }

这段代码做了三件事:

  • 导入Mailer 单例与邮件模板组件;
  • 调用mailer.send,第一个参数是模板组件(可传入基于用户输入的 props),第二个参数是发送选项;
  • 发送选项中的tosubjectreplyTofrom决定邮件收件人、主题、回复地址与发件人。

发送选项的类型契约

send的第二个参数类型是MailSendOptions(见 packages/mailer/core/src/types.ts#L111-L118),它继承MailSendWithoutRenderingOptions,在 types.ts#L89-L102 中定义了完整的字段集:

字段类型必填说明
toMailAddress \| MailAddress[]收件人,MailAddress可以是纯字符串或{ name?, address }对象
ccMailAddress \| MailAddress[]抄送
bccMailAddress \| MailAddress[]密送
fromMailAddress否(可由 defaults 提供)发件人,若最终缺失会抛Missing from address
replyToMailAddress回复地址
subjectstring主题,缺失会抛Missing subject
headersRecord<string, string>自定义邮件头
attachmentsMailAttachment[]附件,MailAttachment支持filenamepathcontent(字符串或 Buffer)
handlerhandler key覆盖生产模式下的 handler
rendererrenderer key覆盖默认渲染器

默认发送参数 defaults

示例里显式写了replyTo,但如果大量邮件都想默认使用replyTo: 'no-reply@example.com',不必每处重复。可以在api/src/lib/mailer.ts中通过defaults统一设置:

defaults: { replyTo: 'no-reply@example.com', },

源码对defaults的处理在 packages/mailer/core/src/utils.ts 的extractDefaults中:cc/bcc/replyTo/from会被预先通过convertAddress转换成标准字符串(形如Name <address>或裸地址),attachmentsheaders缺省为空数组/空对象。注意defaults的类型是Partial<Omit<MailBasicSendOptions, 'to' | 'subject'>>(types.ts#L78),即不能tosubject放进 defaults——这两项必须每次发送时显式指定。

发送时,constructCompleteSendOptions(utils.ts#L61-L130)会把"本次发送选项"与"defaults"合并:本次显式给出的字段优先,未给出的字段回退到 defaults,最终形成一份完整的MailSendOptionsComplete。若合并后仍缺少fromsubjectto,会分别抛出对应的错误。

三种运行模式的自动分流

Mailer 会根据NODE_ENV自动选择模式(各模式的判定与 handler 路由逻辑见 mailer.ts#L319-L345):

模式触发条件邮件去向
test默认NODE_ENV === 'test'测试 handler(默认 in-memory)
development默认NODE_ENV !== 'production'开发 handler(默认 Studio)
production上述均不满足发送选项指定的 handler,未指定则用handling.default

每个模式的when都可以是一个布尔值或返回布尔的函数,handler则指定该模式下使用的 handler key。send/sendWithoutRendering内部都通过switch (this.mode)选取 handler;如果选出的 handler 为null,则直接返回空结果{},即"no-op 不发信"(mailer.ts#L182-L185)。

测试模式:InMemory 收件箱与快照断言

NODE_ENVtest时,Mailer 进入测试模式,所有邮件(无论调用时指定了哪个 handler)都会改走测试 handler。默认行为是:

  • 创建 Mailer 时检查@redwoodjs/mailer-handler-in-memory是否可用(mailer.ts#L66-L90);
  • 可用则自动装载InMemoryMailHandler作为测试 handler,并打印一条 warn 日志;
  • 不可用则测试 handler 退化为 no-op,邮件不投递、不落库;
  • 由于yarn rw setup mailer已把 in-memory 包加入 devDependencies,正常项目默认即有可用的测试收件箱。

如果需要显式控制测试模式,可在api/src/lib/mailer.ts中加入:

test: { when: process.env.NODE_ENV === 'test', handler: 'someOtherHandler', }
  • when:布尔值或返回布尔值的函数,决定 Mailer 创建时是否进入测试模式;
  • handler:测试模式下实际使用的 handler key;若设为null则测试模式下完全不发信。

在测试代码中,可以通过mailer.getTestHandler()拿到测试 handler(mailer.ts#L347-L356),进而读取inbox做断言。文档给出的完整测试示例:

describe('contacts', () => { scenario('creates a contact', async () => { const result = await createContact({ input: { name: 'String', email: 'String', message: 'String' }, }) expect(result.name).toEqual('String') expect(result.email).toEqual('String') expect(result.message).toEqual('String') // Mail const testHandler = mailer.getTestHandler() as InMemoryMailHandler expect(testHandler.inbox.length).toBe(1) const sentMail = testHandler.inbox[0] expect({ ...sentMail, htmlContent: undefined, textContent: undefined, }).toMatchInlineSnapshot(` { "attachments": [], "bcc": [], "cc": [], "from": "contact-us@example.com", "handler": "nodemailer", "handlerOptions": undefined, "headers": {}, "htmlContent": undefined, "renderer": "reactEmail", "rendererOptions": {}, "replyTo": "String", "subject": "New Contact Us Submission", "textContent": undefined, "to": [ "inbox@example.com", ], } `) expect(sentMail.htmlContent).toMatchSnapshot() expect(sentMail.textContent).toMatchSnapshot() }) })

这个测试覆盖了三层断言:

  1. 发送数量inbox.length为 1,确认恰好发了一封;
  2. 发送选项:通过内联快照断言tofromreplyTosubjectcc/bcc/headers/attachments以及handlerrenderer等元信息与期望一致;
  3. 渲染结果htmlContenttextContent分别用toMatchSnapshot()锁定,保证模板改动不会悄悄破坏产出。

值得注意的是,InMemoryMailHandler会把utilities中的handlerrendererkey 一并记录到 inbox 条目里(in-memory/src/index.ts#L32-L40),因此快照里能看到"handler": "nodemailer""renderer": "reactEmail"——即使在测试模式下,也保留了"生产环境会走哪个通道"的可观测信息。官方核心测试 packages/mailer/core/src/tests/mailer.test.ts 也用 Mock handler/renderer 验证了模式切换、默认 handler 校验等行为,可作为深入阅读的起点。

开发模式:Studio 本地收件箱与实时预览

与测试模式类似,Mailer 还提供开发模式。当NODE_ENV不是production时自动进入;默认尝试加载@redwoodjs/mailer-handler-studio作为开发 handler(mailer.ts#L91-L115),把邮件送入 Redwood Studio 内置的本地 SMTP 收件箱。

可通过如下配置自定义:

development: { when: process.env.NODE_ENV !== 'production', handler: 'someOtherHandler', },

配置语义与test完全一致:when决定是否进入开发模式,handler指定该模式使用的 handler(null表示不发信)。

Template Previews 模板预览

开发模式下,Studio 可以提供邮件模板的实时预览:你更新模板代码时预览会自动重新渲染;还可以提供一个 JSON payload 作为模板组件的 props。官方文档明确指出预览是"近似"效果,但对大多数场景足以覆盖 90% 的观感判断。

Local Inbox 本地收件箱

使用默认的 Studio 开发 handler 时,你应用里发出的每封邮件都会进入 Studio 内置的本地 SMTP 收件箱。这样你可以在本地完整跑通"发送 -> 收件 -> 查看"流程,无需自建本地邮箱,也不必借助在线临时邮箱服务。

:::warning Redwood Studio 目前仍是实验性功能,处于持续开发中。上述 UI 可能与最新版本略有差异,功能细节也可能会随时间调整。 :::

生产模式:直达默认通道

NODE_ENV既不是test也不是development时,Mailer 进入生产模式。此模式下不会重定向任何邮件:邮件直接交给发送选项中指定的 handler;未指定则使用handling.default配置的默认 handler。这也是配置文件中default字段为什么是必填项的原因——生产模式的兜底通道完全由它决定。

另外,Mailer还暴露了sendWithoutRendering方法(mailer.ts#L252-L317):当你已经有现成的 HTML/文本内容、不需要渲染器时,可以直接把内容交给 handler 发送,同样支持按模式分流与 defaults 合并。此外还有一组 getter 可用于编程式访问当前环境下的实际通道:getTestHandler()getDevelopmentHandler()getDefaultProductionHandler()getDefaultHandler()getDefaultRenderer()

自定义 Handler 与 Renderer:扩展生态

如果官方 handler/renderer 无法满足你的技术栈,Mailer 并不阻止你自建。做法是:

  1. 阅读现有实现作为参照:
    • handlers:packages/mailer/handlers
    • renderers:packages/mailer/renderers
  2. 实现AbstractMailHandler/AbstractMailRenderer定义的接口(位于 packages/mailer/core 的@redwoodjs/mailer-core包中),并实现internal()方法暴露实例内部状态;
  3. 在你的api/src/lib/mailer.ts中注册并设为默认(或按模式指定);
  4. 可以把自己的实现开源分享给 RedwoodJS 社区,也欢迎在社区论坛中交流。

一个自定义 handler 的最小形态可以参考 mailer.test.ts 里的MockMailHandler:继承AbstractMailHandler,实现sendinternal即可。Mailer构造时会自动校验你注册的默认 handler/renderer 是否真实存在于映射表中,不存在会直接抛错,从而避免生产环境出现"配置了却发不出"的隐性故障。

小结

RedwoodJS Mailer 的价值在于把"邮件能力"做成了与框架一体的工程化组件:api/src/lib/mailer.ts一处装配,api/src/mail目录统一管理模板,send一条 API 覆盖渲染与投递,而 test/development/production 三态路由保证了从开发、测试到上线的全链路安全。配合 InMemory 收件箱做快照级测试、Studio 做模板预览与本地收件,它确实是官方文档所说的"端到端设计、开发与测试的完整邮件工具包"。若需深入实现细节,可从 packages/mailer/core 的Mailer类及其单元测试读起,再逐层查看你所用 handler 与 renderer 的具体实现。

  • 后端
  • 前端
  • Web框架
  • 开发工具

【免费下载链接】redwood

RedwoodGraphQL

项目地址:https://gitcode.com/gh_mirrors/re/redwood
点击查看免费下载

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询