- 原文作者:Shipeng Feng
- 译文出自:掘金翻譯計劃
- 译者: Zheaoli
- 校對者:Kulbear, hpoenixf
我已經使用各種模板引擎很久了,現在終於有時間研究一下模板引擎到底是如何工作的了。
簡介#
簡單的說,模板引擎是一種可以用來完成涉及大量文本數據的編程任務的工具。一般而言,我們經常在一個 web 應用中利用模板引擎來生成 HTML 。在 Python 中,當你想使用模板引擎的時候,你會發現你有不少的選擇,比如jinja 或者是mako。從現在開始,我們將利用 tornado 中的模板引擎來講解模板引擎的工作原理,在 tornado 中,自帶的模板引擎相對的簡單,能方便我們去深入的剖析其原理。
在我們研究(模板引擎)的實現原理之前,先讓我們來看一個簡單的接口調用例子。
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 代碼就可以解決:
deftemplate_engine(template_string, **context):# process herereturn result_string
在整個工作過程中,模板引擎將會分為如下兩個階段對我們的字符串進行操作:
- 解析
- 渲染
在解析階段,我們將我們準備好的字符串進行解析,然後格式化成可被渲染的格式,其可能是能被 rendered.Consider
所解析的字符串,解析器可能是一種語言的解釋器或是一個語言的編譯器。如果解析器是一種解釋器的話,在解析過程中將會生成一種特殊的數據結構來存放數據,然後渲染器會遍歷整個數據結構來進行渲染。例如 Django 的模板引擎中的解析器就是一種基於解釋器的工具。除此之外,解析器可能會生成一些可執行代碼,渲染器將只會執行這些代碼,然後生成對應的結果。在 Jinja2 , Mako ,Tornado 中,模板引擎都在使用編譯器來作為解析工具。
編譯#
如同上面所說的一樣,我們需要解析我們所編寫的模板字符串,然後 tornado 中的模板解析器將會將我們所編寫的模板字符串編譯成可執行的 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
函數裡處理我們的模板。這個函數將可以使用全局命名空間裡的所有有效變量。這個函數將創建一個包含多個 string 的列表並將它們合併後返回。顯然找到一個局部變量比找一個全局變量要快多了。同時,我們對其餘代碼的優化也在這個階段完成,比如:
_buffer.append('hello')
_append_buffer = _buffer.append
# faster for repeated use
_append_buffer('hello')
在 { { ... } }
中的表達式將會被提取出來,然後添加進 string
列表中。在 tornado
模板模塊中,在 { { ... } }
所編寫的表達式沒有任何的限制,if 和 for 代碼塊都可以準確地轉換成為 Python 代碼。
讓我們來看看具體的代碼實現吧#
讓我們來看看模板引擎的具體實現吧。我們在 Template
類中編聲明核心變量,當我們創建一個 Template
對象後,我們便可以編譯我們所編寫的模板字符串,隨後我們便可以根據編譯的結果來對其進行渲染。我們只需要對我們所編寫的模板字符串進行一次編譯,然後我們可以緩存我們的編譯結果,下面是 Template
類的簡化版本的構造器:
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
這個類,它用於從我們自定義的模板中讀取數據:
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
函數裡,我們先要創建一個 _TemplateReader
對象:
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
函數:
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)))
在我們對特殊標記的代碼塊進行處理之前,我們先將靜態的部分添加至節點列表中。
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
中,我們需要將我們獲取的代碼塊按 Python 語法進行格式化。
現在讓我們看看之前所提到的模板引擎的渲染部分,我們通過在 Template
對象中實現 generate
方法來調用從模板中解析出來的 Python
代碼。
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 官方文檔)