模板引擎是Web开发中通常用于动态生成网页的工具,例如PHP常用的Smarty、Python的Jinja、Node的Jade等。本文通过Python(Approach: Building a toy template engine in Python)和Js(JavaScript Micro-Templating)的两个简单模板引擎项目学习怎样写一个模板引擎。
一般模板由下面三部分组成:
- 文本
- 变量
- 组块
通常变量和代码组块由特定的分隔符标识,如:
1 2 3 4 |
Hello, {{name}}! {% if role == "admin" %} <a href="/dashboard">Dashboard</a> {% end %} |
对文本的渲染就是返回文本本身;变量和组块的渲染依赖于我们赋予变量名的值和约定的组块语法规则(如条件、循环等)。要将字符串当做变量进行求值,首先想到的是eval
方法:
1 2 3 4 |
name = "rainy" print("Hello, " + eval("name") + "!") # Hello, rainy! |
许多编程语言中的eval
方法用于将字符串转化成表达式进行求值,完成类似编译器本身的工作,而实质上模板引擎更像是一个针对于模板的编译器。我们知道编译器一般采用抽象语法树(AST)这种树形结构来对程序源码进行表征,如果我们将模板看作是源码,同样可以将其表征为抽象语法树,例如上面的模板文件可以表示为:
要将模板文件变成上图所示的AST结构,首先需要按照分隔符划分,例如在Python中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import re VAR_TOKEN_START = '{{' VAR_TOKEN_END = '}}' BLOCK_TOKEN_START = '{%' BLOCK_TOKEN_END = '%}' TOK_REGEX = re.compile(r"(%s.*?%s|%s.*?%s)" % ( VAR_TOKEN_START, VAR_TOKEN_END, BLOCK_TOKEN_START, BLOCK_TOKEN_END )) content = """Hello, {{name}}! {% if role == "admin" %} <a href="/dashboard">Dashboard</a> {% end %}""" TOK_REGEX.split(content) # OUTPUT => ['Hello, ', '{{name}}', '\n', '{% if role == "admin" %}', '\n<a href="/dashboard">Dashboard</a>\n', '{% end %}', ''] |
构建成AST之后对每一节点逐一进行渲染(render),例如对变量的渲染可以用下面的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def resolve(name, context): for tok in name.split('.'): context = context[tok] return context class VarTmpl(): def __init__(self, var): self.var = var def render(self, **kwargs): return resolve(self.var, kwargs) tmpl = VarTmpl("name") tmpl.render(name = "rainy") #=> rainy tmpl.render(name = "python") #=> python |
对组块的渲染稍微复杂一些但原理上类似于eval
:
1 2 3 4 |
role = 'user' eval('role == "admin"') # OUTPUT False |
只不过所有组块的语法和求值规则需要重新定义,有兴趣可以查看源码。下面再来看基于Js的一种解决方案。
从上文可以看出,模板引擎的核心在于区分字符串和表达式,而表达式本身又是以字符串的形式呈现。为了实现字符串与表达式之间的切换,上面Python的版本采用eval
(或者更专业点的:ast.literal_eval)。当然Js中也有与之类似的eval
方法,但Js还有另外一个非常灵活的特性,在定义一个函数时,可以用下面两种方式:
一般模板由下面三部分组成:
- 文本
- 变量
- 组块
通常变量和代码组块由特定的分隔符标识,如:
1 2 3 4 |
Hello, {{name}}! {% if role == "admin" %} <a href="/dashboard">Dashboard</a> {% end %} |
对文本的渲染就是返回文本本身;变量和组块的渲染依赖于我们赋予变量名的值和约定的组块语法规则(如条件、循环等)。要将字符串当做变量进行求值,首先想到的是eval
方法:
1 2 3 4 |
name = "rainy" print("Hello, " + eval("name") + "!") # Hello, rainy! |
许多编程语言中的eval
方法用于将字符串转化成表达式进行求值,完成类似编译器本身的工作,而实质上模板引擎更像是一个针对于模板的编译器。我们知道编译器一般采用抽象语法树(AST)这种树形结构来对程序源码进行表征,如果我们将模板看作是源码,同样可以将其表征为抽象语法树,例如上面的模板文件可以表示为:
要将模板文件变成上图所示的AST结构,首先需要按照分隔符划分,例如在Python中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import re VAR_TOKEN_START = '{{' VAR_TOKEN_END = '}}' BLOCK_TOKEN_START = '{%' BLOCK_TOKEN_END = '%}' TOK_REGEX = re.compile(r"(%s.*?%s|%s.*?%s)" % ( VAR_TOKEN_START, VAR_TOKEN_END, BLOCK_TOKEN_START, BLOCK_TOKEN_END )) content = """Hello, {{name}}! {% if role == "admin" %} <a href="/dashboard">Dashboard</a> {% end %}""" TOK_REGEX.split(content) # OUTPUT => ['Hello, ', '{{name}}', '\n', '{% if role == "admin" %}', '\n<a href="/dashboard">Dashboard</a>\n', '{% end %}', ''] |
构建成AST之后对每一节点逐一进行渲染(render),例如对变量的渲染可以用下面的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def resolve(name, context): for tok in name.split('.'): context = context[tok] return context class VarTmpl(): def __init__(self, var): self.var = var def render(self, **kwargs): return resolve(self.var, kwargs) tmpl = VarTmpl("name") tmpl.render(name = "rainy") #=> rainy tmpl.render(name = "python") #=> python |
对组块的渲染稍微复杂一些但原理上类似于eval
:
1 2 3 4 |
role = 'user' eval('role == "admin"') # OUTPUT False |
只不过所有组块的语法和求值规则需要重新定义,有兴趣可以查看源码。下面再来看基于Js的一种解决方案。
从上文可以看出,模板引擎的核心在于区分字符串和表达式,而表达式本身又是以字符串的形式呈现。为了实现字符串与表达式之间的切换,上面Python的版本采用eval
(或者更专业点的:ast.literal_eval)。当然Js中也有与之类似的eval