我理解的模板引擎
簡單來說,模板引擎就是先定義好一個模板,然後餵它資料,就會生成對應的html結構。 模板是一段預先定義好的字串,是一個類html的結構,裡面穿插著一些控制語句(if、for等), 比如如下:
<p>Welcome, {{ user_name }}!</p>
{% if is_show %}
Your name: {{ user_name }}
{% endif %}
<p>Fruits:</p>
<ul>
{% for product in product_list %}
<li>{{ product.name }}:{{ product.price }}</li>
{% endfor %}
</ul>
複製程式碼
資料是json資料,喂的資料不同,生成的html也不同,如
{
'user_name': 'Jack',
'is_show': True,
'product_list': [
{
'show': True,
'name': 'Apple',
'price': 20
},
{
'show': False,
'name': 'Pear',
'price': 21
},
{
'show': True,
'name': 'Banana',
'price': 22
}
]
}
複製程式碼
就會生成如下所示的html:
<p>Welcome, Jack!</p>
Your name: Jack
<p>Fruits:</p>
<ul>
<li>Apple:20</li>
<li>Banana:22</li>
</ul>
複製程式碼
這樣就體現了資料與檢視分離的思想,以後修改任何一方都是很方便的。
要做的事
我們要做的事情就是根據已知的模板和資料來生成對應的html,於是我們可以定義這樣一個函式,該函式有兩個引數和一個返回值, 引數分別對應模板和資料,而返回值對應最終的html。函式原型如下:
def TemplateEngine(template, context):
...
return html_data
複製程式碼
template是str型別,context是dict型別,html_data也是str型別
支援的語法
由於我在工作中經常用到Django開發,所以我對Django的模板引擎比較熟悉,這裡就採用Django支援的語法來講解。其實說白了,大體上就 兩種語法,{{ }}和{% %}。{{ }}裡面包含的是一個(變)量,資料來自於context,整個會被context裡面對應的資料替換掉, 如前面的例子{{ user_name }}最終會被替換成Jack。{% %}是控制結構,有四種:{% if %}、{% for %}、{% endif %}、{% endfor %}。 {% if %}、{% endif %}必須成對出現,同理,{% for %}、{% endfor %}也必須成對出現。
實現思路
大體上實現一個模板引擎有三種方法,替換型、解釋型和編譯型。替換型就是簡單的字串替換,如{{ user_name }}被替換成Jack,對應 如下程式碼:
'{user_name}'.format(user_name = 'Jack')
複製程式碼
這種最簡單,一般來說執行效率也最低。解釋型和編譯型都是生成對應的(python)程式碼,然後直接執行這個程式碼來生成最終的html,這個實現難度 相對替換型來說複雜了一點。本篇先只講替換型。
總的思路是這樣:我們從最外層按照普通字串、{{ }}、{% %}將模板切塊,然後遞迴處理每一個塊,最後將每一個子塊的結果拼接起來。 關鍵詞依次是:切塊、遞迴處理、拼接。我們依次來講解每個步驟。
切塊
還是舉前面那個例子,
<p>Welcome, {{ user_name }}!</p>
{% if is_show %}
Your name: {{ user_name }}
{% endif %}
<p>Fruits:</p>
<ul>
{% for product in product_list %}
<li>{{ product.name }}:{{ product.price }}</li>
{% endfor %}
</ul>
複製程式碼
為了處理方便,我們把模板切得儘可能的碎,使得每一個小塊都是普通字串塊、{{ }}塊、{% if %}塊、{% endif %}塊、{% for %}塊、{% endfor %}塊中的 一種,如上述模板切成:
['<p>Welcome, ', '{{ user_name }}', '!</p>', '{% if is_show %}', 'Your name: ', '{{ user_name }}', '{% endif %}', '<p>Fruits:</p><ul>',
'{% for product in product_list %}', '<li>', '{{ product.name }}', ':', '{{ product.price }}', '</li>', '{% endfor %}', '</ul>']
複製程式碼
要把模板(str型別)切成如上圖所示(list型別),讀者立馬會想到使用split函式,沒錯。但是這裡最好使用正規表示式的split函式re.split,程式碼如下:
tokens = re.split(r"(?s)({{.*?}}|{%.*?%})", template)
複製程式碼
遞迴處理
在上一節(切塊)我們已經得到了一個list,本節我們只需遍歷它即可。我們遍歷那個list,如果是普通塊並且沒有被{% if %}塊和{% for %}塊包圍,則我們直接將值pusth到最終 的結果中;同理,如果是{{ }}塊並且沒有被{% if %}塊和{% for %}塊包圍,則我們呼叫VarEngine來解析這個{{ }}塊,並且把解析結果pusth到最終的結果中;如果是{% if %}塊, 則我們先不急求值,而是將它push到一個棧中,並且將之後的塊也push到這個棧中,直到遇到對應的{% endif %}塊。遇到了{% endif %}塊之後,我們就呼叫IfBlock來解析這個 棧,並且把解析結果pusth到最終的結果中;跟{% if %}塊類似,如果遍歷到了{% for %}塊,則我們將{% for %}塊push到一個棧中,然後將之後的塊也push到這個棧中,直到遇到對應的 {% endfor %}塊,遇到了{% endfor %}塊之後,我們就呼叫ForBlock來解析這個棧,並且把解析結果pusth到最終的結果中。程式碼(剪輯後)如下:
def recursive_traverse(lst, context):
stack, result = [], []
is_if, is_for, times, match_times = False, False, 0, 0
for item in lst:
if item[:2] != '{{' and item[:2] != '{%':
# 普通塊的處理
result.append(item) if not is_if and not is_for else stack.append(item)
elif item[:2] == '{{':
# {{ }}塊的處理
result.append(VarEngine(item[2:-2].strip(), context).result) if not is_if and not is_for else stack.append(item)
elif item[:2] == '{%':
expression = item[2:-2]
expression_lst = expression.split(' ')
expression_lst = [it for it in expression_lst if it]
if expression_lst[0] == 'if':
# {% if %}塊的處理
stack.append(item)
if not is_for:
is_if = True
times += 1
elif expression_lst[0] == 'for':
# {% for %}塊的處理
stack.append(item)
if not is_if:
is_for = True
times += 1
if expression_lst[0] == 'endif':
# {% endif %}塊的處理
stack.append(item)
if not is_for:
match_times += 1
if match_times == times:
result.append(IfBlock(context, stack).result)
del stack[:]
is_if, is_for, times, match_times = False, False, 0, 0
elif expression_lst[0] == 'endfor':
# {% endfor %}塊的處理
stack.append(item)
if not is_if:
match_times += 1
if match_times == times:
result.append(ForBlock(context, stack).result)
del stack[:]
is_if, is_for, times, match_times = False, False, 0, 0
複製程式碼
result是一個list,是最終的結果
拼接
通過遞迴處理那一節,我們已經把各個塊的執行結果都存放到了列表result中,最後使用join函式將列表轉換成字串就得到了最終的結果。
return ''.join(result)
複製程式碼
各個引擎的實現
在遞迴處理那一節,我們用到了幾個類VarEngine、IfBlock和ForBlock,分別用來處理{{ }}塊、{% if %}塊組成的棧、{% for %}塊組成的棧。 下面來說明一下這幾個引擎的實現。
VarEngine的實現
先直接上程式碼
class VarEngine(Engine):
def _do_vertical_seq(self, key_words, context):
k_lst = key_words.split('|')
k_lst = [item.strip() for item in k_lst]
result = self._do_dot_seq( k_lst[0], context)
for filter in k_lst[1:]:
func = self._do_dot_seq(filter, context, True)
result = func(result)
return result
def __init__(self, k, context):
self.result = self._do_vertical_seq(k, context) if '|' in k else self._do_dot_seq(k, context)
複製程式碼
這裡主要是要注意處理.和|,|表示過濾器,.最常用的表示一個物件的屬性,如
{{ person.username | format_name }}
複製程式碼
person可能表示一個物件,也可能表示一個類例項,username是它的屬性,format_name是一個過濾器(函式),表示將處理左邊給的值(這裡是username)並返回 處理後的值。更復雜一點的,可能有如下的{{ }}塊,如
{{ info1.info2.person.username | format_name1 | format_name2 | format_name3 }}
複製程式碼
VarEngine類繼承自Engine類,_do_dot_seq在Engine類中定義:
class Engine(object):
def _do_dot(self, key_words, context, stay_func = False):
if isinstance(context, dict):
if key_words in context:
return context[key_words]
raise KeyNotFound('{key} is not found'.format(key=key_words))
value = getattr(context, key_words)
if callable(value) and not stay_func:
value = value()
return value
def _do_dot_seq(self, key_words, context, stay_func = False):
if not '.' in key_words:
return self._do_dot(key_words, context, stay_func)
k_lst = key_words.split('.')
k_lst = [item.strip() for item in k_lst]
result = context
for item in k_lst:
result = self._do_dot(item, result, stay_func)
return repr(result)
複製程式碼
_do_dot函式主要是用來處理.(點)情形的,如{{ person.name }},返回結果。有三個引數:key_words、context和stay_func,key_words 是屬性名,如name;context對應上下文(或物件、類例項等),如person;stay_func是如果屬性是一個函式的話,是否要執行這個函式。 程式碼很簡單,就講到這裡。
IfBlock的實現
class IfEngine(Engine):
def __init__(self, key_words, context):
k_lst = key_words.split(' ')
k_lst = [item.strip() for item in k_lst]
if len(k_lst) % 2 == 1:
raise IfNotValid
for item in k_lst[2::2]:
if item not in ['and', 'or']:
raise IfNotValid
cond_lst = k_lst[1:]
index = 0
while index < len(cond_lst):
cond_lst[index] = str(self._do_dot_seq(cond_lst[index], context))
index += 2
self.cond = eval(' '.join(cond_lst))
class IfBlock(object):
def __init__(self, context, key_words):
self.result = '' if not IfEngine(key_words[0][2:-2].strip(), context).cond else recursive_traverse(key_words[1:-1], context)
複製程式碼
IfBlock的邏輯也很簡單,就是先判斷if條件是否為真(通過IfEngine判斷),如果為真,則遞迴下去(呼叫recursive_traverse),如果為假,則直接返回空 字串。這裡稍微講下IfEngine的實現,主要是對and、or的處理,用到了eval函式,這個函式會執行裡面的字串,如eval('True and True and True')會返回True。
ForBlock的實現
class ForBlock(Engine):
def __init__(self, context, key_words):
for_engine = key_words[0][2:-2].strip()
for_engine_lst = for_engine.split(' ')
for_engine_lst = [item.strip() for item in for_engine_lst]
if len(for_engine_lst) != 4:
raise ForNotValid
if for_engine_lst[0] != 'for' or for_engine_lst[2] != 'in':
raise ForNotValid
iter_obj = self._do_dot_seq(for_engine_lst[3], context)
self.result = ''
for item in iter_obj:
self.result += recursive_traverse(key_words[1:-1], {for_engine_lst[1]:item})
複製程式碼
這裡採用了Python的for語法for...in...,如{% for person in persons %},同IfBlock類似,這裡也採用了遞迴(呼叫recursive_traverse)實現。
總結
本文用130行程式碼實現了一個模板引擎,總的思路還是很簡單,無非就是依次處理各個block,最後將各個block的處理結果拼接(join)起來。關鍵是基本功要 紮實,如遞迴、正規表示式等,此外,Python的常用(內建)函式也要搞清楚,如repr、eval(雖然不推薦使用,但是要了解)、str.join、getattr、callable等等,這些 函式可以幫助你達到事半功倍的效果。 本節的原始碼都在github上,歡迎給個star。