原文請見:http://aosabook.org/en/500L/a-template-engine.htmlhtml
代碼請見:https://github.com/aosabook/500lines/tree/master/template-engine前端
大多數程序包含大量的邏輯,以及少許文本數據。編程語言被設計成適合這種類型的編程。可是一些編程任務只涉及一點邏輯,以及大量的文本數據。 對於這些任務,咱們但願有一個更適合這些問題的工具。模板引擎就是這樣一種工具。在本章中,咱們將構建一個簡單的模板引擎。python
最多見的一個以文字爲主的任務是在 web 應用程序。任何 web 應用程序的一個重要工序是生成用於瀏覽器顯示的 HTML。 不多有 HTML 頁面是徹底靜態的:它們至少包含少許的動態數據,好比用戶名。一般,它們包含大量的動態數據:產品列表、好友的新聞更新等等。git
與此同時,每一個HTML頁面都包含大量的靜態文本。這些頁面很大,包含成千上萬個字節的文本。 web 應用程序開發人員有一個問題要解決:如何最好地生成包含靜態和動態數據混合的大段字符串?另外一個問題是: 靜態文本其實是由團隊的另外一個成員、前端設計人員編寫的 HTML 標記,他們但願可以以熟悉的方式使用它。github
爲了便於說明,假設咱們想要生成這個 HTML:web
<p>Welcome, Charlie!</p>
<p>Products:</p>
<ul>
<li>Apple: $1.00</li>
<li>Fig: $1.50</li>
<li>Pomegranate: $3.25</li>
</ul>
複製代碼
這裏,用戶的名字將是動態的,就像產品的名稱和價格同樣。甚至產品的數量也不是固定不變的:有時可能會有更多或更少的產品展現出來。正則表達式
構造這個 HTML 的一種方法是在咱們的代碼中將字符串常量們合併到一塊兒來生成頁面。動態數據將插入以替換某些字符串。咱們的一些動態數據是重複的,就像咱們的產品列表同樣。 這意味着咱們將會有大量重複的 HTML,所以這些內容必須單獨處理,並與頁面的其餘部分合並。express
好比,咱們的 demo 頁面像這樣:編程
# The main HTML for the whole page.
PAGE_HTML = """ <p>Welcome, {name}!</p> <p>Products:</p> <ul> {products} </ul> """
# The HTML for each product displayed.
PRODUCT_HTML = "<li>{prodname}: {price}</li>\n"
def make_page(username, products):
product_html = ""
for prodname, price in products:
product_html += PRODUCT_HTML.format(
prodname=prodname, price=format_price(price))
html = PAGE_HTML.format(name=username, products=product_html)
return html
複製代碼
這是可行的,可是有點亂。HTML 是嵌入在咱們的代碼中的多個字符串常量。頁面的邏輯很難看到,由於靜態文本被拆分爲獨立的部分。如何格式化數據的細節隱藏在 Python 代碼中。爲了修改 HTML 頁面,咱們的前端設計人員須要可以編輯 Python 代碼。想象一下,若是頁面是10(或者100)倍的複雜,代碼會是什麼樣子。它很快就會變得沒法維護。瀏覽器
生成 HTML 頁面的更好方法是使用模板。HTML 頁面是做爲模板編寫的,這意味着該文件主要是靜態的 HTML,其中嵌入了使用特殊符號標記的動態片斷。咱們的 demo 頁面模板能夠像這樣:
<p>Welcome, {{user_name}}!</p>
<p>Products:</p>
<ul>
{% for product in product_list %}
<li>{{ product.name }}:
{{ product.price|format_price }}</li>
{% endfor %}
</ul>
複製代碼
這裏的重點是 HTML 文本,其中嵌入了一些邏輯。將這種以文檔爲中心的方法與上面的以邏輯爲中心的代碼進行對比。前面的程序主要是 Python 代碼,HTML 嵌入在 Python 邏輯中。這裏咱們的程序主要是靜態 HTML 標記。
要在咱們的程序中使用 HTML 模板,咱們須要一個模板引擎:一個使用靜態模板來描述頁面的結構和靜態內容的函數,以及提供動態數據插入模板的動態上下文。模板引擎將模板和上下文結合起來生成完整的 HTML 字符串。模板引擎的工做是解釋模板,用真實數據替換動態片斷。
模板引擎在它們支持的語法中有所不一樣。咱們的模板語法基於 Django,一個流行的 web 框架。既然咱們在 Python 中實現了咱們的引擎,那麼一些 Python 概念將出如今咱們的語法中。在咱們的 demo 示例中,咱們已經看到了這一章的一些語法,下面是咱們將要實現的全部語法:
使用雙花括號插入上下文中的數據:
<p>Welcome, {{user_name}}!</p>
複製代碼
當模板被呈現時,模板中可用的數據將提供給上下文。稍後將進行更詳細的討論。
模板引擎一般使用簡化的、輕鬆的語法來訪問數據中的元素。在 Python 中,這些表達式有不一樣的效果:
dict["key"]
obj.attr
obj.method()
複製代碼
在咱們的模板語法中,全部這些操做都用點來表示:
dict.key
obj.attr
obj.method
複製代碼
點符號將訪問對象屬性或字典值,若是結果值是可調用的,它將自動調用。這與 Python 代碼不一樣,您須要使用不一樣的語法來執行這些操做。這就產生了更簡單的模板語法:
<p>The price is: {{product.price}}, with a {{product.discount}}% discount.</p>
複製代碼
您可使用過濾器函數來修改值,經過管道字符調用:
<p>Short name: {{story.subject|slugify|lower}}</p>
複製代碼
構建好玩的頁面一般須要少許的決策,因此條件語句也是可用的:
{% if user.is_logged_in %}
<p>Welcome, {{ user.name }}!</p>
{% endif %}
複製代碼
循環容許咱們在頁面中包含數據集合:
<p>Products:</p>
<ul>
{% for product in product_list %}
<li>{{ product.name }}: {{ product.price|format_price }}</li>
{% endfor %}
</ul>
複製代碼
與其餘編程語言同樣,條件語句和循環能夠嵌套來構建複雜的邏輯結構。
最後,註釋也不能少:
{# This is the best template ever! #}
複製代碼
總的來講,模板引擎有兩個主要的工做:解析模板,渲染模板。
渲染模板具體涉及:
從解析階段傳遞什麼到呈現階段是關鍵。
解析能夠提供什麼?有兩種選擇:咱們稱它們爲解釋和編譯。
在解釋模型中,解析生成一個表示模板結構的數據結構。呈現階段將根據所找到的指令對數據結構進行處理,並將結果文本組合起來。Django 模板引擎使用這種方法。
在編譯模型中,解析生成某種形式的可直接執行的代碼。呈現階段執行該代碼,生成結果。Jinja2 和 Mako 是使用編譯方法的模板引擎的兩個例子。
咱們的引擎的實現使用編譯模型:咱們將模板編譯成 Python 代碼。當它運行時,組裝成結果。 模板被編譯成 Python 代碼,程序將運行得更快,由於即便編譯過程稍微複雜一些,但它只須要運行一次。 將模板編譯爲 Python 要稍微複雜一些,但它並無您想象的那麼糟糕。並且,正如任何開發人員都能告訴你的那樣,編寫一個會編寫程序的程序比編寫程序要有趣得多!
在咱們瞭解模板引擎的代碼以前,讓咱們看看它要生成的代碼。解析階段將把模板轉換爲 Python 函數。這是咱們的模板:
<p>Welcome, {{user_name}}!</p>
<p>Products:</p>
<ul>
{% for product in product_list %}
<li>{{ product.name }}:
{{ product.price|format_price }}</li>
{% endfor %}
</ul>
複製代碼
針對上面的模板,咱們最後想獲得編譯後的 Python 代碼以下所示:
def render_function(context, do_dots):
c_user_name = context['user_name']
c_product_list = context['product_list']
c_format_price = context['format_price']
result = []
append_result = result.append
extend_result = result.extend
to_str = str
extend_result([
'<p>Welcome, ',
to_str(c_user_name),
'!</p>\n<p>Products:</p>\n<ul>\n'
])
for c_product in c_product_list:
extend_result([
'\n <li>',
to_str(do_dots(c_product, 'name')),
':\n ',
to_str(c_format_price(do_dots(c_product, 'price'))),
'</li>\n'
])
append_result('\n</ul>\n')
return ''.join(result)
複製代碼
幾點說明:
do_dots
函數可使用模板的文本構造了 Templite 對象,而後您可使用它來呈現一個特定的上下文,即數據字典:
# Make a Templite object.
templite = Templite(''' <h1>Hello {{name|upper}}!</h1> {% for topic in topics %} <p>You are interested in {{topic}}.</p> {% endfor %} ''',
{'upper': str.upper},
)
# Later, use it to render some data.
text = templite.render({
'name': "Ned",
'topics': ['Python', 'Geometry', 'Juggling'],
})
複製代碼
在建立對象時,咱們會傳遞模板的文本,這樣咱們就能夠只執行一次編譯步驟,而後調用屢次來重用編譯後的結果。
構造函數還受一個字典參數,一個初始上下文。這些存儲在Templite對象中,當模板稍後呈現時將可用。這些都有利於定義咱們想要在任何地方均可用的函數或常量,好比上一個例子中的upper。
在討論實現 Templite 以前,讓咱們先搞定一個工具類: CodeBuilder
引擎中的大部分工做是解析模板並生成 Python 代碼。爲了幫助生成 Python,咱們建立了 CodeBuilder 類,它幫咱們添加代碼行,管理縮進,最後從編譯的 Python 中給出結果。
CodeBuilder 對象保存了一個字符串列表,這些字符串將一塊兒做爲最終的 Python 代碼。它須要的另外一個狀態是當前的縮進級別:
class CodeBuilder(object):
"""Build source code conveniently."""
def __init__(self, indent=0):
self.code = []
self.indent_level = indent
複製代碼
CodeBuilder 作的事並很少。add_line添加了一個新的代碼行,它會自動將文本縮進到當前的縮進級別,並提供一條新行:
def add_line(self, line):
"""Add a line of source to the code. Indentation and newline will be added for you, don't provide them. """
self.code.extend([" " * self.indent_level, line, "\n"])
複製代碼
indent
和 dedent
提升或減小縮進級別:
INDENT_STEP = 4 # PEP8 says so!
def indent(self):
"""Increase the current indent for following lines."""
self.indent_level += self.INDENT_STEP
def dedent(self):
"""Decrease the current indent for following lines."""
self.indent_level -= self.INDENT_STEP
複製代碼
add_section
由另外一個 CodeBuilder
對象管理。這讓咱們能夠在代碼中預留一個位置,隨後再添加文本。self.code 列表主要是字符串列表,但也會保留對這些 section 的引用:
def add_section(self):
"""Add a section, a sub-CodeBuilder."""
section = CodeBuilder(self.indent_level)
self.code.append(section)
return section
複製代碼
__str__
使用全部代碼生成一個字符串,將 self.code 中的全部字符串鏈接在一塊兒。注意,由於 self.code 能夠包含 sections,這可能會遞歸調用其餘 CodeBuilder
對象:
def __str__(self):
return "".join(str(c) for c in self.code)
複製代碼
get_globals
經過執行代碼生成最終值。他將對象字符串化,而後執行,並返回結果值:
def get_globals(self):
"""Execute the code, and return a dict of globals it defines."""
# A check that the caller really finished all the blocks they started.
assert self.indent_level == 0
# Get the Python source as a single string.
python_source = str(self)
# Execute the source, defining globals, and return them.
global_namespace = {}
exec(python_source, global_namespace)
return global_namespace
複製代碼
最後一個方法利用了 Python 的一些奇異特性。exec
函數執行包含 Python 代碼的字符串。exec
的第二個參數是一個字典,它將收集由代碼定義的全局變量。舉個例子,若是咱們這樣作:
python_source = """\ SEVENTEEN = 17 def three(): return 3 """
global_namespace = {}
exec(python_source, global_namespace)
複製代碼
則 global_namespace['SEVENTEEN']
是 17,global_namespace['three']
返回函數 three
。
雖然咱們只使用 CodeBuilder
來生成一個函數,可是這裏沒有限制它只能作這些。這使得類更易於實現,也更容易理解。 CodeBuilder
容許咱們建立一大塊 Python 源代碼,而且不須要了解咱們的模板引擎相關知識。get_globals
會返回一個字典,使代碼更加模塊化,由於它不須要知道咱們定義的函數的名稱。不管咱們在 Python 源代碼中定義了什麼函數名,咱們均可以從 get_globals
返回的對象中檢索該名稱。 如今,咱們能夠進入 Templite
類自己的實現,看看 CodeBuilder
是如何使用的以及在哪裏使用。
將模板編譯成 Python 函數的全部工做都發生在 Templite 構造函數中。首先,傳入的上下文被保存:
def __init__(self, text, *contexts):
"""Construct a Templite with the given `text`. `contexts` are dictionaries of values to use for future renderings. These are good for filters and global values. """
self.context = {}
for context in contexts:
self.context.update(context)
複製代碼
這裏,使用了 python 的可變參數,能夠傳入多個上下文,且後面傳入的會覆蓋前面傳入的。
咱們用集合 all_vars
來記錄模板中用到的變量,用 loop_vars
記錄模板循環體中用到的變量:
self.all_vars = set()
self.loop_vars = set()
複製代碼
稍後咱們將看到這些如何被用來幫助構造函數的代碼。首先,咱們將使用前面編寫的 CodeBuilder
類來構建咱們的編譯函數:
code = CodeBuilder()
code.add_line("def render_function(context, do_dots):")
code.indent()
vars_code = code.add_section()
code.add_line("result = []")
code.add_line("append_result = result.append")
code.add_line("extend_result = result.extend")
code.add_line("to_str = str")
複製代碼
在這裏,咱們構造了 CodeBuilder
對象,並開始編寫代碼行。咱們的 Python 函數將被稱爲 render_function
,它將接受兩個參數:上下文是它應該使用的數據字典,而 do_dots
是實現點屬性訪問的函數。
咱們建立一個名爲 vars_code
的部分。稍後咱們將把變量提取行寫到這一部分中。vars_code
對象讓咱們在函數中保存一個位置,當咱們有須要的信息時,它能夠被填充。
而後緩存了 list
的兩個方法及 str
到本地變量,正如上面所說的,這樣能夠提升代碼的性能。
接下來,咱們定義一個內部函數來幫助咱們緩衝輸出字符串:
buffered = []
def flush_output():
"""Force `buffered` to the code builder."""
if len(buffered) == 1:
code.add_line("append_result(%s)" % buffered[0])
elif len(buffered) > 1:
code.add_line("extend_result([%s])" % ", ".join(buffered))
del buffered[:]
複製代碼
當咱們建立大量代碼到編譯函數中時,咱們須要將它們轉換爲 append
函數調用。咱們但願將重複的 append
調用合併到一個 extend
調用中,這是一個優化點。爲了使這成爲可能,咱們緩衝了這些塊。
緩衝列表包含還沒有寫入到咱們的函數源代碼的字符串。在咱們的模板編譯過程當中,咱們將附加字符串緩衝,當咱們到達控制流點時,好比 if 語句,或循環的開始或結束時,將它們刷新到函數代碼。
flush_output
函數是一個閉包。這簡化了咱們對函數的調用:咱們沒必要告訴 flush_output
要刷新什麼緩衝區,或者在哪裏刷新它;它清楚地知道全部這些。
若是隻緩衝了一個字符串,則使用 append_result
將其添加到結果中。若是有多個緩衝,那麼將使用 extend_result
將它們添加到結果中。
回到咱們的 Templite 類。在解析控制結構時,咱們但願檢查它們語法是否正確。須要用到棧結構 ops_stack
:
ops_stack = []
複製代碼
例如,當咱們遇到控制語句 \{\% if \%\}
,咱們入棧 if
。當咱們遇到 \{\% endif \%\}
時,出棧並檢查出棧元素是否爲if
。
如今真正的解析開始了。咱們使用正則表達式將模板文本拆分爲多個 token。這是咱們的正則表達式:
tokens = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text)
複製代碼
split
函數將使用正則表達式拆分一個字符串。咱們的模式是圓括號,所以匹配將用於分割字符串,也將做爲分隔列表中的片斷返回。
(?s)
爲單行模式,意味着一個點應該匹配換行符。接下來是匹配表達式/控制結構/註釋,都爲非貪婪匹配。
拆分的結果是字符串列表。例如,該模板文本:
<p>Topics for {{name}}: {% for t in topics %}{{t}}, {% endfor %}</p>
複製代碼
會被分隔爲:
[
'<p>Topics for ', # literal
'{{name}}', # expression
': ', # literal
'{% for t in topics %}', # tag
'', # literal (empty)
'{{t}}', # expression
', ', # literal
'{% endfor %}', # tag
'</p>' # literal
]
複製代碼
將文本拆分爲這樣的 tokens 以後,咱們能夠對這些 tokens 進行循環,並依次處理它們。根據他們的類型劃分,咱們能夠分別處理每種類型。 編譯代碼是對這些 tokens 的循環:
for token in tokens:
# 註釋直接忽略
if token.startswith('{#'):
# Comment: ignore it and move on.
continue
# 表達式:提取出內容交給 _expr_code 進行處理,而後生成一行代碼
elif token.startswith('{{'):
# An expression to evaluate.
expr = self._expr_code(token[2:-2].strip())
buffered.append("to_str(%s)" % expr)
# 控制語句
elif token.startswith('{%'):
# Action tag: split into words and parse further.
# 先將前面生成的代碼刷新到編譯函數之中
flush_output()
words = token[2:-2].strip().split()
if words[0] == 'if':
# An if statement: evaluate the expression to determine if.
# if語句只能有兩個單詞
if len(words) != 2:
self._syntax_error("Don't understand if", token)
# if 入棧
ops_stack.append('if')
# 生成代碼
code.add_line("if %s:" % self._expr_code(words[1]))
# 增長下一條語句的縮進級別
code.indent()
elif words[0] == 'for':
# A loop: iterate over expression result.
# 語法檢查
if len(words) != 4 or words[2] != 'in':
self._syntax_error("Don't understand for", token)
# for 入棧
ops_stack.append('for')
# 記錄循環體中的局部變量
self._variable(words[1], self.loop_vars)
# 生成代碼
code.add_line(
"for c_%s in %s:" % (
words[1],
self._expr_code(words[3])
)
)
# 增長下一條語句的縮進級別
code.indent()
elif words[0].startswith('end'):
# Endsomething. Pop the ops stack.
# 語法檢查
if len(words) != 1:
self._syntax_error("Don't understand end", token)
end_what = words[0][3:]
# end 語句多了
if not ops_stack:
self._syntax_error("Too many ends", token)
# 對比棧頂元素
start_what = ops_stack.pop()
if start_what != end_what:
self._syntax_error("Mismatched end tag", end_what)
# 循環體結束,縮進減小縮進級別
code.dedent()
else:
self._syntax_error("Don't understand tag", words[0])
else:
# Literal content. If it isn't empty, output it.
# 純文本內容
if token:
buffered.append(repr(token))
複製代碼
有幾點須要注意:
repr
來給文本加上引號,不然生成的代碼會像這樣:extend_result([
<h1>Hello , to_str(c_upper(c_name)), !</h1>
])
複製代碼
if token:
來去掉空字符串,避免生成沒必要要的空行代碼循環結束後,須要檢查 ops_stack
是否爲空,不爲空說明控制語句格式有問題:
if ops_stack:
self._syntax_error("Unmatched action tag", ops_stack[-1])
flush_output()
複製代碼
前面咱們經過 vars_code = code.add_section()
建立了一個 section,它的做用是將傳入的上下文解構爲渲染函數的局部變量。
循環完後,咱們收集到了全部的變量,如今能夠添加這一部分的代碼了,如下面的模板爲例:
<p>Welcome, {{user_name}}!</p>
<p>Products:</p>
<ul>
{% for product in product_list %}
<li>{{ product.name }}:
{{ product.price|format_price }}</li>
{% endfor %}
</ul>
複製代碼
這裏有三個變量 user_name
product_list
product
。 all_vars
集合會包含它們,由於它們被用在表達式和控制語句之中。
可是,最後只有 user_name
product_list
會被解構成局部變量,由於 product
是循環體內的局部變量:
for var_name in self.all_vars - self.loop_vars:
vars_code.add_line("c_%s = context[%r]" % (var_name, var_name))
複製代碼
到此,咱們代碼就都加入到 result
中了,最後將他們鏈接成字符串就大功告成了:
code.add_line("return ''.join(result)")
code.dedent()
複製代碼
經過 get_globals
咱們能夠獲得所建立的渲染函數,並將它保存到 _render_function
上:
self._render_function = code.get_globals()['render_function']
複製代碼
如今讓咱們來仔細的分析下表達式的編譯過程。
咱們的表達式能夠簡單到只有一個變量名:
{{user_name}}
複製代碼
也能夠很複雜:
{{user.name.localized|upper|escape}}
複製代碼
這些狀況, _expr_code
都會進行處理。同其餘語言中的表達式同樣,咱們的表達式是遞歸構建的:大表達式由更小的表達式組成。一個完整的表達式是由管道分隔的,其中第一個部分是由逗號分開的,等等。因此咱們的函數天然是遞歸的形式:
def _expr_code(self, expr):
"""Generate a Python expression for `expr`."""
複製代碼
第一種情形是表達式中有 |
。 這種狀況會以 |
作爲分隔符進行分隔,並將第一部分傳給 _expr_code
繼續求值。 剩下的每一部分都是一個函數,咱們能夠迭代求值,即前面函數的結果做爲後面函數的輸入。一樣,這裏要收集函數變量名以便後面進行解構。
if "|" in expr:
pipes = expr.split("|")
code = self._expr_code(pipes[0])
for func in pipes[1:]:
self._variable(func, self.all_vars)
code = "c_%s(%s)" % (func, code)
複製代碼
咱們的渲染函數中的變量都加了c_前綴,下同
第二種狀況是表達式中沒有 |
,可是有 .
。 則以 .
做爲分隔符分隔,第一部分傳給 _expr_code
求值,所得結果做爲 do_dots
的第一個參數。 剩下的部分都做爲 do_dots
的不定參數。
elif "." in expr:
dots = expr.split(".")
code = self._expr_code(dots[0])
args = ", ".join(repr(d) for d in dots[1:])
code = "do_dots(%s, %s)" % (code, args)
複製代碼
好比, x.y.z
會被解析成函數調用 do_dots(x, 'y', 'z')
最後一種狀況是什麼都不包含。這種比較簡單,直接返回帶前綴的變量:
else:
self._variable(expr, self.all_vars)
code = "c_%s" % expr
return code
複製代碼
def _syntax_error(self, msg, thing):
"""Raise a syntax error using `msg`, and showing `thing`."""
raise TempliteSyntaxError("%s: %r" % (msg, thing))
複製代碼
def _variable(self, name, vars_set):
"""Track that `name` is used as a variable. Adds the name to `vars_set`, a set of variable names. Raises an syntax error if `name` is not a valid name. """
if not re.match(r"[_a-zA-Z][_a-zA-Z0-9]*$", name):
self._syntax_error("Not a valid name", name)
vars_set.add(name)
複製代碼
前面咱們已經將模板編譯成了 python 代碼,渲染過程就很簡單了。咱們要作的就是獲得上下文,調用編譯後的函數:
def render(self, context=None):
"""Render this template by applying it to `context`. `context` is a dictionary of values to use in this rendering. """
# Make the complete context we'll use.
render_context = dict(self.context)
if context:
render_context.update(context)
return self._render_function(render_context, self._do_dots)
複製代碼
render
函數首先將初始傳入的數據和參數進行合併獲得最後的上下文數據,最後經過調用 _render_function
來獲得最後的結果。 最後,再來分析一下 _do_dots
:
def _do_dots(self, value, *dots):
"""Evaluate dotted expressions at runtime."""
for dot in dots:
try:
value = getattr(value, dot)
except AttributeError:
value = value[dot]
if callable(value):
value = value()
return value
複製代碼
前面說過,表達式 x.y.z
會被編譯成 do_dots(x, 'y', 'z')
。 下面以此爲例: 首先,將 y 做爲對象 x 的一個屬性嘗試求值。若是失敗,則將其做爲一個鍵求值。最後,若是 y 是可調用的,則進行調用。 而後,以獲得的 value 做爲對象繼續進行後面的相同操做。
爲了保持代碼的精簡,咱們還有不少功能有待實現: