天天看點

詳解模闆引擎工作機制

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