当构建接受用户生成的内容的功能时,你可能需要根据用户指定的内容来显示动态内容。想象一下,当用户邀请某人加入他们的账户时,你想让用户能够定制从你的应用程序中发送的欢迎信息。
Rails程序员非常熟悉用动态文本片断编写内容:我们在编写视图模板时一直在这样做。但我们不想让用户编写ERB或HAML字符串,并在我们的应用中执行它们。这既是一个巨大的安全风险,也对用户来说,不得不学习完整的编程语言来改变一些文字,这并不太友好。
另一种方法是使用一些 "神奇的字符串",你可以把特殊字符串的值换成$NAME 或*|EMAIL|* 。这些有时被称为 "合并标签"。但是实施这种方法通常会导致gsub 、正则表达式和奇怪的边缘案例的汤。
建立这个功能的一个伟大的选择是使用Liquid模板。Liquid是一种非常精简的模板语言,Shopify最常使用它,允许店主定制他们的电子商务商店。
Liquid模板解决了所有这些问题。
它有安全性,因为你可以控制渲染时使用的上下文--这是一种华丽的说法,Liquid模板只能访问你明确传入的数据。语法是最小的,而且它为常见的操作(默认值、大写字母、日期格式等)提供了内置函数。而且该库对输入进行解析,不依赖脆弱的正则表达式--而不是随机破坏,你可以捕捉到无效的语法并进行适当处理。
使用方法
将liquid gem添加到你的项目中。
基本操作是两个步骤。
# Create a template from user-input
template = Liquid::Template.parse("Hi {{ customer.name }}!")
# Render the template with the dynamic data
template.render({"customer": {"name": "Matt"}})
#=> "Hi Matt!"
在实践中,你会想在自己的Rails视图助手中加入一些便利的功能。
- Liquid要求动态数据哈希有字符串键,而Rails应用程序通常使用符号键来表示哈希。你可以在哈希上调用Rails的
deep_stringify_keys方法来转换它们。 - 调用
render!,而不是render,会引发一个异常,这样你就可以退回到返回原始用户输入。 - Liquid提供了
strict_variables和strict_filters选项,可以将未定义的变量或过滤器变成错误。你可能希望这两个选项都是真的,这样用户就可以找出语法错误,而不是让内容默默地变成空白。
在我的项目中,我们添加了这个辅助方法。
# app/helpers/liquid_helper.rb
module LiquidHelper
def liquid(text, context: {})
template = Liquid::Template.parse(text)
template.render!(context.deep_stringify_keys, {
strict_variables: true,
strict_filters: true
})
rescue Liquid::Error
text.to_s
end
end
然后在你的Rails应用程序的任何视图(或邮件)中,你可以调用liquid 帮助器来显示用户生成的动态内容。
@campaign = Campaign.create!(subject: "Welcome {{ customer.name }}!")
<%= liquid(@campaign.subject, context: { customer: { name: @customer.name } }) %>
注意:如果你通过ActionText 使用富文本,你需要在Liquid插值后调用html_safe ,因为输出是原始HTML。
<%= liquid(@campaign.message, context: { customer: { name: @customer.name } }).html_safe %>
这个助手通过返回原始输入来优雅地处理错误情况。
liquid("Hi {{ missing_value }}", context: {})
#=> "Hi \{\{ missing_value }}"
liquid("Hi {{ foo", context: {})
#=> "Hi \{\{ foo"
如果你在Liquid渲染上下文中使用这些模型(而不是每次都建立上下文哈希),你可能也想给你的模型添加便利方法。
class Customer < ApplicationRecord
belongs_to :organization
def to_liquid
# Expose whatever fields you want to be able to use in liquid templates
{
name: name,
email: email,
company_name: organization.name
}
end
end
liquid("New sign up from {{ customer.company_name}}. Say hi to {{ customer.name }} <{{ customer.email }}>!", context: customer.to_liquid)
#=> New sign up from Arrows. Say hi to Matt <matt@arrows.to>!
甚至更高级的经过检验的功能
如果你想为用户提供特定的应用功能,如{{ customer | avatar_url }} 或{{ task.due_date | next_business_day }} 或{{ '#7ab55c' | color_to_rgb }} ,你可以注册你自己的过滤器。
你可以指定resource_limits ,以避免极其缓慢的插值。
这些都超出了本提示的范围,你可以自己去探索。