天天看点

详解模板引擎工作机制

<b>本文讲的是详解模板引擎工作机制,</b>

我已经使用各种模版引擎很久了,现在终于有时间研究一下模版引擎到底是如何工作的了。

<a></a>

在我们研究(模版引擎)的实现原理之前,先让我们来看一个简单的接口调用例子。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

from tornado import template

PAGE_HTML = """

&lt;html&gt;

Hello, {{ username }}!

&lt;ul&gt;

{% for job in job_list %}

&lt;li&gt;{{ job }}&lt;/li&gt;

{% end %}

&lt;/ul&gt;

&lt;/html&gt;

"""

t = template.Template(PAGE_HTML)

print t.generate(username='John', job_list=['engineer'])

这段代码里的 <code>username</code> 将会动态的生成,<code>job</code> 列表也是如此。你可以通过安装 <code>tornado</code> 并运行这段代码来看看最后的效果。

如果你仔细观察 <code>PAGE_HTML</code> ,你会发现这段模板字符串由两个部分组成,一部分是固定的字符串,另一部分是将会动态生成的内容。我们将会用特殊的符号来标注动态生成的部分。在整个工作流程中,模板引擎需要正确输出固定的字符串,同时需要将正确的结果替换我们所标注的需要动态生成的字符串。

使用模板引擎最简单的方式就是像下面这样用一行 python 代码就可以解决:

deftemplate_engine(template_string, **context):# process herereturn result_string

在整个工作过程中,模板引擎将会分为如下两个阶段对我们的字符串进行操作:

解析

渲染

在解析阶段,我们将我们准备好的字符串进行解析,然后格式化成可被渲染的格式,其可能是能被<code>rendered.Consider</code> 所解析的字符串,解析器可能是一个语言的解释器或是一个语言的编译器。如果解析器是一种解释器的话,在解析过程中将会生成一种特殊的数据结构来存放数据,然后渲染器会遍历整个数据结构来进行渲染。例如 Django 的模板引擎中的解析器就是一种基于解释器的工具。除此之外,解析器可能会生成一些可执行代码,渲染器将只会执行这些代码,然后生成对应的结果。在 Jinja2 , Mako,Tornado 中,模板引擎都在使用编译器来作为解析工具。

如同上面所说的一样,我们需要解析我们所编写的模板字符串,然后 tornado 中的模板解析器将会将我们所编写的模板字符串编译成可执行的 Python 代码。我们的解析工具负责生成Python代码,而仅仅由单个Python函数构成:

def parse_template(template_string):

# compilation

return python_source_code

在我们分析 <code>parse_template</code> 的代码之前,让我们先看个模板字符串的例子:

Hello, { { username } }!

{ % for job in jobs % }

&lt;li&gt;{ { job.name } }&lt;/li&gt;

{ % end % }

模板引擎里的 <code>parse_template</code> 函数将会将上面这个字符串编译成 Python 源码,最简单的实现方式如下:

def _execute():

_buffer = []

_buffer.append('\n&lt;html&gt;\n Hello, ')

_tmp = username

_buffer.append(str(_tmp))

_buffer.append('!\n &lt;ul&gt;\n ')

for job in jobs:

_buffer.append('\n &lt;li&gt;')

_tmp = job.name

_buffer.append('&lt;/li&gt;\n ')

_buffer.append('\n &lt;/ul&gt;\n&lt;/html&gt;\n')

return''.join(_buffer)

现在我们在 <code>_execute</code> 函数里处理我们的模版。这个函数将可以使用全局命名空间里的所有有效变量。这个函数将创建一个包含多个 string 的列表并将他们合并后返回。显然找到一个局部变量比找一个全局变量要快多了。同时,我们对于其余代码的优化也在这个阶段完成,比如:

_buffer.append('hello')

_append_buffer = _buffer.append

# faster for repeated use

_append_buffer('hello')

在 <code>{ { ... } }</code> 中的表达式将会被提取出来,然后添加进 <code>string</code> 列表中。在 <code>tornado</code> 模板模块中,在 <code>{ { ... } }</code> 所编写的表达式没有任何的限制,if 和 for 代码块都可以准确地转换成为 Python代码。

让我们来看看模板引擎的具体实现吧。我们在 <code>Template</code> 类中编声明核心变量,当我们创建一个<code>Template</code> 对象后,我们便可以编译我们所编写的模板字符串,随后我们便可以根据编译的结果来对其进行渲染。我们只需要对我们所编写的模板字符串进行一次编译,然后我们可以缓存我们的编译结果,下面是 <code>Template</code> 类的简化版本的构造器:

class Template(object):

def__init__(self, template_string):

self.code = parse_template(template_string)

self.compiled = compile(self.code, '&lt;string&gt;', 'exec')

上段代码里的 <code>compile</code> 函数将会将字符串编译成为可执行代码,我们可以稍后调用 <code>exec</code> 函数来执行我们生成的代码。现在,让我们来看看 <code>parse_template</code> 函数的实现,首先,我们需要将我们所编写的模板字符串转化成一个个独立的节点,为我们后面生成 Python 代码做好准备。在这过程中,我们需要一个 <code>_parse</code> 函数,我们先把它放在一边,等下在回来看看这个函数。现,我们需要编写一些辅助函数来帮助我们从模板文件里读取数据。现在让我们来看看 <code>_TemplateReader</code> 这个类,它用于从我们自定义的模板中读取数据:

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 &lt; 0:

return self.text[key]

return self.text[self.pos + key]

def __str__(self):

return self.text[self.pos:]

为了生成 Python 代码,我们需要去看看 <code>_CodeWriter</code> 这个类的源码,这个类可以编写代码行和管理缩进,同时它也是一个 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

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

return self.buffer.getvalue()

在 <code>parse_template</code> 函数里,我们先要创建一个 <code>_TemplateReader</code> 对象:

reader = _TemplateReader(template_string)

file_node = _File(_parse(reader))

writer = _CodeWriter()

file_node.generate(writer)

return str(writer)

然后,我们将我们所创建的 <code>_TemplateReader</code> 对象传入 <code>_parse</code> 函数中以便生成节点列表。这里生成的所有节点都是模板文件的子节点。接着,我们创建一个 <code>_CodeWriter</code> 对象,然后 <code>file_node</code> 对象会把生成的 Python 代码写入 <code>_CodeWriter</code> 对象中。然后我们返回一系列动态生成的 Python 代码。<code>_Node</code> 类将会用一种特殊的方法去生成 Python 源码。这个先放着,我们等下再绕回来看。 现在先让我们回头看看前面所说的 <code>_parse</code> 函数:

def _parse(reader, in_block=None):

body = _ChunkList([])

while True:

# Find next template directive

curly = 0

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 &lt; reader.remaining() and

reader[curly + 1] == '{' and reader[curly + 2] == '{'):

break

我们将在文件中无限循环下去来查找我们所规定的特殊标记符号。当我们到达文件的末尾处时,我们将文本节点添加至列表中然后退出循环。

# Append any text before the special token

if curly &gt; 0:

body.chunks.append(_Text(reader.consume(curly)))

在我们对特殊标记的代码块进行处理之前,我们先将静态的部分添加至节点列表中。

start_brace = reader.consume(2)

在遇到 <code>{ {</code> 或者 <code>{ %</code> 的符号时,我们便开始着手处理相应的的表达式:

# 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))

当遇到 <code>{ {</code> 之时,便意味着后面会跟随一个表达式,我们只需要将表达式提取出来,并添加至<code>_Expression</code> 节点列表中。

# Block

assert start_brace == "{ %", start_brace

end = reader.find("% }")

raise ParseError("Missing end block % }")

raise ParseError("Empty block tag ({ % % })")

operator, space, suffix = contents.partition(" ")

# End tag

if operator == "end":

if not in_block:

raise ParseError("Extra { % end % } block")

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)

raise ParseError("unknown operator: %r" % operator)

在遇到模板里的代码块的时候,我们需要通过递归的方式将代码块提取出来,并添加至 <code>_ControlBlock</code>节点列表中。当遇到 <code>{ % end % }</code> 时,意味着这个代码块的结束,这个时候我们可以跳出相对应的函数了。

好了现在,让我们看看之前所提到的 <code>_Node</code> 节点,别慌,这其实是很简单的:

class _Node(object):

def generate(self, writer):

raise NotImplementedError()

class _ChunkList(_Node):

def __init__(self, chunks):

self.chunks = chunks

for chunk in self.chunks:

chunk.generate(writer)

`_ChunkList` 只是一个节点列表而已。

~~~Python

class _File(_Node):

def __init__(self, body):

self.body = body

writer.write_line("def _execute():")

with writer.indent():

writer.write_line("_buffer = []")

self.body.generate(writer)

writer.write_line("return ''.join(_buffer)")

在 <code>_File</code> 中,它会将 <code>_execute</code> 函数写入 <code>CodeWriter</code>。

class _Expression(_Node):

def __init__(self, expression):

self.expression = expression

writer.write_line("_tmp = %s" % self.expression)

writer.write_line("_buffer.append(str(_tmp))")

class _Text(_Node):

def __init__(self, value):

self.value = value

value = self.value

if value:

writer.write_line('_buffer.append(%r)' % value)

<code>_Text</code> 和 <code>_Expression</code> 节点的实现也非常简单,它们只是将我们从模板里获取的数据添加进列表中。

class _ControlBlock(_Node):

def __init__(self, statement, body=None):

self.statement = statement

writer.write_line("%s:" % self.statement)

在 <code>_ControlBlock</code> 中,我们需要将我们获取的代码块按 Python 语法进行格式化。

现在让我们看看之前所提到的模板引擎的渲染部分,我们通过在 <code>Template</code> 对象中实现 <code>generate</code> 方法来调用从模板中解析出来的 <code>Python</code> 代码。

def generate(self, **kwargs):

namespace = { }

namespace.update(kwargs)

exec self.compiled in namespace

execute = namespace["_execute"]

return execute()

在给予的全局命名空间中, exec 函数将会执行编译过的代码对象。然后我们就可以在全局中调用_execute 函数了。

经过上面的一系列操作,我们便可以尽情的编译我们的模板并得到相对应的结果了。其实在 tornado 模板引擎中,还有很多特性是我们没有讨论到的,不过,我们已经了解了其最基础的工作机制,你可以在此基础上去研究你所感兴趣的部分,比如:

模板继承

模板包含

其余的一些逻辑控制语句,比如 <code>else</code> , <code>elfi</code> , <code>try</code> 等等

空白控制

特殊字符转译

<b></b>

<b>原文发布时间为:2016年08月13日</b>

<b>本文来自云栖社区合作伙伴掘金,了解相关信息可以关注掘金网站。</b>