<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 = """
<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'])
這段代碼裡的 <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 % }
<li>{ { job.name } }</li>
{ % end % }
模闆引擎裡的 <code>parse_template</code> 函數将會将上面這個字元串編譯成 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('</li>\n ')
_buffer.append('\n </ul>\n</html>\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, '<string>', '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 < 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 < reader.remaining() and
reader[curly + 1] == '{' and reader[curly + 2] == '{'):
break
我們将在檔案中無限循環下去來查找我們所規定的特殊标記符号。當我們到達檔案的末尾處時,我們将文本節點添加至清單中然後退出循環。
# Append any text before the special token
if curly > 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>