[譯] 一個模板引擎是如何工作的?
原文: 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模塊比我們這里討論的特性要多得多,但我們已經了解到了基本思想,如果感興趣的話,你可以發現更多:
- 模板繼承
- 模板包含
- 更多控制邏輯,例如else, elif, try, 等等
- 空格控制
- 轉義
- 更多模板指令
來自:https://github.com/ictar/pythondocument/blob/master/Others/一個模板引擎是如何工作的?.md