模板引擎-模板引擎漸進進化

模板渲染

在web開發的過程中,現在比較多的開發方式都是前後端分離開發的方式來進行的,這樣讓前後端的開發更加解耦、前後端解耦開發的效率會比較高。在一些簡單的場景或者項目要求比較簡單的情況下,模板渲染的方式來渲染前端頁面的方式,會是一個比較簡單快速的方式來實現前端的展示,特別在一些監控系統或者壓測工具中,前端基本上都是通過一個簡單的頁面渲染,來進行數據的展示例如openfalcon的dashborad。在另一種場景下,比如需要動態的生成一些項目的示例文件的時候也可以使用模板渲染的方式,例如django在startproject的時候就是通過模板渲染的方式來生成一個基礎的project。

模板渲染-支持字典替換

模板渲染的初衷就是想動態的根據不同的輸入渲染出不同的結果,本質就是字符串的更改替換。

"hello {{name}}" =={"name": "world"} => "hello world"
"hello {{name}}" =={"name": "china"} => "hello china"

最基礎的功能就想實現簡單的文字替換功能。

import re


class Templite(object):

    def __init__(self, text, *contexts):
        self.text = text
        self.context = {}
        self.result = []
        for context in contexts:
            self.context.update(context)

    def _syntax_error(self, msg, thing):
        raise Exception("{0}  {1}".format(msg, thing))

    def render(self, context=None):
        if context:
            self.context.update(context)
        tokens = re.split(r"(?s)({{.*?}})", self.text)
        for token in tokens:
            if token.startswith("{{"):
                words = token[2:-2].strip().split(" ")
                if len(words) != 1:
                    self._syntax_error("Too Many words", token)
                if words[0] not in self.context:
                    self._syntax_error("You should set  {0}  in context ".format(words[0]), words[0])
                self.result.append(self.context[words[0]])
            else:
                self.result.append(token)
        return "".join(self.result)


if __name__ == '__main__':
    text = """
hello {{ name }}
    """
    t = Templite(
        text
    )
    print(t.render({"name": "world"}))

該段代碼就是簡單的將字符串進行了正則匹配,好像最基礎的模板功能就這樣實現了,通過簡單的更改就支持了特定的{{name}}的格式的替換,此時如果我們還想做個擴展如果此時輸入的渲染的不僅僅是一個簡單的字典呢。

模板渲染-支持屬性查找

class Show(object):
	show = None
	def __init__(self, show):
		self.show = show
s = Show("owner show")
"hello {{name.show}}" =={"name": s}==> "hello owner show"

此時再繼續改造渲染的方法如下;

import re


class Templite(object):

    def __init__(self, text, *contexts):
        self.text = text
        self.context = {}
        self.result = []
        for context in contexts:
            self.context.update(context)

    def _syntax_error(self, msg, thing):
        raise Exception("{0}  {1}".format(msg, thing))

    def _expr_code(self, expr):
        if "." in expr:
            words = expr.split(".")
            if words[0] not in self.context:
                self._syntax_error("Var {0} must in context".format(words[0]), words)
            value = self.context[words[0]]
            for v in words[1:]:
                try:
                    value = getattr(value, v)
                except AttributeError:
                    value = value[v]
                if callable(value):
                    value = value()
            return value
        else:
            if expr not in self.context:
                self._syntax_error("You should set  {0}  in context ".format(expr), expr)
            return self.context[expr]

    def render(self, context=None):
        if context:
            self.context.update(context)
        tokens = re.split(r"(?s)({{.*?}})", self.text)
        for token in tokens:
            if token.startswith("{{"):
                words = token[2:-2].strip().split(" ")
                if len(words) != 1:
                    self._syntax_error("Too Many words", token)
                self.result.append(self._expr_code(words[0]))
            else:
                self.result.append(token)
        return "".join(self.result)


if __name__ == '__main__':
    text = """
hello {{ name.show }}
    """
    t = Templite(
        text
    )

    class Show(object):
        show = None
        def __init__(self, show):
            self.show = show

    show = Show("owner show")
    print(t.render({"name": show}))

此時渲染的數據就是通過擴展{{}}的模式方式來擴展了模板渲染的效果,通過循環遞歸調用.對應的屬性來獲取最終的屬性值。

模板渲染-支持管道過濾

有時候需要對渲染的字符串在真正渲染的時候進行一定的處理,比如在輸入的時候我想統一通過自定義的函數來保證渲染的數據都是大寫或者都是小寫。

"hello {{name}}" =={"name": "wORLd"}==通過處理都變爲小寫==> "hello world"

"hello {{name|to_lower}}" =={"name": "wORLd", "to_lower": str.lower} ==> "hello world"

此時修改代碼

import re


class Templite(object):

    def __init__(self, text, *contexts):
        self.text = text
        self.context = {}
        self.result = []
        for context in contexts:
            self.context.update(context)

    def _syntax_error(self, msg, thing):
        raise Exception("{0}  {1}".format(msg, thing))

    def _expr_code(self, expr):
        if "." in expr:
            words = expr.split(".")
            if words[0] not in self.context:
                self._syntax_error("Var {0} must in context".format(words[0]), words)
            value = self.context[words[0]]
            for v in words[1:]:
                try:
                    value = getattr(value, v)
                except AttributeError:
                    value = value[v]
                if callable(value):
                    value = value()
            return value
        elif "|" in expr:
            words = expr.split("|")
            if words[0] not in self.context:
                self._syntax_error("Var {0} must in context".format(words[0]), words)
            value = self.context[words[0]]
            for v in words[1:]:
                if v not in self.context:
                    self._syntax_error("Var {0} callback func must in context".format(v), words)
                value = self.context[v](value)
            return value
        else:
            if expr not in self.context:
                self._syntax_error("You should set  {0}  in context ".format(expr), expr)
            return self.context[expr]

    def render(self, context=None):
        if context:
            self.context.update(context)
        tokens = re.split(r"(?s)({{.*?}})", self.text)
        for token in tokens:
            if token.startswith("{{"):
                words = token[2:-2].strip().split(" ")
                if len(words) != 1:
                    self._syntax_error("Too Many words", token)
                self.result.append(self._expr_code(words[0]))
            else:
                self.result.append(token)
        return "".join(self.result)


if __name__ == '__main__':
    text = """
hello {{ name|to_lower }}
    """
    t = Templite(
        text
    )

    print(t.render({"name": "wORLd", "to_lower": str.lower}))

此時對於單個渲染的處理基本上已經滿足了需求,滿足了多層的對象調用,也可以通過類似於管道的模式來進行數據的過濾等操作。

模板渲染-支持條件渲染

在模板渲染的過程中,有時候想根據不同的輸入條件來渲染出不同的結果,此時引入新的語法{% %}。

"hello {% if name %} {{name}}{% endif %}" =={"name": "if_test_name"} ==> "hello if_test_name"
"hello {%if name %} {{name}} {% endif %}" =={"name": ""} ==> "hello"
import re


class Templite(object):

    def __init__(self, text, *contexts):
        self.text = text
        self.context = {}
        self.result = []
        for context in contexts:
            self.context.update(context)

    def _syntax_error(self, msg, thing):
        raise Exception("{0}  {1}".format(msg, thing))

    def _expr_code(self, expr):
        if "." in expr:
            words = expr.split(".")
            if words[0] not in self.context:
                self._syntax_error("Var {0} must in context".format(words[0]), words)
            value = self.context[words[0]]
            for v in words[1:]:
                try:
                    value = getattr(value, v)
                except AttributeError:
                    value = value[v]
                if callable(value):
                    value = value()
            return value
        elif "|" in expr:
            words = expr.split("|")
            if words[0] not in self.context:
                self._syntax_error("Var {0} must in context".format(words[0]), words)
            value = self.context[words[0]]
            for v in words[1:]:
                if v not in self.context:
                    self._syntax_error("Var {0} callback func must in context".format(v), words)
                value = self.context[v](value)
            return value
        else:
            if expr not in self.context:
                self._syntax_error("You should set  {0}  in context ".format(expr), expr)
            return self.context[expr]

    def render(self, context=None):
        if context:
            self.context.update(context)
        tokens = re.split(r"(?s)({{.*?}}|{%.*?%})", self.text)
        op_stack = []
        for token in tokens:
            if token.startswith("{{"):
                words = token[2:-2].strip().split(" ")
                if len(words) != 1:
                    self._syntax_error("Too Many words", token)
                if op_stack:
                    value = op_stack[-1]
                    value["buffered"].append(self._expr_code(words[0]))
                else:
                    self.result.append(self._expr_code(words[0]))
            elif token.startswith("{%"):
                words = token[2:-2].strip().split()
                if words[0] == "if":
                    if len(words) != 2:
                        self._syntax_error("IF condition must len 2", words)
                    op_stack.append({
                        "tag": "if",
                        "data": self._expr_code(words[1]),
                        "buffered": []
                    })
                elif words[0].startswith("end"):
                    if len(words) != 1:
                        self._syntax_error("END condition must len 1", words)
                    end_what = words[0][3:]
                    if not op_stack:
                        self._syntax_error("not enough stack", op_stack)
                    start_what = op_stack.pop()
                    if end_what != start_what["tag"]:
                        self._syntax_error("error end tag  ", start_what)
                    if start_what["tag"] == "if":
                        if start_what["data"]:
                            if op_stack:
                                value = op_stack[-1]
                                value["buffered"].extend(start_what["buffered"])
                            else:
                                self.result.extend(start_what["buffered"])

            else:
                if op_stack:
                    value = op_stack[-1]
                    value["buffered"].append(token)
                else:
                    self.result.append(token)
        return "".join(self.result)


if __name__ == '__main__':
    text = """
hello {% if name %}
    {{ name }}
    {% if sub %}
        {{ sub }}
    {% endif %}
{% endif %}
    """
    t = Templite(
        text
    )

    print(t.render({"name": "if_test_name", "sub": "subss"}))

此時,通過添加對if的支持,從而能夠根據不同的輸入來進行不同的進行展示,主要通過條件來進行渲染,渲染的條件也可以嵌套渲染,從而完成對模板的渲染。在以上的改進中,無論是if或者直接渲染都沒有設計到代碼塊中的變量作用域的影響,都是全局的作用域,即所有解析出來的渲染結果都是通過全局的context來保存的。此時我們再繼續拓展模板渲染的功能。

模板渲染-支持選好渲染

假如我們想支持在代碼中for循環來進行數據的渲染,如下所示;

"{% for n in nums %}{{n}}{% endfor %}" =={"nums": [1, 2]}==> "12"
import re
import copy


class Templite(object):

    def __init__(self, text, *contexts):
        self.text = text
        self.context = {}
        self.result = []
        for context in contexts:
            self.context.update(context)

    def _syntax_error(self, msg, thing):
        raise Exception("{0}  {1}".format(msg, thing))

    def _expr_code(self, expr, context=None):
        if context is None:
            context = self.context
        if "." in expr:
            words = expr.split(".")
            if words[0] not in context:
                self._syntax_error("Var {0} must in context".format(words[0]), words)
            value = context[words[0]]
            for v in words[1:]:
                try:
                    value = getattr(value, v)
                except AttributeError:
                    value = value[v]
                if callable(value):
                    value = value()
            return value
        elif "|" in expr:
            words = expr.split("|")
            if words[0] not in context:
                self._syntax_error("Var {0} must in context".format(words[0]), words)
            value = context[words[0]]
            for v in words[1:]:
                if v not in context:
                    self._syntax_error("Var {0} callback func must in context".format(v), words)
                value = context[v](value)
            return value
        else:
            if expr not in context:
                self._syntax_error("You should set  {0}  in context ".format(expr), expr)
            return context[expr]

    def render(self, context=None):
        if context:
            self.context.update(context)
        tokens = re.split(r"(?s)({{.*?}}|{%.*?%})", self.text)
        op_stack = []
        length = len(tokens)
        i = 0
        while i < length:
            token = tokens[i]
            i += 1
            if token.startswith("{{"):
                words = token[2:-2].strip().split(" ")
                if len(words) != 1:
                    self._syntax_error("Too Many words", token)
                if op_stack:
                    value = op_stack[-1]
                    value["buffered"].append(self._expr_code(words[0]))
                else:
                    self.result.append(self._expr_code(words[0]))
            elif token.startswith("{%"):
                words = token[2:-2].strip().split()
                if words[0] == "if":
                    if len(words) != 2:
                        self._syntax_error("IF condition must len 2", words)
                    op_stack.append({
                        "tag": "if",
                        "data": self._expr_code(words[1]),
                        "buffered": []
                    })
                elif words[0] == "for":
                    if len(words) != 4:
                        self._syntax_error("FOR condition must len 4", words)
                    if words[2] != "in":
                        self._syntax_error("FOR must need in", words[2])
                    # 渲染代碼
                    for_op = []
                    count_for = 0
                    while i < length:
                        n_token = tokens[i]
                        i += 1
                        c_words = n_token[2:-2].strip().split()
                        if c_words and c_words[0] == "endfor":
                            if count_for == 0:
                                break
                            else:
                                count_for -= 1
                        if c_words and c_words[0] == "for":
                            count_for += 1
                        for_op.append(n_token)
                    value = self._expr_code(words[3])
                    n_context = {}
                    for k in self.context:
                        if k == words[3]:
                            continue
                        n_context[k] = self.context[k]
                    for_res = []
                    detail = "".join(for_op)
                    for v in value:
                        if isinstance(v, int):
                            v = str(v)
                        n_context[words[1]] = v

                        tmp = Templite(detail)
                        for_res.append(tmp.render(n_context))
                    self.result.extend(for_res)

                elif words[0].startswith("end"):
                    if len(words) != 1:
                        self._syntax_error("END condition must len 1", words)
                    end_what = words[0][3:]
                    if not op_stack:
                        self._syntax_error("not enough stack", op_stack)
                    start_what = op_stack.pop()
                    if end_what != start_what["tag"]:
                        self._syntax_error("error end tag  ", start_what)
                    if start_what["tag"] == "if":
                        if start_what["data"]:
                            if op_stack:
                                value = op_stack[-1]
                                value["buffered"].extend(start_what["buffered"])
                            else:
                                self.result.extend(start_what["buffered"])
            else:
                if op_stack:
                    value = op_stack[-1]
                    value["buffered"].append(token)
                else:
                    self.result.append(token)

        return "".join(self.result)


if __name__ == '__main__':
    text = """
hello {{ w }}
hello {% if name %}
    {{ name }}
    {% if sub %}
        {{ sub }}
    {% endif %}
{% endif %}

{% for n in nums %}
    {{ n }}
    {% for j in numst %}
        {{ j }}
        {{ name }}
    {% endfor %}
{% endfor %}
    """
    t = Templite(
        text
    )

    print(t.render({"name": "if_test_name", "sub": "subss", "w": "werwer", "nums": [1, 2], "numst": {3, 4}}))

此時,支持的for循環(實例代碼可能有其他bug大家可自行修改),主要的思路就是將每個for循環進行拆分,for循環中對應包含了全局的變量,而且每個for中對應的局部變量也只在循環當中有效,此時將每個for循環檢查出來並重新初始化一個Templite來解析出數據,然後將數據拼接到一起,從而完成for循環的渲染。

總結

本文只是做了一個簡單的探索,模板渲染其實都是對字符串的解析替換的工作,主要對數據進行了不同規則的替換,本文的思路其實是最簡單直白的思路,直接挨個解析不同的規則,但是這種效率其實相對比較差,因爲在render的過程中有大量的字符串的拼接操作,僅作爲一個實現的思路來考慮。在Python中更好的一個模板實現的思路其實可以參考大神編寫的模板實現的 實例代碼,實例中使用的方式其實就是通過字符串拼接成一個函數,然後通過exec來執行該函數來進行不同的條件或者循環判斷等條件,後續有機會可以深入查看一下。由於本人才疏學淺,如有錯誤請批評指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章