130行代碼寫一個模板引擎

我理解的模板引擎

簡單來講,模板引擎就是先定義好一個模板,而後餵它數據,就會生成對應的html結構。 模板是一段預先定義好的字符串,是一個類html的結構,裏面穿插着一些控制語句(if、for等), 好比以下: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>

複製代碼

數據是json數據,喂的數據不一樣,生成的html也不一樣,如python

{
    '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:git

<p>Welcome, Jack!</p>
    Your name: Jack
<p>Fruits:</p>
<ul>
    <li>Apple:20</li>
    <li>Banana:22</li>
</ul>
複製代碼

這樣就體現了數據與視圖分離的思想,之後修改任何一方都是很方便的。github

要作的事

咱們要作的事情就是根據已知的模板和數據來生成對應的html,因而咱們能夠定義這樣一個函數,該函數有兩個參數和一個返回值, 參數分別對應模板數據,而返回值對應最終的html。函數原型以下:正則表達式

def TemplateEngine(template, context):
    ...
    return html_data
複製代碼

template是str類型,context是dict類型,html_data也是str類型express

支持的語法

因爲我在工做中常常用到Django開發,因此我對Django的模板引擎比較熟悉,這裏就採用Django支持的語法來說解。其實說白了,大致上就 兩種語法,{{ }}{% %}{{ }}裏面包含的是一個(變)量,數據來自於context,整個會被context裏面對應的數據替換掉, 如前面的例子{{ user_name }}最終會被替換成Jack。{% %}是控制結構,有四種:{% if %}{% for %}{% endif %}{% endfor %}{% if %}{% endif %}必須成對出現,同理,{% for %}、{% endfor %}也必須成對出現。json

實現思路

大致上實現一個模板引擎有三種方法,替換型、解釋型和編譯型。替換型就是簡單的字符串替換,如{{ user_name }}被替換成Jack,對應 以下代碼:app

'{user_name}'.format(user_name = 'Jack')

複製代碼

這種最簡單,通常來講運行效率也最低。解釋型和編譯型都是生成對應的(python)代碼,而後直接運行這個代碼來生成最終的html,這個實現難度 相對替換型來講複雜了一點。本篇先只講替換型。less

總的思路是這樣:咱們從最外層按照普通字符串、{{ }}、{% %}將模板切塊,而後遞歸處理每個塊,最後將每個子塊的結果拼接起來。 關鍵詞依次是:切塊、遞歸處理、拼接。咱們依次來說解每一個步驟。函數

切塊

仍是舉前面那個例子,

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

相關文章
相關標籤/搜索