原文: How a template engine works
我已经使用模板引擎很长一段时间了,现在终于有时间来了解一下模板引擎是如何工作的。
简单地说,模板引擎是一个工具,你可以用它来进行涉及到很多的文本数据的编程任务。最常见的用法是Web应用程序中的HTML生成。尤其是在Python中,如果你想要使用一个模板引擎,那么现在我们有几种选择,例如 jinja 或者 mako 。在这里,我们要通过深入tornado web框架的template模块,找出一个模板引擎是如何工作的,这是一个简单的系统,这样我们就可以专注于过程的基本思路。
进入实现细节之前,让我们首先来看看简单的API使用:
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'])
这里,用户名在PAGE_HTML中是动态的,工作列表也是。你可以安装 tornado
,然后运行代码来看看输出。
如果我们进一步看看 PAGE_HTML
,那我们很容易就可以发现,一个模板字符串有两个部分,静态文本部分和动态部分。我们使用特殊标记来区分开动态部分。总的来说,模板引擎应该接受模板字符串,然后原样输出静态部分,它还需要利用给定的上下文来处理动态部分,然后生成正确的字符串结果。所以,基本上,一个模板引擎就是一个Python函数:
def template_engine(template_string, **context): # process here return result_string
在处理过程中,模板引擎有两个阶段:
解析阶段接受模板字符串,然后生成可以渲染的结果。将模板字符串想成源代码的话,解析工具可以是一个编程语言解释器或者编程语言编译器。如果工具是解释器,那么解析会生成一个数据结构,而渲染工具将会根据这个结构来生成结果文本。Django模板引擎解析工具就是这么一个解释器。另外,解析生成一些可执行代码,那么渲染工具仅仅执行代码并生成结果。Jinja2, Mako和Tornado的template模块都使用编译器作为解析工具。
如上所述,现在,我们需要解析模板字符串,而tornado的template模块中的解析工具将模板编译成Python代码。我们的解析工具只是一个Python函数,它生成Python代码:
def parse_template(template_string): # compilation return python_source_code
在我们进入 parse_template
的实现之前,让我们看看它所生成的代码,下面是一个样例模板源字符串:
<html> Hello, {{ username }}! <ul> {% for job in jobs %} <li>{{ job.name }}</li> {% end %} </ul> </html>
我们的 parse_template
函数将把这个模板编译成Python代码,它仅仅是一个函数,简化版本如下:
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
的函数中,该函数访问全局命名空间的所有上下文变量。该函数创建一个字符串列表,然后将它们连在一起变成结果字符串。 username
位于局部变量 _tmp
中,查询局部变量比查询全局变量快得多。这里,还可以做一些其他的优化,例如:
_buffer.append('hello') _append_buffer = _buffer.append # faster for repeated use _append_buffer('hello')
{{ ... }}
中的字符串被分析附加到字符串缓冲列表中。在tornado template模块中,对你的语句中可以包含的表达式并无限制,if和for块直接被转换成Python。
现在,让我们看看实际实现。我们所使用的核心接口是 Template
类,当我们创建一个 Template
对象时,会对模板字符串进行编译,接着可以用它来渲染一个给定的上下文。我们仅需一次编译,就可以把该模板对象缓存起来,构造器的简化版本如下:
class Template(object): def __init__(self, template_string): self.code = parse_template(template_string) self.compiled = compile(self.code, '<string>', 'exec')
compile
会将 source
编译成一个code对象。稍后,我们可以使用一个 exec
语句来执行它。现在,构建 parse_template
函数,首先,我们需要将模板字符串解析成节点(node)列表,它清楚如何生成Python代码,我们需要一个名为 _parse
的函数,稍后我们将看到这个函数,我们现在需要一些辅助器,用以读取整个模板文件,我们有 _TemplateReader
类,在我们处理模板文件的时候,它为我们处理读取。我们需要从头开始,找到一些特殊的标记, _TemplateReader
将会记录当前位置,并为我们提供执行方法:
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上下文管理器:
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
的开头,我们首先创建一个模板读取器:
def parse_template(template_string): reader = _TemplateReader(template_string) file_node = _File(_parse(reader)) writer = _CodeWriter() file_node.generate(writer) return str(writer)
然后,我们将读取器传递给 _parse
函数,并生成节点列表。所有这些节点都是模板文件节点的子节点。我们创建一个CodeWriter对象,文件节点将Python代码写入到CodeWriter中,然后返回生成的Python代码。 _Node
类将会处理特殊情况下的Python代码生成,稍后我们会看到。现在,回到 _parse
函数:
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
进入无限循环以查找剩余文件中的模板指令,如果抵达文件末端,则附加文本节点并退出,否则,说明找到了一个模板指令。
# Append any text before the special token if curly > 0: body.chunks.append(_Text(reader.consume(curly)))
在我们处理了特殊的token后,如果有静态部分,则将其附加到文本节点。
start_brace = reader.consume(2)
获取起始括号,它应该是 '{{'
或者 '{%'
。
# 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
节点。
# 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
类的秘密了,它非常简单:
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
仅是一个节点列表。
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。
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
节点也相当简单,只是附加从模板源获得的东西。
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
节点,我们需要缩进并带缩进编写子节点列表。
现在,让我们回到渲染部分,通过使用 Template
对象的 generate
方法,我们渲染一个上下文, generate
方法仅仅是调用编译好了的Python代码:
def generate(self, **kwargs): namespace = {} namespace.update(kwargs) exec self.compiled in namespace execute = namespace["_execute"] return execute()
exec
函数在给定的全局命名空间内执行编译好的代码,然后,从全局命名空间内抓取 _execute
函数,然后调用它。
就是这样啦,将模板编译成Python函数,然后执行以获取结果。tornado的template模块比我们这里讨论的特性要多得多,但我们已经了解到了基本思想,如果感兴趣的话,你可以发现更多: