Phlex 为 Rails 邮件服务:无需 ERB 的 Action Mailer
使用 Phlex 组件和布局渲染 Action Mailer 邮件:简洁、可组合、纯 Ruby 实现
发布日期:2025年2月27日
更新日期:2025年3月2日
整天写 Ruby 很有趣,但要在 HTML 中嵌入 Ruby 就不那么令人愉快了。在过去几个月里,我发现频繁在 Ruby 和 ERB 视图及部分代码之间切换,极大地打断了我的工作流,甚至让人越来越感到厌烦。
这时我发现了 Phlex(了解更多),这是一款允许你在纯 Ruby 环境下编写 HTML(组件、视图和布局)的 Ruby 库。我已经在多个 Rails 应用中完全用 Phlex 取代了 ERB,因此我想尝试将其应用于 Action Mailer。再见,ERB!
为什么这个方法会如此吸引人呢?除了单纯为了尝试而尝试之外,还有以下几个优势:
- 告别 ERB:无论是邮件还是普通页面,都可以全程使用 Phlex。
- 集中管理 HTML 和文本版邮件内容:在一个地方同时管理邮件的 HTML 和纯文本版本。
- 使用 Phlex 组件构建布局和视图:让代码更加模块化和易于维护。
对于我个人而言,在 Rails 应用中彻底摆脱 ERB 是一件值得庆祝的事情。毕竟,如果某个工具不能带来“喜悦”(参考 Marie Kondo 的整理法则),那我们就应该果断舍弃它。
完整代码可以在我的 GitHub 仓库中找到,希望你也能像我一样觉得这个想法充满吸引力!
项目源码链接: visini/rails-phlex-action-mailer
接下来,我们将从一个基础的 Rails 应用入手,逐步展示如何将所有东西迁移到 Phlex。
ERB 方式的传统方法
UserMailer 类是 Action Mailer 的入口点,负责处理 Rails 应用中的邮件发送功能。下面是一个典型的实现示例:
class UserMailer < ApplicationMailer
def welcome
@user = params[:user]
mail(to: @user.email, subject: "欢迎")
end
end
我们还需要为该邮件提供对应的视图文件或“邮件模板”。由于并非所有邮件客户端都支持 HTML,我们需要分别提供 HTML 和纯文本版本。通过 ERB 模板,我们可以嵌入 Ruby 代码以访问实例变量或调用辅助方法。
HTML 版本模板:
<div>我们很高兴您加入我们!感谢您的注册。</div>
<%= link_to '点击这里登录', 'https://example.com' %>
纯文本版本模板:
我们很高兴您加入我们!感谢您的注册。
点击这里登录:https://example.com
此外,我们还需要定义一些基本的布局。布局中可以添加诸如电子邮件顶部的 logo、底部的链接、联系信息等内容。通过这种方式,修改布局变得非常方便——只需在一处进行更改即可。
HTML 布局模板:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<body>
<div><b>应用名称</b></div>
<%= yield %>
<div>此致</div>
<div>公司名称</div>
</body>
</html>
纯文本布局模板:
应用名称
<%= yield %>
此致
公司名称
上述模板结合它们各自的布局后,生成的邮件内容如下:
HTML 版本:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<body>
<div><b>应用名称</b></div>
<div>我们很高兴您加入我们!感谢您的注册。</div>
<a href="https://example.com">点击这里登录</a>
<div>此致</div>
<div>公司名称</div>
</body>
</html>
纯文本版本:
应用名称
我们很高兴您加入我们!感谢您的注册。
点击这里登录:https://example.com
此致
公司名称
可以看到,最终生成的邮件内容包含了布局的内容包裹着主体内容。
为了测试这些模板的正确性,我们可以编写单元测试,并通过 fixture 文件来验证 HTML 和纯文本版本的邮件内容是否符合预期。
使用 Phlex 构建邮件模板
首先,按照 Phlex 安装指南 在 Rails 应用中安装 Phlex。接下来,我们可以为邮件模板创建 Phlex 视图。以下是 HTML 和纯文本两个版本的实现。
HTML 版本:
class Views::Mailers::Users::Welcome::Html < Views::Base
def initialize(user)
@user = user
end
def view_template
div do
plain "我们很高兴您加入我们!感谢您的注册。"
end
a href: "https://example.com" do
plain "点击这里登录"
end
end
end
纯文本版本:
class Views::Mailers::Users::Welcome::Text < Views::Base
def initialize(user)
@user = user
end
def view_template
plain "我们很高兴您加入我们!感谢您的注册。"
plain "\n\n"
plain "点击这里登录:https://example.com"
end
end
接着,我们在原来的 ERB 模板中渲染这些 Phlex 视图:
HTML 版本:
<%= render Views::Mailers::Users::Welcome::Html.new(user: @user) %>
纯文本版本:
<%= render Views::Mailers::Users::Welcome::Text.new(user: @user) %>
此时,虽然我们已经通过 Phlex 渲染了视图,但仍然需要保留 ERB 文件作为“入口点”。这看起来有些多余,因为文件数量从原本的两份增加到了四份,增加了复杂性。不过别急,让我们继续改进。
为邮件模板设置 Phlex 布局
类似于 ERB 布局,我们也可以为邮件模板创建 Phlex 布局。首先,我们需要告诉 Action Mailer 不要使用任何默认布局,因为我们将在 Phlex 中手动组装模板和布局。
class ApplicationMailer < ActionMailer::Base
default from: "from@example.com"
layout false
end
删除原有的 ERB 布局文件后,我们开始为 HTML 和纯文本版本创建 Phlex 布局。
HTML 布局:
class Components::Layouts::Mailers::Html < Phlex::HTML
def view_template(&block)
doctype
html do
head do
meta charset: "utf-8"
end
body do
div do
b { "应用名称" }
end
yield block
div { "此致" }
div { "公司名称" }
end
end
end
end
纯文本布局:
class Components::Layouts::Mailers::Text < Phlex::HTML
def view_template(&block)
plain "应用名称"
plain "\n\n"
yield block
plain "\n\n\n"
plain "此致"
plain "\n"
plain "公司名称"
end
end
在视图中,我们可以利用 around_template 钩子指定具体的布局:
HTML 版本:
class Views::Mailers::Users::Welcome::Html < Views::Base
include Components
def around_template
render Components::Layouts::Mailers::Html.new do
super
end
end
def view_template
div do
plain "我们很高兴您加入我们!感谢您的注册。"
end
a href: "https://example.com" do
plain "点击这里登录"
end
end
end
纯文本版本:
class Views::Mailers::Users::Welcome::Text < Views::Base
include Components
def around_template
render Components::Layouts::Mailers::Text.new do
super
end
end
def view_template
plain "我们很高兴您加入我们!感谢您的注册。"
plain "\n\n"
plain "点击这里登录:https://example.com"
end
end
现在,邮件内容已经成功地通过 Phlex 布局进行了包裹渲染。但每次新增邮件模板时,都需要重复大量相似的代码(例如定义 around_template 钩子等)。为了让代码更加简洁,我们需要进一步重构。
重构 ApplicationMailer 和视图
最后,我们对 ApplicationMailer 进行重构,引入一个自定义方法 render_email 来简化邮件内容的渲染过程。这样一来,我们不再需要在每个视图中定义 around_template,并且可以将 HTML 和纯文本视图合并到同一个类中,从而避免重复劳动。
改进后的 ApplicationMailer 类:
class ApplicationMailer < ActionMailer::Base
default from: "from@example.com"
layout false
def render_email(view_class, subject:, to:, view_params: {})
mail({subject:, to:, content_type: "multipart/alternative"}) do |format|
format.html {
render Components::Layouts::Mailers::Html.new {
render view_class.const_get(:Html).new(**view_params)
}
}
format.text {
render Components::Layouts::Mailers::Text.new {
render view_class.const_get(:Text).new(**view_params)
}
}
end
end
end
在邮件器类中调用 render_email 方法:
class UserMailer < ApplicationMailer
def welcome
@user = params[:user]
render_email(
Views::Mailers::Users::Welcome,
subject: "欢迎",
to: @user.email,
view_params: {user: @user}
)
end
end
合并后的视图类:
class Views::Mailers::Users::Welcome < Views::Base
def initialize(user:)
@user = user
end
class Html < self
def view_template
div do
plain "我们很高兴您加入我们!感谢您的注册。"
end
a href: "https://example.com" do
plain "点击这里登录"
end
end
end
class Text < self
def view_template
plain "我们很高兴您加入我们!感谢您的注册。"
plain "\n\n"
plain "点击这里登录:https://example.com"
end
end
end
完成上述步骤后,我们可以删除以下四个文件:
app/views/mailers/users/welcome/html.rbapp/views/mailers/users/welcome/text.rbapp/views/user_mailer/welcome.html.erbapp/views/user_mailer/welcome.text.erb
至此,整个应用中已经没有任何 ERB 文件存在,邮件模板完全由 Phlex 和纯 Ruby 构建而成。
添加内联样式
为了让邮件更加美观,我们需要为内容添加样式。然而,大多数邮件客户端仅支持内联样式,因此我们需要通过工具(如 roadie-rails)将样式自动嵌入到 HTML 元素中。
在布局中定义样式的初始方案如下:
class Components::Layouts::Mailers::Html < Phlex::HTML
def view_template(&block)
doctype
html do
head do
meta charset: "utf-8"
style do
plain <<~CSS
body {
font-family: Arial, sans-serif;
}
.highlight {
background-color: yellow;
}
.signature {
font-style: italic;
}
CSS
end
end
body do
div do
b { "应用名称" }
end
yield block
div { "此致" }
div(class: "signature") { "公司名称" }
end
end
end
end
启用 roadie-rails 后,所有的样式都会自动内联到 HTML 元素中,确保兼容各种邮件客户端。
总结
通过用 Phlex 替代 ERB,我们成功实现了使用纯 Ruby 编写邮件模板的目标。不仅可以统一管理 HTML 和纯文本版本的邮件内容,还能充分利用 Phlex 组件的强大功能来设计布局和视图。
完整的项目代码已托管在 GitHub 上,供有兴趣的开发者参考:
