模板渲染
在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来执行该函数来进行不同的条件或者循环判断等条件,后续有机会可以深入查看一下。由于本人才疏学浅,如有错误请批评指正。