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

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

简介

简单的说,模版引擎是一种可以用来完成涉及大量文本数据的编程任务的工具。一般而言,我们经常在一个 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 , MakoTornado 中,模板引擎都在使用编译器来作为解析工具。

编译

如同上面所说的一样,我们需要解析我们所编写的模板字符串,然后 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日

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

详解模板引擎工作机制相关推荐

  1. 深入浅出之Smarty模板引擎工作机制(一)

    深入浅出Smarty模板引擎工作机制,我们将对比使用smarty模板引擎和没使用smarty模板引擎的两种开发方式的区别,并动手开发一个自己的模板引擎,以便加深对smarty模板引擎工作机制的理解. ...

  2. 深入浅出之Smarty模板引擎工作机制(二)

    源代码下载地址:深入浅出之Smarty模板引擎工作机制 接下来根据以下的Smarty模板引擎原理流程图开发一个自己的模板引擎用于学习,以便加深理解. Smarty模板引擎的原理,其实是这么一个过程: ...

  3. Smarty模板引擎工作机制(一)

    深入浅出Smarty模板引擎工作机制,我们将对比使用smarty模板引擎和没使用smarty模板引擎的两种开发方式的区别,并动手开发一个自己的模板引擎,以便加深对smarty模板引擎工作机制的理解. ...

  4. Smarty模板引擎工作机制(二)

    源代码下载地址:深入浅出之Smarty模板引擎工作机制 接下来根据以下的Smarty模板引擎原理流程图开发一个自己的模板引擎用于学习,以便加深理解. Smarty模板引擎的原理,其实是这么一个过程: ...

  5. 图文详解 HDFS 的工作机制及其原理

    大家好,我是大D. 今天开始给大家分享关于大数据入门技术栈--Hadoop的学习内容. 初识 Hadoop 为了解决大数据中海量数据的存储与计算问题,Hadoop 提供了一套分布式系统基础架构,核心内 ...

  6. php模板缓存_Smarty模板引擎缓存机制详解

    本文实例讲述了Smarty模板引擎缓存机制.分享给大家供大家参考,具体如下: 首先说下smarty缓存和编译,这是两个不同的概念,编译默认情况下是启动的,而缓存机制需要人为开启,smarty编译过的文 ...

  7. 详解模板注入漏洞(下)

    作者 | 原作者gosecure,翻译整理shan66 来源 | http://gosecure.github.io/ 在上一篇文章中,我们为读者详细介绍了模版注入漏洞的概念,模版引擎的识别方法,以及 ...

  8. 我的世界Java版最大村庄_我的世界19w11a:详解MC全新村庄机制,我已经准备好重开存档了!...

    已经两周没见到JAVA版的快照了,难道Mojang在憋大招么?没错,本周快照19w11a确实是憋了一个大招! 我的世界19w11a:正式版发布不远了!村庄大改完成,全新交易来袭我的世界19w11a:详 ...

  9. 异常处理器详解 Java多线程异常处理机制 多线程中篇(四)

    在Thread中有异常处理器相关的方法 在ThreadGroup中也有相关的异常处理方法 示例 未检查异常 对于未检查异常,将会直接宕掉,主线程则继续运行,程序会继续运行 在主线程中能不能捕获呢? 我 ...

最新文章

  1. linux基本命令之rsync
  2. python语言有什么用-为什么现在很多人都使用Python语言有什么优势
  3. .net中的4种事务总结
  4. 海量数据 - join处理
  5. 电机编码器调零步骤_编码器原理、霍尔应用原理、调整步骤三个方面进行解读编码器调试...
  6. OPPO K3将登陆印度市场 高性价比能否占据一席之地
  7. python接口测试_测试大牛都会的接口测试+Python测开+Linux技能+Git命令合集!
  8. Wi-Fi 6 认证计划启动:开启 Wi-Fi 新纪元
  9. 软件工程中需要学习和掌握的软件都有哪些_上海理工大学869软件工程考研经验...
  10. form表单提交serialize()方法和
  11. 办公必备:4个在线文档格式转换网站
  12. android开发如何获取电话号码的归属地信息
  13. 【暴力破解】medusacrowbar工具
  14. 基于seq2seq自动生成小说
  15. 《动手学深度学习》(PyTorch版)代码注释 - 51 【Style_transfer】
  16. macOS 虚拟摄像头:CamTwist for Mac
  17. 教你学java_教妹学Java:Spring 入门篇
  18. 达梦数据库安装及连接
  19. 今日头条信息流广告怎么做?(今日头条信息流广告费用解析)
  20. Navicat Premium安装教程(激活)

热门文章

  1. 商业银行首发互联网金融品牌
  2. 选择程序员还是游戏建模师?游戏建模师前景是假的吗?
  3. [solidity语法学习二]基于Remix以太坊的BlindAuction模板代码分析
  4. 深夜换脸啦,神秘的程序员们头像包第四发
  5. 奥巴马开学演讲稿!建议有孩子的父母亲都读下
  6. 流利阅读12.22 ‘Spider-Man: Into the Spider-Verse’: A Fresh Take on a Venerable Hero
  7. 搜索店铺列表API 返回值说明
  8. c语言程序函数的结构,C语言课件:第11讲函数与程序结构
  9. 解决部分华为,乐视手机听筒模式听不到声音问题
  10. Quartz的oracle建表语句