庆祝「翻译」节点诞生,发一篇译文。
原文: How to write a template engine in less than 30 lines of code
声明:本文基于模板引擎库 mote。其简洁的代码赋予了我灵感,如果你之前没有探索过模板引擎的内部实现,相信这会是一个绝佳的学习范例。
模板引擎是一种使用模板来生成文本(字符串)的工具,它还有助于将呈现逻辑与应用逻辑分离。
除非你正挣扎于某些历史悠久的软件代码中(或者你开发的是没有用户界面的软件),否则你极有可能已经接触过模板引擎了。
但是你有没有思考过模板引擎的工作原理呢?你尝试过自己构建一个吗?如果我们瞥一眼主流的模板引擎的代码库,你会看到有几百行 (erb)的代码,甚至几千行 (erubis)。即使是名叫 slim 的家伙也并没有那么苗条。
所以你或许会觉得处理模板是一个相当复杂的问题,然而下面我会一步步地向你展示其实你可以只用少量代码就构建出一个模板引擎。
那让我们开始吧。
本文我们将要实现的模板引擎只有两条规则:
{{ article.title }}
。完了?就两条规则?没错,记住,第一条规则让我们可以访问 Ruby 的一切,也就是说所有常见的模板特性(比如循环、调用上层函数、嵌入 partial 等)都可以靠这条规则来实现。如此简单的特性甚至还带来了一个额外的好处:你不需要去学一个新的模板语言或者 DSL,因为你已经会 Ruby 了。
你可以这样来调用其他模板:
% render("path/to/template.template", {})
也可以写注释:
% # this is a comment
或者执行 blocks:
% 3.times do |i|
{{i}}
% end
根据上面的特性,我们可以来编写一个示例模板:
<html>
<body>
% if access == 0
<div> no access :( </div>
% else
<ul>
% data.each do |i|
<li>{{i}}</li>
% end
</ul>
% end
% # comments are just normal ruby comments
</body>
</html>
下面我们称这个模板为 index.template
。
下面我们要做的就是怎样编写一个方法来解析这个模板并输出正确的字符串。在此之前,我们先来看一个中间步骤:如何使用纯粹的 Ruby 代码来输出 HTML。
在模板引擎出现以前,你同样可以使用纯 Ruby 来达到与 index.template
一样的效果,就像下面的代码:
def render_index(access, data)
output = "" # 一个新的字符串,用来存放输出的内容
output << "<html>"
output << "<body>"
if access == 0
output << "<div> no access :( </div>"
else
output << "<ul>"
data.each do |i|
output << "<li>#{ i }</li>"
end
output << "</ul>"
end
output << "</body>"
output << "</html>"
return output
end
把它粘贴到 IRB 中试试:
>> render_index(0,["foo", "bar"])
=> "<html><body><div> no access :( </div></body></html>"
>> render_index(1,["foo", "bar"])
=> "<html><body><ul><li>foo</li><li>bar</li></ul></body></html>"
假如你的应用程序涉及的范围非常有限,或许你就根本用不着模板引擎,这样就大功告成了!你可以根据需要自己编写 render_index()
、render_header()
、render_footer()
方法。PHP 本身其实就是一个模板引擎,这也解释了为什么 PHP 社区的人们会经常这么做。
然而我们来看 render_index()
的目的是为了探寻如何将 index.template
转换成 render_index()
,并将转换的方法共通化,这样我们的模板引擎就出来了。我们并不想真的去编写 render_index()
、render_header()
、render_footer()
方法,也不想用代码生成器来实现。我们想要的是能够动态生成一个行为类似 render_index()
的方法,而不是亲手来写 render_index()
方法的代码。
在看具体的实现方式之前,我们再来看一个中间步骤:
def define_render_index()
func = "" # 空字符串,用来存储构建方法的字符串
func << "def render_index(access, data) \n"
func << "output = \"\" \n"
func << "output << \"<html>\" \n"
func << "output << \"<body>\" \n"
func << "if access == 0\n"
func << " output << \"<div> no access :( </div>\" \n"
func << "else\n"
func << " output << \"<ul>\" \n"
func << " data.each do |i|\n"
func << " output << \"<li> \#{ i } </li>\" \n"
func << " end \n"
func << " output << \"</ul>\" \n"
func << "end\n"
func << " output << \"</body>\" \n"
func << " output << \"</html>\" \n"
func << " return output \n"
func << "end\n"
eval(func)
end
把它粘贴到 IRB 中并调用:
>> define_render_index()
=> nil
>> render_index(1, ["foo", "bar"])
=> "<html><body><ul><li>foo</li><li>bar</li></ul></body></html>"
现在我们就得到一个更为完整的蓝图了:我们可以将原始的模板逐行转换为一系列字符串,然后修改这些字符串使其能够被 Ruby 解释。生成了这个方法以后,在调用它时就会执行我们所期望的模板的行为。
上面这个逐行转换的过程就是我们的 parse
方法的根基,下面就让我们具体来看。
我们不想让 func
字符串像 define_render_index()
一样以一个具名函数开头,所以我们用 Proc 并将其保存至一个变量,有需要时就 .call
它。
define_render_index()
还写死了其中的变量:access 和 data。但我们需要将变量名传给 parse
函数,这样才能构建定义 Proc 的字符串。这里我们把变量名直接以字符串形式传递给 parse 方法,就像这样:
parse(template, "access, data")
上面的 define_render_index()
已经告诉我们逐行处理模板来创建一个新的 Ruby 方法时所需遵循的规则了。这些规则就是:
所有的双引号必须被转义,每一行的内容用双引号围起来,并在每一行的最后添加换行符 "\n"。
如果行的第一个字符(不包括空格)是 %
,就把 %
删除。
变换前:
% if data.empty?
变换后:
"if data.empty?\n"
"output <<"
变换前:
<html>
变换后:
"output << \"<html>\" \n"
我们会使用正则表达式来实现这一点
变换前:
<li>{{ i }}</li>
变换后:
"output << \"<li> \#{ i } </li>\" \n"
要运用上述规则,我们首先用 .split("\n")
将原始模板文件分割成数组,这样数组中的每个元素就是模板文件的每一行了。然后循环这个数组来构建 func
字符串,最后将其 eval
。
我们最终得到的 parse 函数如下:
def parse(template,vars = "")
lines = File.read(template).split("\n")
func = "Proc.new do |#{vars}| \n output = \"\" \n "
lines.each do |line|
if line =~ /^\s*(%)(.*?)$/
func << " #{line.gsub(/^\s*%(.*?)$/, '\1') } \n"
else
func << " output << \" #{line.gsub(/\{\{([^\r\n\{]*)\}\}/, '#{\1}') }\" \n "
end
end
func << " output; end \n "
eval(func)
end
你可以在 IRB 中亲自尝试一番:
>> index = parse("index.template", "access, data")
=> nil
>> index.call(1,["Foo"])
=> " <html>\n <body>\n<ul>\n<li>foo</li> \n</ul>\n </body>\n </html>\n"
大功告成了!就这么几行代码,你就实现出一个颇为强劲的模板引擎了。由于没有涉及太多多余的功能,我们将复杂度降到了最低,也提高了模板语言的清晰程度。复杂度的降低能够让应用程序条例更清晰,更不容易出错,还能加快开发特性的速度。
上面的代码恐怕不行!但是 mote,也就是本文的灵感来源,当然可以。mote 附带了一些 helper 方法和缓存功能,我们已经成功将其运用在了各种大型的 Web 应用中。更不用提 mote 的速度相当快。
同时我还想就其简洁这个显著的特点说两句——虽然 mote 的体积极其小,但对于其要解决的问题给出了一个专注而又完整的解决之道。
希望这篇文章会对从未探寻过模板引擎的人或者想要做一个模板引擎的人有所帮助。欢迎留下你的评论或者反馈。