后端前端Web框架开发工具【免费下载链接】redwoodRedwoodGraphQL项目地址https://gitcode.com/gh_mirrors/re/redwood点击查看免费下载本文是一篇面向 RedwoodJS 开发者的完整实战指南以用户列表 邮件发送 审计日志为案例带你走通从项目创建、数据库建模、Scaffold 界面生成到接入 SendInBlue SMTP 服务、封装 nodemailer 发送函数、再到通过服务调用服务写入审计日志的完整链路。读完本文你将掌握在 RedwoodJS 中发送事务性邮件的标准做法理解 Service 层函数直接互相调用的威力及其安全边界并能将这套模式迁移到通知、验证码、账单等任意邮件场景。整体思路几乎所有业务应用最终都会遇到发邮件的需求。RedwoodJS 官方给出的典型做法是在 API 侧使用 npm 包 nodemailer 通过 SMTP 协议投递邮件配合像 SendInBlue 这类云邮件服务商提供可靠的投递能力同时利用 RedwoodJS 的一个强大特性——一个 Service 直接调用另一个 Service 的导出函数——来实现每次发信都写入一条审计记录的附加能力。本文将要构建的应用包含两个数据模型User用户及其邮箱地址发送邮件的目标Audit发送历史审计日志与用户建立一对多关联。核心链路为Web 端点击Send email按钮 → GraphQL mutation 进入 users Service → 调用封装的sendEmail发送邮件 → 再调用 audits Service 的createAudit写入审计日志。一、环境准备创建项目并安装 nodemailer首先创建一个全新的 RedwoodJS 项目本教程使用 TypeScript 版本yarn create redwood-app --typescript email创建完成后进入项目目录并在api工作区安装nodemailer邮件发送库需要运行在 API 侧yarn workspace api add nodemailerRedwoodJS 使用 Yarn workspace 组织api与web两个工作区因此通过yarn workspace api add将依赖精确安装到后端这正是 Redwood 多包工程的标准依赖管理方式。二、数据库设计User 与 Audit 双模型打开api/db/schema.prisma删除示例模型替换为下面两个模型model User { id String id default(uuid()) createdAt DateTime default(now()) updatedAt DateTime default(now()) updatedAt email String unique name String? audits Audit[] } model Audit { id String id default(uuid()) createdAt DateTime default(now()) updatedAt DateTime default(now()) updatedAt userId String user User relation(fields: [userId], references: [id]) log String }几个设计要点值得说明技术上User真正必需的字段只有email和关联字段audits但实践中建议始终从一开始就带上id、createdAt、updatedAt——事后补加字段的成本远高于一开始就加上。name字段是为了让示例更贴近真实业务。Audit同样做了简化真实的审计追踪通常需要更丰富的字段操作人、IP、上下文等单条log字符串仅用于演示。最关键的是一对多关系Audit通过userId外键关联User这样就能通过user.audits轻松找出某个用户的所有发送记录。模型定义完成后执行数据库迁移让表结构落地yarn rw prisma migrate dev --name email这条命令会为这两个模型生成迁移文件并更新 Prisma Client。三、用 Scaffold 快速生成 CRUD 界面RedwoodJS 的 Scaffold 是其招牌功能之一可以一次性生成模型对应的页面、组件、SDL 与 Service 全套代码。依次为两个模型生成脚手架yarn rw g scaffold User yarn rw g scaffold Audit然后启动开发服务器yarn rw dev浏览器会自动打开 Redwood 默认首页。点击/users链接进入用户列表页手动创建几个用户。注意由于后续要真实发送邮件请务必使用可以实际接收邮件的邮箱地址。可以使用类似 fakenamegenerator 的在线工具生成带有真实可用邮箱的随机用户点击其页面上的激活链接后即可正常收信。创建三个用户后列表页会展示三行用户数据进入单个用户的详情页可以看到完整的用户信息。下一步我们要在详情页上加一个发送邮件的按钮。四、前端在 User 详情页添加Send email按钮修改脚手架生成的User组件web/src/components/User/User.tsx加入发送邮件的按钮和对应的 GraphQL mutation。下面是完整代码避免遗漏任何细节import { useMutation } from redwoodjs/web import { toast } from redwoodjs/web/toast import { Link, routes, navigate } from redwoodjs/router const DELETE_USER_MUTATION gql mutation DeleteUserMutation($id: String!) { deleteUser(id: $id) { id } } const EMAIL_USER_MUTATION gql mutation EmailUserMutation($id: String!) { emailUser(id: $id) { id } } const timeTag (datetime) { return ( time dateTime{datetime} title{datetime} {new Date(datetime).toUTCString()} /time ) } const User ({ user }) { const [deleteUser] useMutation(DELETE_USER_MUTATION, { onCompleted: () { toast.success(User deleted) navigate(routes.users()) }, onError: (error) { toast.error(error.message) }, }) const [emailUser] useMutation(EMAIL_USER_MUTATION, { onCompleted: () { toast.success(Email sent) }, onError: (error) { toast.error(error.message) }, }) const onDeleteClick (id) { if (confirm(Are you sure you want to delete user id ?)) { deleteUser({ variables: { id } }) } } const onEmailClick (user) { if (confirm(Are you sure you want to send an email to ${user.name}?)) { emailUser({ variables: { id: user.id } }) } } return ( div classNamerw-segment header classNamerw-segment-header h2 classNamerw-heading rw-heading-secondary User {user.id} Detail /h2 /header table classNamerw-table tbody tr thId/th td{user.id}/td /tr tr thCreated at/th td{timeTag(user.createdAt)}/td /tr tr thUpdated at/th td{timeTag(user.updatedAt)}/td /tr tr thEmail/th td{user.email}/td /tr tr thName/th td{user.name}/td /tr /tbody /table /div nav classNamerw-button-group Link to{routes.editUser({ id: user.id })} classNamerw-button rw-button-blue Edit /Link button typebutton classNamerw-button rw-button-red onClick{() onDeleteClick(user.id)} Delete /button button typebutton classNamerw-button rw-button-blue onClick{() onEmailClick(user)} Send email /button /nav / ) } export default User代码要点通过useMutation声明了DELETE_USER_MUTATION与新增的EMAIL_USER_MUTATION两个 mutation后者的onCompleted会弹出Email sent的成功提示onError则展示错误信息。按钮点击后先用confirm与用户二次确认再以variables: { id }触发emailUsermutation。useMutation、toast、Link/routes/navigate分别来自redwoodjs/web、redwoodjs/web/toast与redwoodjs/router这些都是 Redwood 前端包中内置导出的标准 API。五、打通后端SDL 与 Service前端 mutation 需要后端 SDLSchema Definition Language暴露同名 mutation。在users.sdl.ts中追加export const schema gql // ... type Mutation { // ... emailUser(id: String!): User! requireAuth } 这里requireAuth指令要求调用者必须经过身份认证属于 Redwood 的安全基线更多指令可参考仓库中的 directives 文档。随后在 users Serviceapi/src/services/users/users.ts里先写一个占位实现验证 GraphQL 链路是否打通// ... import type { Prisma } from prisma/client // ... export const emailUser async ({ id }: Prisma.UserWhereUniqueInput) { const user await db.user.findUnique({ where: { id }, }) console.log(Sending email to, user) return user } // ...六、接入 SendInBlue获取 SMTP 凭据要真实投递邮件必须有一个可通过 SMTP 通信的邮件服务器。nodemailer 官方示例中的 Ethereal 仅用于测试——邮件永远不会真正送达。自建 GMail 地址则需要配置复杂的 OAuth2。最稳妥的方案是使用专门的云邮件 SaaS 服务它们大多提供足以支撑小型生产应用流量的免费额度——SendInBlue现 Brevo每天提供 300 封免费邮件。在 SendInBlue 注册并完成验证后按以下步骤获取 SMTP 密钥点击右上角公司名称所在菜单选择SMTP API在选项卡中点击SMTP生成一个新的 SMTP Key名称随意复制生成的密钥。回到代码编辑器打开.env文件在末尾新增一行环境变量SEND_IN_BLUE_KEYxsmtpsib-7fa6eb37c244429933ea870185063c493ba1c820f826c5f620877dd815392602-rZgB6GUV1CF2NLAK示例中展示的是格式参考请务必替换为你自己生成的真实密钥。环境变量承载敏感凭据切勿提交到版本库RedwoodJS 会把.env中的变量注入到 API 进程。如果开发服务器仍在运行需要重启一次才能加载新的环境变量。七、核心实现封装 email.ts 发送函数在 API 侧的lib目录下新建api/src/lib/email.ts将邮件发送逻辑封装为独立可复用的模块import * as nodemailer from nodemailer interface Options { to: string | string[] subject: string text: string html: string } export async function sendEmail({ to, subject, text, html }: Options) { console.log(Sending email to:, to) // create reusable transporter object using SendInBlue for SMTP const transporter nodemailer.createTransport({ host: smtp-relay.sendinblue.com, port: 587, secure: false, // true for 465, false for other ports auth: { user: youremail.com, pass: process.env.SEND_IN_BLUE_KEY, }, }) // send mail with defined transport object const info await transporter.sendMail({ from: Your Name youremail.com, to: Array.isArray(to) ? to : [to], // list of receivers subject, // Subject line text, // plain text body html, // html body }) return info }使用前需要注意把youremail.com替换为注册 SendInBlue 时使用的邮箱两处auth.user与from并注意与 SendInBlue 网站上显示的邮箱保持一致它区分大小写Your Name可以改成你自己的发件人名称。port: 587配合secure: false是标准的 STARTTLS 提交端口若使用 465 端口则需要secure: true代码注释已说明。sendEmail返回 nodemailer 的info对象其中包含messageId等投递元信息可用于后续日志记录。回到 users Service先导入发送函数// ... import { sendEmail } from src/lib/email // ...再在文件中新增一个测试邮件辅助函数负责构造邮件内容// ... function sendTestEmail(emailAddress: string) { const subject Test Email const text This is a manually triggered test email.\n\n It was sent from a RedwoodJS application. const html This is a manually triggered test email.brbr It was sent from a RedwoodJS application. return sendEmail({ to: emailAddress, subject, text, html }) } // ...最后把前面emailUser里的console.log替换为真实发送调用// ... await sendTestEmail(user.email) // ...现在点击详情页的 Send email 按钮终端会打印Sending email to: 邮箱几分钟后邮件就会出现在收件箱中若使用 fakenamegenerator 生成的临时邮箱投递可能需要更长时间请耐心等待。八、服务间调用实现发送审计最后一步是加入审计每当 users Service 发送邮件就调用 audits Service 写入一条审计日志。RedwoodJS 让这件事异常简单——只需 import 另一个 Service即可直接使用其导出的全部函数// ... import { createAudit } from ../audits/audits // ... export const emailUser async ({ id }: Prisma.UserWhereUniqueInput) { // ... await sendTestEmail(user.email) await createAudit({ input: { userId: id, log: Admin sent test email to user }, }) // ... } // ...createAudit的参数结构input包裹、userId关联既有用户、log为日志内容初看可能不直观但 TypeScript 类型会给出清晰的提示。审计条目的时间戳与 id 会由数据库自动生成。安全提示Service 直调会绕过 GraphQL 指令这里有一个必须强调的安全边界Service 之间的直接函数调用会绕过 GraphQL 层的权限指令。Web 端通过 GraphQL 调用 Service 时受requireAuth等指令保护但如果在 API 内部某个使用skipAuth任何人可访问的 Service 导入了另一个 Service它就可以直接调用对方导出的任意函数——无论目标函数的 SDL 上挂了什么指令。在本例中emailUser本身带有requireAuth所以不受此问题影响但当你采用Service 调 Service模式时务必确保调用链入口的公开程度符合预期。查看审计结果打开之前 scaffold 生成审计页面开发服务器默认地址为http://localhost:8910/audits就能看到每次发送邮件产生的审计记录实现邮件发送历史可追溯。九、从源码理解邮件发送在 RedwoodJS 中的位置本教程采用的nodemailer SMTP 直连是 RedwoodJS 生态中一种轻量、自托管的邮件方案。如果你希望获得更工程化的邮件能力模板渲染、多服务商切换、测试/开发沙箱、Studio 预览可以进一步了解仓库中官方维护的 Mailer 模块详见 Mailer 官方文档。从源码中可以印证其设计脉络Mailer 核心packages/mailer/core/src/mailer.ts在构造时会根据NODE_ENV自动决定运行模式测试环境自动回退到redwoodjs/mailer-handler-in-memory的InMemoryMailHandler不会真正发信开发环境自动回退到 Studio handler生产环境才使用默认 handler。这一模式切换机制正是为了解决本教程中提到的测试/开发时不要误发真实邮件的痛点避免邮件意外泄露。nodemailer handler 实现packages/mailer/handlers/nodemailer/src/index.ts通过nodemailer.createTransport(config.transport, config.defaults)创建 transporter并在send中将to/cc/bcc/from/replyTo/subject/headers/text/html/attachments映射为transporter.sendMail的参数最终返回包含messageID的MailResult——与本教程手写的email.ts在思路上完全同构只是被抽象成了可插拔的 Handler 接口。类型契约packages/mailer/core/src/types.ts定义了MailerConfighandling/rendering/defaults/development/test、MailSendOptionsto/cc/bcc/from/replyTo/subject/headers/attachments等结构你可以看到to/cc/bcc均支持string | { name, address }的地址形态附件则支持filename/path/content三种来源。配置入口执行yarn rw setup mailer会在api/src/lib/mailer.ts生成 Mailer 配置注册 handler、renderer 并指定默认项邮件模板组件约定放在api/src/mail目录与api/src/lib/email.ts这种手写封装是两条并行的落地路线可根据团队偏好选择。十、完整验证与运行按顺序走完上述步骤后整个应用的验证路径如下yarn rw dev启动开发服务器访问/users创建若干测试用户进入某用户详情页点击Send email确认弹窗提示Email sent在 API 侧终端观察Sending email to: ...日志到测试邮箱确认收到主题为Test Email的邮件注意区分纯文本text与 HTML 版本访问http://localhost:8910/audits确认出现对应的审计记录且每条记录都关联到正确的用户。至此你已经在 RedwoodJS 中完整实现了用户管理 → 触发邮件 → 审计留痕的闭环。这套模式可以平移到任意业务场景注册欢迎信、密码重置、订单通知、验证码发送——只需要替换sendTestEmail中的主题与正文模板并在需要时按同样的方式在发送前/后调用其他 Service 完成附加业务逻辑。赞分享后端前端Web框架开发工具【免费下载链接】redwoodRedwoodGraphQL项目地址https://gitcode.com/gh_mirrors/re/redwood点击查看免费下载相关推荐RedwoodJS 实战用 Nodemailer SendInBlue 构建邮件发送与审计日志服务RedwoodJS 实战用 Nodemailer SendInBlue 构建邮件发送与审计日志服务 本文是 RedwoodJS 官方文档 Sending后端前端Web框架开发工具使用 Nodemailer 与 SendInBlue 在 RedwoodJS 中发送邮件从 Prisma 建模到服务间调用审计使用 Nodemailer 与 SendInBlue 在 RedwoodJS 中发送邮件从 Prisma 建模到服务间调用审计 本篇技术指南基于 Redwoo后端前端Web框架开发工具RedwoodJS 实战基于 Nodemailer 与 SendInBlue 实现邮件发送与审计日志RedwoodJS 实战基于 Nodemailer 与 SendInBlue 实现邮件发送与审计日志 本篇指南以 RedwoodJS v4.x 为背景完整演后端前端Web框架开发工具上一篇React-Static中的构建分析识别bundle中的问题下一篇BibiGPT终极指南AI音视频内容一键总结的完整解决方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
