
Baserow 邮件模板编译管线MJML Eta 构建期编译 Django 邮件模板的实现解析【免费下载链接】baserowBuild databases, automations, apps agents with AI — no code. Open source platform available on cloud and self-hosted. GDPR, HIPAA, SOC 2 compliant. Best Airtable alternative.项目地址: https://gitcode.com/GitHub_Trending/ba/baserowBaserow 的所有交易类/通知类邮件注册确认、密码重置、工作区邀请、通知汇总等都以.mjml.eta源文件形式维护在 Django 模板目录中由backend/email_compiler目录下的 Node.js 脚本在构建期或开发期 watch 模式编译为可直接被 Django 渲染的 HTML 模板。本文基于 email_compiler 的 README 与 baserowEmailCompiler.js 源码完整讲解这条「Eta 模板 → MJML → Django HTML 模板」两级编译管线的设计动机、运行方式与源码实现细节帮助你在 Baserow 中新增或修改邮件模板时既知其然、又知其所以然。为什么邮件模板需要一条编译管线邮件客户端对 HTML/CSS 的解析能力参差不齐Baserow 选择 MJML一个专为邮件设计的 XML 标记语言来编写邮件由 MJML 负责生成各邮件客户端兼容的 HTML。但 MJML 本身没有内置模板引擎无法处理「公共布局 局部内容」的复用结构同时 Django 模板引擎在运行时才求值不可能指望线上服务在发邮件时再去跑一次 MJML 命令。基础布局文件 base.layout.eta 的头部注释完整记录了这一设计取舍不能直接用 Django 模板Django 模板在运行时求值而运行时并不希望也不能执行mjml命令不能先用 MJML 处理含有 MJML 片段的 Django 模板MJML 无法解析/处理嵌套在 Django{% block %}/{% extends %}标签里的 MJML 片段因此引入Eta作为第一个构建期模板步骤先用 Eta 把各邮件内容「套」进公共布局拼装出完整合法的 MJML 文件再由 MJML 编译为 HTML 版 Django 模板。最终邮件 HTML 在构建期生成运行时只依赖 Django 渲染普通 HTML 模板无需在部署环境里携带和运行 MJML。项目结构与依赖backend/email_compiler目录是一个独立的 Node 子项目package.json 中的依赖与职责一一对应依赖用途mjml(^4.15.0)将 MJML 编译为邮件 HTML是管线的第二级eta(^2.0.0)第一级模板引擎负责把.mjml.eta套入*.layout.eta布局chokidar(^3.5.3)watch 模式下监听模板文件变更并触发重新编译glob(^9.0.0)按**/*.mjml.eta等模式批量查找待编译文件chalk(^5.0.0)彩色控制台输出源码中目前直接使用了 ANSI 转义序列scripts定义了两个入口均指向同一个主脚本 baserowEmailCompiler.jsyarn run watch→node ./baserowEmailCompiler.js watch持续监听并增量编译yarn run compile→node ./baserowEmailCompiler.js执行一次全量编译后退出。运行方式按照 README 的说明有两种使用途径Docker 开发环境自动执行just dc-dev up -d启动开发环境时会自动以 watch 模式启动编译器无需手动干预。这一点在 docker-compose.dev.yml 中有对应的mjml-email-compiler服务定义mjml-email-compiler: image: baserow_web-frontend:dev command: bash -c cd /baserow/backend/email_compiler/ yarn install yarn run watch volumes: - ./backend:/baserow/backend # ...该服务复用 web-frontend 的开发镜像把仓库的backend目录挂载进容器后运行yarn run watch实现宿主机上模板文件的实时编译。手动编译进入backend/email_compiler目录依次执行yarn install yarn run watch # 持续监听模式 yarn run compile # 编译一次后退出另外源码 支持通过环境变量MJML_FILE_SEARCH_ROOT覆盖默认的模板搜索根目录const BASEROW_BACKEND_SRC_DIR path.join(__dirname, .., src) const MJML_FILE_SEARCH_ROOT process.env.MJML_FILE_SEARCH_ROOT ? process.env.MJML_FILE_SEARCH_ROOT : BASEROW_BACKEND_SRC_DIR const MJML_ETA_FILE_GLOB path.join(MJML_FILE_SEARCH_ROOT, **, *.mjml.eta) const ETA_LAYOUT_FILE_GLOB path.join(MJML_FILE_SEARCH_ROOT, **, *.layout.eta)默认情况下搜索根是backend/src即 Django 模板所在位置两个 glob 分别匹配全部邮件源文件**/*.mjml.eta与布局文件**/*.layout.eta。从源码结构看MJML_FILE_SEARCH_ROOT允许你在不改代码的前提下把编译器指向其他目录例如为插件开发单独组织模板。编译流程源码解析单文件编译两级渲染核心函数compileEtaAndMjml(mjmlEtaFile)baserowEmailCompiler.js#L27-L51实现了单文件的两级编译function compileEtaAndMjml(mjmlEtaFile) { // 1. 用 Eta 渲染源文件得到纯 MJML 文本 Eta.configure({ // views 设为源文件所在目录使 layout(...) 可按相对路径解析布局 views: path.dirname(mjmlEtaFile), }) const tmplText fs.readFileSync(mjmlEtaFile, utf8) const mjmlText Eta.render(tmplText, {}) // 2. 用 MJML 编译为邮件 HTML const html mjml2html(mjmlText, { validationLevel: strict, // 严格校验MJML 书写错误直接报错 beautify: true, // 输出缩进美化的 HTML }).html // 3. 原位写出与源文件同名的 .html 文件 const targetHtmlFile mjmlEtaFile.replace(.mjml.eta, .html) fs.writeFileSync(targetHtmlFile, html) }三个关键点值得注意Eta.configure({ views: ... })Eta 的layout(path)指令依赖 views 目录做相对解析这里把它设为源文件自身所在目录因此每个.mjml.eta都可以用相对自己目录的路径如../base.layout.eta引用布局validationLevel: strictMJML 处于严格校验模式非法标签/属性会在编译期暴露而不是产出残缺的邮件 HTML输出路径规则xxx.mjml.eta→ 同目录xxx.html也就是说编译产物直接落在 Django 模板树内Django 侧按普通模板名引用即可。watch 模式增量与联动重编译main(args)baserowEmailCompiler.js#L72-L90根据命令行第一个参数是否为watch决定运行形态两种模式都会对MJML_ETA_FILE_GLOB建立 chokidar 监听并处理add事件新文件加入即编译watch 模式额外注册两类回调.mjml.eta的change事件 → 只重新编译被修改的那个邮件模板.layout.eta的change事件 → 触发recompileAllEtaAndMjmlFilesAfterLayoutFileChanges全量重新编译所有.mjml.eta文件因为布局是全体邮件共享的任何改动都影响全部产物。watch 模式用{ persistent: watchMode }配置 chokidar保证一次性compile模式下监听器不会阻止进程退出。模板实例布局与邮件如何协作Baserow 当前的全部邮件源文件集中在 backend/src/baserow/core/templates/baserow/ 下包括公共布局 base.layout.eta 以及 9 个业务邮件模板如 notifications_summary.mjml.eta、workspace_invitation.mjml.eta、reset_password.mjml.eta、account_deleted.mjml.eta等对应编译产物为同名的.html文件。布局文件base.layout.eta的职责顶部 Eta 注释块解释了为何采用「Eta 先行」的两级方案见上文定义mj-head中的全局样式title、text、button、notification-title、notification-description、mb-20/mt-20等mj-class所有邮件共享同一套 Inter 字体与配色通过mj-raw positionfile-start{% load i18n %}/mj-raw在编译产物开头注入 Django 的 i18n 加载指令使所有邮件模板天然支持多语言页头展示{{ logo_url }}与{{ logo_additional_text }}Django 运行时变量由后端注入便于自托管实例自定义品牌占位符%~ it.body %是 Eta 布局机制的插入点各邮件模板的内容会被渲染到这里。邮件模板以 notifications_summary.mjml.eta 为例的典型写法% layout(../base.layout.eta) % mj-section mj-column mj-text mj-classtitle mb-20 {% blocktrans trimmed count counternew_notifications_count %} You have {{ counter }} new notification {% plural %} You have {{ counter }} new notifications {% endblocktrans %} /mj-text mj-raw!-- htmlmin:ignore --{% for notification in notifications %}!-- htmlmin:ignore --/mj-raw ...其中有两个容易踩坑的要点Django 标签不能裸写MJML 编译阶段要求输入是「合法 MJML」因此所有 Django 控制结构{% for %}、{% if %}、{% blocktrans %}等都被包进mj-raw标签原样透传同时借助!-- htmlmin:ignore --注释防止 HTML 压缩步骤把 Django 标签吃掉MJML 组件与 Django 变量共存{{ notification.url }}、{{ notification.title }}这类 Django 变量会被 MJML 当作普通文本保留最终产物是「含 Django 占位符的 HTML」由 Django 在真正发邮件时求值。编译后的这些模板由 Django 侧消费例如通知汇总邮件的 Celery 任务位于 backend/src/baserow/core/notifications/tasks.pysend_daily_and_weekly_notifications_summary_by_email等账号类邮件则由 backend/src/baserow/core/emails.py 统一封装发送——Django 代码视角看到的只是一份普通 HTML 模板完全无感知 MJML 的存在。小结与实操要点在 Baserow 中新增一封邮件的正确姿势是在backend/src/baserow/core/templates/baserow/core/下新建xxx.mjml.eta首行用% layout(../base.layout.eta) %套入公共布局Docker 开发环境下的 watch 进程会自动将其编译为xxx.htmlDjango 端即可按模板名引用。生产构建同理在backend/email_compiler下执行yarn install yarn run compile。管线的关键约束MJML 内容必须完整合法编译器使用 strict 校验Django 标签必须放入mj-raw布局文件改动会触发全量重编译。若需调整模板搜索范围设置MJML_FILE_SEARCH_ROOT环境变量即可默认值为backend/src。以上路径均可在当前仓库中直接查看编译器源码见 backend/email_compiler/baserowEmailCompiler.js依赖声明见 backend/email_compiler/package.json开发环境集成见 docker-compose.dev.yml。【免费下载链接】baserowBuild databases, automations, apps agents with AI — no code. Open source platform available on cloud and self-hosted. GDPR, HIPAA, SOC 2 compliant. Best Airtable alternative.项目地址: https://gitcode.com/GitHub_Trending/ba/baserow创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考