
Bitwarden Server 邮件模板体系基于 MJML Handlebars 的双层编译流水线实战指南【免费下载链接】serverBitwarden infrastructure/backend (API, database, Docker, etc).项目地址: https://gitcode.com/GitHub_Trending/ser/server本篇技术指南聚焦 Bitwarden server 仓库中src/Core/MailTemplates/Mjml目录所实现的邮件模板体系它如何用 MJMLMailJet Markup Language以组件化方式编写响应式邮件模板再编译为*.html.hbs交由 Handlebars 注入变量、最终由IMailer/IMailService发送。读完本文你将掌握 MJML 模板的构建命令、自定义组件注册、与 C# 邮件服务对接的完整开发与测试流程并能直接在当前仓库中复现每一步操作。背景为什么用 MJML 写邮件模板传统响应式邮件模板需要兼容 Outlook、Gmail、Apple Mail 等数十种邮件客户端手写 table 布局与内联样式极其痛苦。MJML 是一种专为邮件设计的标记语言其核心理念是开发者用语义化组件编写模板MJML 编译器负责生成跨客户端兼容的内联样式 HTML。官方仓库的组件化能力同时提升了代码质量与复用性。在 Bitwarden server 中邮件 HTML 全部由 MJML 生成。整个链路包含三种文件形态各有分工文件类型作用产生方式*.mjml开发者编写的组件化邮件源码手写*.html.hbs已编译、含 Handlebars 变量的 HTML 邮件模板由*.mjml构建生成*.txt.hbs纯文本版邮件模板手写MJML 不参与整体流程可用一句话概括MJML 模板编译成 HTMLHTML 再交给 Handlebars 填充{{变量}}渲染最终邮件。由于*.mjml源码中的{{ }}会被 MJML 编译器原样保留开发者可以在 MJML 里直接书写 Handlebars 双花括号语法实现一份源码、两阶段渲染。提示MJML 全称是MailJet Markup Language。与旧体系的关系仓库中曾长期使用MailServiceHandlebarsMailService统一管理全部邮件但其缺陷明显所有邮件集中在一个类里无法由各团队独立维护。官方已在 Mail 服务说明 中明确标注MailService已废弃新邮件一律采用 MJML IMailer方案MailService路径下仍保留了 Handlebars/MJML 目录作为兼容产物。*.txt.hbs的创建方式不受 MJML 影响保持原有手写模式。构建流水线从 MJML 到 HTML.HBSMjml目录是一个独立的 npm 包bitwarden/mjml-emails见 package.json依赖mjml4.15.3、mjml-core4.15.3并用nodemon实现监听编译、prettier统一格式。构建逻辑集中在 build.js。构建脚本解析build.js的核心流程是扫描emails/**/*.mjml排除components目录因为它只存放可复用片段而非成品模板对每个文件调用mjml2html()编译并把结果按原目录结构写入out/目录。几个关键实现细节值得注意校验级别编译时传入validationLevel: strict任何 MJML 语法问题都会报错并统计失败数最终以非零退出码结束构建include 解析通过filePath参数告知 MJML 当前文件的绝对路径使mj-include相对引用能被正确解析组件注册目录mjmlConfigPath: __dirname指向.mjmlconfig所在目录输出命名普通构建输出*.html加--hbs标志则输出*.html.hbs--minify输出压缩版本。package.json中的scripts与 README 中列出的命令一一对应其中build:watch的等价实现是nodemon ./build.js --watch emails --watch components --ext mjml,js。常用命令速查在src/Core/MailTemplates/Mjml目录下执行npm ci # 编译全部 *.mjml 为 *.html 到 ./out 目录 npm run build # 监听 *.mjml 与 *.js 变更自动重新编译新增文件不会被跟踪需重新执行本命令 npm run build:watch # 编译为 *.html.hbs 到 ./out 目录 npm run build:hbs # 编译为压缩后的 *.html.hbs 到 ./out 目录 npm run build:minify # 对源码应用 prettier 格式化 npm run prettierbuild.js还支持--trace打印输入/输出目录等调试信息、--clean等参数可用node ./build.js --help查看。目录结构与实际模板解剖Mjml目录下分三块components/全局可复用 MJML 片段与自定义 JS 组件如head.mjml、footer.mjml、logo.mjml、mj-bw-hero.jsemails/按业务域组织的成品邮件模板如AdminConsole/、Auth/、Billing/及根级invite.mjml构建产物out/编译后的*.html/*.html.hbs。以根级 emails/invite.mjml 为例一个真实模板的骨架如下mjml mj-head mj-include path../components/head.mjml / /mj-head mj-body mj-wrapper css-classborder-fix padding20px 20px mj-bw-hero img-srchttps://assets.bitwarden.com/email/v1/business.png titleA Bitwarden member has invited you to Bitwarden Password Manager button-textFinish account setup button-url# / mj-section mj-column mj-button href#Join Organization Now/mj-button mj-text This invitation expires on bTuesday, January 23, 2024 2:59PM UTC/b. /mj-text /mj-column /mj-section mj-bw-learn-more-footer / /mj-wrapper mj-include path../components/footer.mjml / /mj-body /mjml这个例子同时演示了三种复用手段mj-include引入静态片段head、footer、自定义组件mj-bw-hero、以及内联的 MJML 原生组件mj-wrapper/mj-section/mj-column/mj-button/mj-text。head.mjml全站统一的样式基座components/head.mjml 定义了所有邮件共享的样式与排版Helvetica Neue字体族、16px 基准字号、#175ddc品牌蓝按钮、#1B2029正文文字色、660px 宽度的mj-body背景同时提供.link内联样式类和.border-fix的 table 圆角修正解决邮件客户端border-collapse兼容问题。当前所有模板都统一引入该文件保证跨邮件的一致性未来若支持多布局再行拆分。开发流程从零新建一封 MJML 邮件编写与预览在emails/下对应团队的目录中创建cool-email.mjml运行npm run build:watch用浏览器打开out/目录中编译出的 HTML 预览效果修改*.mjml或组件*.js后刷新浏览器即可看到最新结果。用 IMailer 验证变量填充MJML 编译产物中的{{变量}}需要 Handlebars 注入真实数据后才算有效邮件。官方流程要求 ViewModel、.html.hbs构建产物、.text.hbs三者位于同一目录详见 Platform/Mail README 的 Step 3 说明运行npm run build:hbs生成*.html.hbs将out/目录中的全部*.html.hbs复制到对应的src/Core/MailTemplates/Mjml目标目录与 ViewModel 同目录。如果改动了共享组件必须全量覆盖该目录下所有文件以捕获*.html.hbs中的变化运行发送邮件的代码进行验证。压缩版*.html.hbs是交付物deliverable只有被正确放置到src/Core/MailTemplates/Mjml对应目录、并作为内嵌资源编译进程序集IMailer实现才能读取到它见下文渲染器源码分析。用 IMailService 验证已废弃仅兼容警告IMailService已废弃请改用上面的IMailer流程。历史流程类似npm run build:hbs后将Core/MailTemplates/Mjml/out的全部文件复制到src/Core/MailTemplates/Handlebars/MJML目录共享组件改动时同样需要全量覆盖再运行发信代码交付物需放入src/Core/MailTemplates/Handlebars/相关目录供IMailService消费。自定义组件以 mj-bw-hero 为例MJML 支持自定义组件在.mjmlconfig中注册一个返回 MJML 标记字符串的 JavaScript 类即可。mj-bw-hero是仓库内置的典型示例其实现见 components/mj-bw-hero.jsconst { BodyComponent } require(mjml-core); class MjBwHero extends BodyComponent { static dependencies { // 声明父标签mj-column / mj-wrapper 内部允许使用 mj-bw-hero mj-column: [mj-bw-hero], mj-wrapper: [mj-bw-hero], // 声明子标签mj-bw-hero 不允许嵌套任何子标签 mj-bw-hero: [], }; static allowedAttributes { img-src: string, // REQUIRED: 蓝色头部右侧展示的图片地址 title: string, // REQUIRED: 说明邮件主要目的的大号文字 button-text: string, // OPTIONAL: 按钮上显示的文字 button-url: string, // OPTIONAL: 点击按钮跳转的 URL sub-title: string, // OPTIONAL: 为标题提供补充信息的小字 }; static defaultAttributes {}; componentHeadStyle (breakpoint) { return media only screen and (max-width:${breakpoint}) { .mj-bw-hero-responsive-img { display: none !important; } } ; }; render() { const buttonElement /* 按属性是否存在拼接 mj-button */; const subTitleElement /* 按 sub-title 是否存在拼接 mj-text */; return this.renderMJML(mj-section ....../mj-section); } } module.exports MjBwHero;该实现展示了几个关键模式dependencies静态属性同时充当 MJML 校验器的白名单——声明哪些标签可作父级、哪些可作子级保证 strict 校验通过allowedAttributes声明组件接受的属性与类型属性是否必填由开发者自行控制条件渲染render()中按button-text/button-url、sub-title是否同时存在来决定是否生成对应元素实现同一组件、多种形态响应式样式componentHeadStyle(breakpoint)生成媒体查询在窄屏下隐藏右侧图片display: none !important这是邮件场景典型的移动端适配手段。在模板中的使用方式与普通 MJML 标签一致mj-bw-hero img-srchttps://assets.bitwarden.com/email/v1/business.png titleVerify your email to access this Bitwarden Send /注册组件.mjmlconfig自定义组件必须写入 .mjmlconfig 才能被编译与渲染。当前仓库共注册了 8 个组件分为全局与 AdminConsole 专属两组{ packages: [ components/mj-bw-hero, components/mj-bw-simple-hero, components/mj-bw-icon-row, components/mj-bw-learn-more-footer, emails/AdminConsole/components/mj-bw-inviter-info, emails/AdminConsole/components/mj-bw-ac-hero, emails/AdminConsole/components/mj-bw-ac-icon-row, emails/AdminConsole/components/mj-bw-ac-icon-row-without-bulletins, emails/AdminConsole/components/mj-bw-ac-learn-more-footer ] }从源码结构看AdminConsole 团队在emails/AdminConsole/components/下沉淀了面向组织管理类邮件的专属组件含邀请人信息、带/不带项目符号的图标行等体现了全局组件 业务域组件的分层复用思路。各 MJML 标签的属性集并不统一动手前建议查阅 MJML 官方组件文档确认用法。mj-include静态模板复用除自定义组件外还可以通过mj-include引用静态 MJML 片段将重复出现的段落抽成独立文件。仓库实际用法见 emails/invite.mjmlmj-wrapper padding5px 20px 10px 20px mj-include path../../components/learn-more-footer.mjml / /mj-wrapper路径相对于当前*.mjml文件解析且在编译时会校验引用文件是否存在。与 C# 后端的对接渲染器源码级印证IMailer 体系推荐Mailer 体系由四个核心部件构成Platform/Mail/README.mdIMailer—— 发送邮件的服务接口BaseMailTView—— 定义收件人、主题、分类等元数据的抽象基类BaseMailView—— 模板数据 ViewModel 的抽象基类IMailRenderer—— 模板渲染接口由HandlebarMailRenderer实现。HandlebarMailRendererHandlebarMailRenderer.cs的实现细节正好印证了 MJML 交付物的使用方式命名约定模板名按{ViewModel全名}.html.hbs/.text.hbs解析例如Bit.Core.Auth.Models.Mail.VerifyEmailView.html.hbs因此交付物必须与 ViewModel 同目录、同名内嵌资源*.html.hbs作为程序集内嵌资源读取GetManifestResourceStream这正是 README 强调必须把交付物放到正确目录的底层原因——文件需经.csproj的EmbeddedResource Include**\*.hbs /声明后随编译进入程序集自托管覆盖Self-Hosted 场景下优先从MailTemplateDirectory磁盘目录读取模板并对路径穿越做了防护StartsWith(baseDirectory)校验性能设计使用ConcurrentDictionarystring, LazyTaskHandlebarsTemplate缓存已编译模板Lazy配合ExecutionAndPublication保证每个模板恰好编译一次Handlebars 实例本身惰性初始化且线程安全适合并发渲染。发送一封新邮件的最小链路是定义XxxView : BaseMailView→ 定义XxxMail : BaseMailXxxView重写Subject→ 编写同名.html.hbs/.text.hbs→ 调用await _mailer.SendEmail(mail)。关键邮件可通过IgnoreSuppressList true绕过退订名单仅限账号恢复、OTP 等场景。IMailService 兼容路径旧体系由HandlebarsMailService实现IMailService消费 Handlebars 目录下的产物其中包含MJML子目录存放编译产物。它仍保留用于存量邮件新开发一律走IMailer。邮件模板的总体说明可参考 MailTemplates/README.md。邮件静态资源管理模板中引用的图片如https://assets.bitwarden.com/email/v1/business.png统一托管在assets.bitwarden.com/email/v1路径下对应独立的 assets 静态存储邮件素材的增删改通过专门的 assets 仓库 PR 流程完成。常见问题与最佳实践build:watch不跟踪新文件nodemon只监听已有文件的变更新建*.mjml或*.js后需手动重新执行一次npm run build:watch修改共享组件必须全量覆盖组件被多个模板引用其渲染结果已烙进各*.html.hbs因此复制交付物时应覆盖目标目录全部文件避免旧模板残留strict 校验失败会中断构建编译错误会逐条打印formattedMessage并累计失败数最终process.exit(1)CI 中可直接据此判断构建成败变量命名规范Handlebars 变量建议使用 camelCase如{{userName}}、{{organizationName}}URL 变量使用描述性前缀如{{actionUrl}}BaseMailView内置CurrentYear属性供版权年份等场景直接使用纯文本不可省略每封邮件都应同时提供*.text.hbs兼顾无障碍阅读、低版本客户端兼容与 HTML 渲染失败时的兜底。结语MJML Handlebars 的双层流水线是 Bitwarden server 邮件体系的事实标准MJML 负责响应式 HTML 生成Handlebars 负责动态变量注入二者通过*.html.hbs交付物衔接再由IMailer渲染发送。开发者只需在emails/下编写*.mjml、注册自定义组件、跑通build:hbs并放置交付物即可把一封风格统一、跨客户端兼容的邮件接入系统——这一整套流程在当前仓库中均有源码与配置可循可直接按本文步骤复现。【免费下载链接】serverBitwarden infrastructure/backend (API, database, Docker, etc).项目地址: https://gitcode.com/GitHub_Trending/ser/server创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考