首页 编程 软件学院 查看内容

如何用不到 30 行代码写一个模板引擎?

2015-6-19 10:37 |来自: 伯乐在线 1709 0

摘要: 如何用不到30行代码写一个模板引擎?模板引擎从内部来看真的很简单 注意:本文以模板库 mote 为基础,其简洁性给了我启发,对于没有了解过模板引擎内部机制的人来说,它是很好的研究材料。 前言:什么是模板? 模 ...
关键词: 模板 output render index func 一个 引擎 字符串 html 代码

如何用不到30行代码写一个模板引擎?模板引擎从内部来看真的很简单

注意:本文以模板库 mote 为基础,其简洁性给了我启发,对于没有了解过模板引擎内部机制的人来说,它是很好的研究材料。

前言:什么是模板?

模板引擎是从模板生成文本(字符串)并且帮助分离表示层和业务逻辑的工具。

除非你已经被遗留软件的代码缠住了(或者没有开发过有 UI 的软件),否则你可能已经用过一个以上的模板引擎了。

但它们究竟是怎么工作的?你怎么创建一个?快速浏览一些主要的模板库,会发现它们的代码没有几千行也有几百行。即使是名副其实的“ slim ”也不是如此“苗条”。

所以你可能认为模板引擎是个难题,但我想要把问题一步步分解,并且给你展示你也可以通过几行代码来打造自己的模板引擎。

好了,让我们深入下去…

定义特性

在这篇文章中,模板引擎将会有两条规则:

1.以%开头的代码行都认为是ruby代码。

2.在任意一行的{{ … }}符号中插入ruby代码。我们可以将其用于像{{article.title}}这样的语句。

就这样?就这两条规则?没错–记住第一条规则让我们可以使用ruby的所有功能。这意味着你的通常的模板特性(循环,调用高阶函数,嵌入局部模板)都是可用的。它们甚至还带来福利:你不需要学习一门新的模板语言或者某领域的特定语言,因为你已经知道ruby了。

你可以像这样调用另一个模板:

% render("path/to/template.template", {})

可以添加注释:

% # this is a comment

可以执行语句块:

% 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。

一个就像index.template的渲染函数

在一个不存在模板引擎的世界里,通过如下的纯ruby代码,你可以实现我们希望通过index.template来实现的同样的逻辑。

def render_index(access, data)
output = "" #a new string to hold our output in 
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(注:Interactive Ruby Shell)并得到如下结果:

>> 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 = "" # new empty string to store the string we're using to build our function in
  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中运行。这个方法被调用时将展示从模板而来的预期行为。

这样逐行翻译的过程将是我们的解析函数的根基,现在我们来看看这个过程。

解析函数

1.使用Proc

不像define_render_index(),我们不以一个命名函数开始我们的func字符串。–取而代之的我们将使用Proc,然后将其存入一个变量并且在需要的时候调用它。

2.给Proc设置变量

define_render_index()也硬编码了它的变量:access和data。但我们需要给解析函数传递这些变量的名字,以便它构建合适的Proc定义字符串。

在这里我们将变量名作为字符串传递给解析函数,就像

parse(template, "access, data")

3.把模板逐行翻译为函数字符串

研究上面的define_render_index(),告诉我们为了创建一个可以求值的ruby方法所需要对模板逐行应用的所有规则。它们是这样的:

任何情况下双引号必须转义,每行内容被双引号包围并且每行结尾需要加上“n”行结束符。

如果行首字符(不是空格符)为%,那么删除%。

before:
  % if data.empty? 
after:
  "if data.empty?\n"

其余行前面加上 “output <<”。

before:
  <html>
after:
  "output << \"<html>\" \n"

4. 将{{ … }}转换为#{ … }

我们将以正则表达式来做这个

before:
  <li></li> 
after:
  "output << \"<li> \#{ i } </li>\" \n"

为了执行上述规则,我们用.split(“n”) 将原始模板文件分割为数组,每个数组元素都是用字符串表示的模板中的一行。然后遍历得到的数组,构建我们将求值的字符串func。

将这些放在一起就是一个解析函数:

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非常快。

我还要强调一个重要的问题关于简洁–虽然mote非常小,但对于它被设计来解决的问题,它是集中且完整的解决方案。

我希望这篇文章能给那些从没看过模板引擎底层或是考虑去构建一个的人增长些知识。如果你想要评论或者反馈,请告诉我。

声明:文章版权归原作者所有 部分文章转自互联网 如有侵权请联系 [邮箱地址] 删除

路过

雷人

握手

鲜花

鸡蛋

最新评论

返回顶部