本文讲的是详解模板引擎工作机制,
我已经使用各种模版引擎很久了,现在终于有时间研究一下模版引擎到底是如何工作的了。
简介
简单的说,模版引擎是一种可以用来完成涉及大量文本数据的编程任务的工具。一般而言,我们经常在一个 web 应用中利用模板引擎来生成 HTML 。在 Python 中,当你想使用模板引擎的时候,你会发现你有不少的选择,比如jinja 或者是mako。从现在开始,我们将利用 tornado 中的模板引擎来讲解模板引擎的工作原理,在 tornado 中,自带的模板引擎相对的简单,能方便我们去深入的剖析其原理。
在我们研究(模版引擎)的实现原理之前,先让我们来看一个简单的接口调用例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
from tornado import template PAGE_HTML = """ <html> Hello, {{ username }}! <ul> {% for job in job_list %} <li>{{ job }}</li> {% end %} </ul> </html> """ t = template.Template(PAGE_HTML) print t.generate(username='John', job_list=['engineer']) |
这段代码里的 username
将会动态的生成,job
列表也是如此。你可以通过安装 tornado
并运行这段代码来看看最后的效果。
详解
如果你仔细观察 PAGE_HTML
,你会发现这段模板字符串由两个部分组成,一部分是固定的字符串,另一部分是将会动态生成的内容。我们将会用特殊的符号来标注动态生成的部分。在整个工作流程中,模板引擎需要正确输出固定的字符串,同时需要将正确的结果替换我们所标注的需要动态生成的字符串。
使用模板引擎最简单的方式就是像下面这样用一行 python 代码就可以解决:
1 |
deftemplate_engine(template_string, **context):# process herereturn result_string |
在整个工作过程中,模板引擎将会分为如下两个阶段对我们的字符串进行操作:
- 解析
- 渲染
在解析阶段,我们将我们准备好的字符串进行解析,然后格式化成可被渲染的格式,其可能是能被rendered.Consider
所解析的字符串,解析器可能是一个语言的解释器或是一个语言的编译器。如果解析器是一种解释器的话,在解析过程中将会生成一种特殊的数据结构来存放数据,然后渲染器会遍历整个数据结构来进行渲染。例如 Django 的模板引擎中的解析器就是一种基于解释器的工具。除此之外,解析器可能会生成一些可执行代码,渲染器将只会执行这些代码,然后生成对应的结果。在 Jinja2 , Mako,Tornado 中,模板引擎都在使用编译器来作为解析工具。
编译
如同上面所说的一样,我们需要解析我们所编写的模板字符串,然后 tornado 中的模板解析器将会将我们所编写的模板字符串编译成可执行的 Python 代码。我们的解析工具负责生成Python代码,而仅仅由单个Python函数构成:
1 2 3 |
def parse_template(template_string): # compilation return python_source_code |
在我们分析 parse_template
的代码之前,让我们先看个模板字符串的例子:
1 2 3 4 5 6 7 8 |
<html> Hello, { { username } }! <ul> { % for job in jobs % } <li>{ { job.name } }</li> { % end % } </ul> </html> |
模板引擎里的 parse_template
函数将会将上面这个字符串编译成 Python 源码,最简单的实现方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
def _execute(): _buffer = [] _buffer.append('\n<html>\n Hello, ') _tmp = username _buffer.append(str(_tmp)) _buffer.append('!\n <ul>\n ') for job in jobs: _buffer.append('\n <li>') _tmp = job.name _buffer.append(str(_tmp)) _buffer.append('</li>\n ') _buffer.append('\n </ul>\n</html>\n') return''.join(_buffer) |
现在我们在 _execute
函数里处理我们的模版。这个函数将可以使用全局命名空间里的所有有效变量。这个函数将创建一个包含多个 string 的列表并将他们合并后返回。显然找到一个局部变量比找一个全局变量要快多了。同时,我们对于其余代码的优化也在这个阶段完成,比如:
1 2 3 4 5 |
_buffer.append('hello') _append_buffer = _buffer.append # faster for repeated use _append_buffer('hello') |
在 { { ... } }
中的表达式将会被提取出来,然后添加进 string
列表中。在 tornado
模板模块中,在 { { ... } }
所编写的表达式没有任何的限制,if 和 for 代码块都可以准确地转换成为 Python代码。
让我们来看看具体的代码实现吧
让我们来看看模板引擎的具体实现吧。我们在 Template
类中编声明核心变量,当我们创建一个Template
对象后,我们便可以编译我们所编写的模板字符串,随后我们便可以根据编译的结果来对其进行渲染。我们只需要对我们所编写的模板字符串进行一次编译,然后我们可以缓存我们的编译结果,下面是 Template
类的简化版本的构造器:
1 2 3 4 |
class Template(object): def__init__(self, template_string): self.code = parse_template(template_string) self.compiled = compile(self.code, '<string>', 'exec') |
上段代码里的 compile
函数将会将字符串编译成为可执行代码,我们可以稍后调用 exec
函数来执行我们生成的代码。现在,让我们来看看 parse_template
函数的实现,首先,我们需要将我们所编写的模板字符串转化成一个个独立的节点,为我们后面生成 Python 代码做好准备。在这过程中,我们需要一个 _parse
函数,我们先把它放在一边,等下在回来看看这个函数。现,我们需要编写一些辅助函数来帮助我们从模板文件里读取数据。现在让我们来看看 _TemplateReader
这个类,它用于从我们自定义的模板中读取数据:
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 30 31 32 33 34 35 36 37 38 39 |
class _TemplateReader(object): def __init__(self, text): self.text = text self.pos = 0 def find(self, needle, start=0, end=None): pos = self.pos start += pos if end is None: index = self.text.find(needle, start) else: end += pos index = self.text.find(needle, start, end) if index != -1: index -= pos return index def consume(self, count=None): if count is None: count = len(self.text) - self.pos newpos = self.pos + count s = self.text[self.pos:newpos] self.pos = newpos return s def remaining(self): return len(self.text) - self.pos def __len__(self): return self.remaining() def __getitem__(self, key): if key < 0: return self.text[key] else: return self.text[self.pos + key] def __str__(self): return self.text[self.pos:] |
为了生成 Python 代码,我们需要去看看 _CodeWriter
这个类的源码,这个类可以编写代码行和管理缩进,同时它也是一个 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 |
class _CodeWriter(object): def __init__(self): self.buffer = cStringIO.StringIO() self._indent = 0 def indent(self): return self def indent_size(self): return self._indent def __enter__(self): self._indent += 1 return self def __exit__(self, *args): self._indent -= 1 def write_line(self, line, indent=None): if indent == None: indent = self._indent for i in xrange(indent): self.buffer.write(" ") print self.buffer, line def __str__(self): return self.buffer.getvalue() |
在 parse_template
函数里,我们先要创建一个 _TemplateReader
对象:
1 2 3 4 5 6 |
def parse_template(template_string): reader = _TemplateReader(template_string) file_node = _File(_parse(reader)) writer = _CodeWriter() file_node.generate(writer) return str(writer) |
然后,我们将我们所创建的 _TemplateReader
对象传入 _parse
函数中以便生成节点列表。这里生成的所有节点都是模板文件的子节点。接着,我们创建一个 _CodeWriter
对象,然后 file_node
对象会把生成的 Python 代码写入 _CodeWriter
对象中。然后我们返回一系列动态生成的 Python 代码。_Node
类将会用一种特殊的方法去生成 Python 源码。这个先放着,我们等下再绕回来看。 现在先让我们回头看看前面所说的 _parse
函数:
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 |
def _parse(reader, in_block=None): body = _ChunkList([]) while True: # Find next template directive curly = 0 while True: curly = reader.find("{", curly) if curly == -1 or curly + 1 == reader.remaining(): # EOF if in_block: raise ParseError("Missing { %% end %% } block for %s" % in_block) body.chunks.append(_Text(reader.consume())) return body # If the first curly brace is not the start of a special token, # start searching from the character after it if reader[curly + 1] not in ("{", "%"): curly += 1 continue # When there are more than 2 curlies in a row, use the # innermost ones. This is useful when generating languages # like latex where curlies are also meaningful if (curly + 2 < reader.remaining() and reader[curly + 1] == '{' and reader[curly + 2] == '{'): curly += 1 continue break |
我们将在文件中无限循环下去来查找我们所规定的特殊标记符号。当我们到达文件的末尾处时,我们将文本节点添加至列表中然后退出循环。
1 2 3 |
# Append any text before the special token if curly > 0: body.chunks.append(_Text(reader.consume(curly))) |
在我们对特殊标记的代码块进行处理之前,我们先将静态的部分添加至节点列表中。
1 |
start_brace = reader.consume(2) |
在遇到 { {
或者 { %
的符号时,我们便开始着手处理相应的的表达式:
1 2 3 4 5 6 7 8 9 10 11 |
# Expression if start_brace == "{ {": end = reader.find("} }") if end == -1 or reader.find("\n", 0, end) != -1: raise ParseError("Missing end expression } }") contents = reader.consume(end).strip() reader.consume(2) if not contents: raise ParseError("Empty expression") body.chunks.append(_Expression(contents)) continue |
当遇到 { {
之时,便意味着后面会跟随一个表达式,我们只需要将表达式提取出来,并添加至_Expression
节点列表中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# Block assert start_brace == "{ %", start_brace end = reader.find("% }") if end == -1 or reader.find("\n", 0, end) != -1: raise ParseError("Missing end block % }") contents = reader.consume(end).strip() reader.consume(2) if not contents: raise ParseError("Empty block tag ({ % % })") operator, space, suffix = contents.partition(" ") # End tag if operator == "end": if not in_block: raise ParseError("Extra { % end % } block") return body elif operator in ("try", "if", "for", "while"): # parse inner body recursively block_body = _parse(reader, operator) block = _ControlBlock(contents, block_body) body.chunks.append(block) continue else: raise ParseError("unknown operator: %r" % operator) |
在遇到模板里的代码块的时候,我们需要通过递归的方式将代码块提取出来,并添加至 _ControlBlock
节点列表中。当遇到 { % end % }
时,意味着这个代码块的结束,这个时候我们可以跳出相对应的函数了。
好了现在,让我们看看之前所提到的 _Node
节点,别慌,这其实是很简单的:
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 |
class _Node(object): def generate(self, writer): raise NotImplementedError() class _ChunkList(_Node): def __init__(self, chunks): self.chunks = chunks def generate(self, writer): for chunk in self.chunks: chunk.generate(writer) `_ChunkList` 只是一个节点列表而已。 ~~~Python class _File(_Node): def __init__(self, body): self.body = body def generate(self, writer): writer.write_line("def _execute():") with writer.indent(): writer.write_line("_buffer = []") self.body.generate(writer) writer.write_line("return ''.join(_buffer)") |
在 _File
中,它会将 _execute
函数写入 CodeWriter
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class _Expression(_Node): def __init__(self, expression): self.expression = expression def generate(self, writer): writer.write_line("_tmp = %s" % self.expression) writer.write_line("_buffer.append(str(_tmp))") class _Text(_Node): def __init__(self, value): self.value = value def generate(self, writer): value = self.value if value: writer.write_line('_buffer.append(%r)' % value) |
_Text
和 _Expression
节点的实现也非常简单,它们只是将我们从模板里获取的数据添加进列表中。
1 2 3 4 5 6 7 8 9 |
class _ControlBlock(_Node): def __init__(self, statement, body=None): self.statement = statement self.body = body def generate(self, writer): writer.write_line("%s:" % self.statement) with writer.indent(): self.body.generate(writer) |
在 _ControlBlock
中,我们需要将我们获取的代码块按 Python 语法进行格式化。
现在让我们看看之前所提到的模板引擎的渲染部分,我们通过在 Template
对象中实现 generate
方法来调用从模板中解析出来的 Python
代码。
1 2 3 4 5 6 |
def generate(self, **kwargs): namespace = { } namespace.update(kwargs) exec self.compiled in namespace execute = namespace["_execute"] return execute() |
在给予的全局命名空间中, exec 函数将会执行编译过的代码对象。然后我们就可以在全局中调用_execute 函数了。
最后
经过上面的一系列操作,我们便可以尽情的编译我们的模板并得到相对应的结果了。其实在 tornado 模板引擎中,还有很多特性是我们没有讨论到的,不过,我们已经了解了其最基础的工作机制,你可以在此基础上去研究你所感兴趣的部分,比如:
- 模板继承
- 模板包含
- 其余的一些逻辑控制语句,比如
else
,elfi
,try
等等 - 空白控制
- 特殊字符转译
- 更多没讲到的模板指令(译者注:请参考 tornado 官方文档
原文发布时间为:2016年08月13日
本文来自合作伙伴掘金,了解相关信息可以关注掘金网站。