摘要:使用技術(shù)我們將使用將模板編譯為代碼的方式來(lái)解析和渲染模板。下面我們就一起來(lái)實(shí)現(xiàn)這個(gè)方法。
假設(shè)我們要生成下面這樣的 html 字符串:
welcome, Tom
- age: 20
- weight: 100
- height: 170
要求姓名以及 中的內(nèi)容是根據(jù)變量動(dòng)態(tài)生成的,也就是這樣的:
welcome, {name}
{info}
沒(méi)接觸過(guò)模板的同學(xué)可能會(huì)想到使用字符串格式化的方式來(lái)實(shí)現(xiàn):
HTML = """""" def gen_html(person): name = person["name"] info_list = [ "welcome, {name}
{info}
這種方案有一個(gè)很明顯的問(wèn)題那就是,需要拼接兩個(gè) html 片段。 使用過(guò)模板技術(shù)的同學(xué)應(yīng)該很容易就想到,在 Web 開(kāi)發(fā)中生成 HTML 的更常用的辦法是使用模板:
HTML = """""" def gen_html(person): return Template(HTML).render({"person": person})welcome, {{ person["name"] }}
{% for item, value in person["info"].items() %}
- {{ item }}: {{ value }}
{% endfor %}
本系列文章要講的就是如何從零開(kāi)始實(shí)現(xiàn)一個(gè)這樣的模板引擎( Template )。
使用技術(shù)我們將使用將模板編譯為 python 代碼的方式來(lái)解析和渲染模板。 比如上面的模板將被編譯為如下 python 代碼:
def render_function(): result = [] result.extend([ "", "" ]) return "".join(result)welcome, " str(person["name"]), "
", "" ]) for item, value in person["info"].items(): result.extend([ "
" "- ", str(item), ": ", str(value), "
" ]) result.extend([ "
然后通過(guò) exec 執(zhí)行生成的代碼,之后再執(zhí)行 render_function() 就可以得到我們需要的 html 字符串了:
namespace = {"person": person} exec(code, namespace) render_function = namespace["render_function"] html = render_function()
模板引擎的核心技術(shù)就是這些了,下面讓我們一步一步的實(shí)現(xiàn)它吧。
CodeBuilder我們都知道 python 代碼是高度依賴(lài)縮進(jìn)的,所以我們需要一個(gè)對(duì)象用來(lái)保存我們生成代碼時(shí)的當(dāng)前縮進(jìn)情況, 同時(shí)也保存已經(jīng)生成的代碼行(可以直接在 github 上下載 template1a.py ):
# -*- coding: utf-8 -*- # tested on Python 3.5.1 class CodeBuilder: INDENT_STEP = 4 # 每次縮進(jìn)的空格數(shù) def __init__(self, indent=0): self.indent = indent # 當(dāng)前縮進(jìn) self.lines = [] # 保存一行一行生成的代碼 def forward(self): """縮進(jìn)前進(jìn)一步""" self.indent += self.INDENT_STEP def backward(self): """縮進(jìn)后退一步""" self.indent -= self.INDENT_STEP def add(self, code): self.lines.append(code) def add_line(self, code): self.lines.append(" " * self.indent + code) def __str__(self): """拼接所有代碼行后的源碼""" return " ".join(map(str, self.lines)) def __repr__(self): """方便調(diào)試""" return str(self)
forward 和 backward 方法可以用來(lái)控制縮進(jìn)前進(jìn)或后退一步,比如在生成 if 語(yǔ)句的時(shí)候:
if age > 13: # 生成完這一行以后,需要切換縮進(jìn)了 ``forward()`` ... ... # 退出 if 語(yǔ)句主體的時(shí)候,同樣需要切換一次縮進(jìn) ``backward()`` ...Template
這個(gè)模板引擎的核心部分就是一個(gè) Template 類(lèi),用法:
# 實(shí)例化一個(gè) Template 對(duì)象 template = Template("""hello, {{ name }}
{% for skill in skills %}you are good at {{ skill }}.
{% endfor %} """) # 然后,使用一些數(shù)據(jù)來(lái)渲染這個(gè)模板 html = template.render( {"name": "Eric", "skills": ["python", "english", "music", "comic"]} )
一切魔法都在 Template 類(lèi)里。下面我們寫(xiě)一個(gè)基本的 Template 類(lèi)(可以直接在 github 上下載 template1b.py ):
class Template: def __init__(self, raw_text, indent=0, default_context=None, func_name="__func_name", result_var="__result"): self.raw_text = raw_text self.default_context = default_context or {} self.func_name = func_name self.result_var = result_var self.code_builder = code_builder = CodeBuilder(indent=indent) self.buffered = [] # 生成 def __func_name(): code_builder.add_line("def {}():".format(self.func_name)) code_builder.forward() # 生成 __result = [] code_builder.add_line("{} = []".format(self.result_var)) self._parse_text() self.flush_buffer() # 生成 return "".join(__result) code_builder.add_line("return "".join({})".format(self.result_var)) code_builder.backward() def _parse_text(self): pass def flush_buffer(self): # 生成類(lèi)似代碼: __result.extend(["", name, "
"]) line = "{0}.extend([{1}])".format( self.result_var, ",".join(self.buffered) ) self.code_builder.add_line(line) self.buffered = [] def render(self, context=None): namespace = {} namespace.update(self.default_context) if context: namespace.update(context) exec(str(self.code_builder), namespace) result = namespace[self.func_name]() return result
以上就是 Template 類(lèi)的核心方法了。我們之后要做的就是實(shí)現(xiàn)和完善 _parse_text 方法。 當(dāng)模板字符串為空時(shí)生成的代碼如下:
>>> import template1b >>> template = template1b.Template("") >>> template.code_builder def __func_name(): __result = [] __result.extend([]) return "".join(__result)
可以看到跟上面[使用技術(shù)]那節(jié)所說(shuō)生成的代碼是類(lèi)似的。下面我們就一起來(lái)實(shí)現(xiàn)這個(gè) _parse_text 方法。
變量首先要實(shí)現(xiàn)是對(duì)變量的支持,模板語(yǔ)法是 {{ variable }} 。 既然要支持變量,首先要做的就是把變量從模板中找出來(lái),這里我們可以使用正則表達(dá)式來(lái)實(shí)現(xiàn):
re_variable = re.compile(r"{{ .*? }}") >>> re_variable = re.compile(r"{{ .*? }}") >>> re_variable.findall("{{ title }}
") ["{{ title }}"] >>>
知道了如何匹配變量語(yǔ)法,下面我們要把變量跟其他的模板字符串分割開(kāi)來(lái),這里還是用的 re:
>> re_variable = re.compile(r"({{ .*? }})") >>> re_variable.split("{{ title }}
") ["", "{{ title }}", "
"]
這里的正則之所以加了個(gè)分組是因?yàn)槲覀兺瑫r(shí)還需要用到模板里的變量。 分割開(kāi)來(lái)以后我們就可以對(duì)每一項(xiàng)進(jìn)行解析了。支持 {{ variable }} 語(yǔ)法的 Template 類(lèi)增加了如下代碼 (可以直接在 github 上下載 template1c.py ):
class Template: def __init__(self, raw_text, indent=0, default_context=None, func_name="__func_name", result_var="__result"): # ... self.buffered = [] self.re_variable = re.compile(r"{{ .*? }}") self.re_tokens = re.compile(r"({{ .*? }})") # 生成 def __func_name(): code_builder.add_line("def {}():".format(self.func_name)) # ... def _parse_text(self): tokens = self.re_tokens.split(self.raw_text) for token in tokens: if self.re_variable.match(token): variable = token.strip("{} ") self.buffered.append("str({})".format(variable)) else: self.buffered.append("{}".format(repr(token)))
_parse_text 中之所以要用 repr ,是因?yàn)榇藭r(shí)需要把 token 當(dāng)成一個(gè)普通的字符串來(lái)處理, 同時(shí)需要考慮 token 中包含 " 和 " 的情況。 下面是幾種有問(wèn)題的寫(xiě)法:
"str({})".format(token): 這種是把 token 當(dāng)成變量來(lái)用了,生成的代碼為 str(token)
""{}"".format(token): 這種雖然是把 token 當(dāng)成了字符串,但是會(huì)有轉(zhuǎn)義的問(wèn)題,當(dāng) token 中包含 " 時(shí)生成的代碼為 ""hello""
下面先來(lái)看一下新的 template1c.py 生成了什么樣的代碼:
>>> from template1c import Template >>> template = Template("{{ title }}
") >>> template.code_builder def __func_name(): __result = [] __result.extend(["",str(title),"
"]) return "".join(__result)
沒(méi)問(wèn)題,跟預(yù)期的是一樣的。再來(lái)看一下 render 的效果:
>>> template.render({"title": "Python"}) "Python
"
不知道你有沒(méi)有發(fā)現(xiàn),其實(shí) {{ variable }} 不只支持變量,還支持表達(dá)式和運(yùn)算符:
>>> Template("{{ 1 + 2 }}").render() "3" >>> Template("{{ items[0] }}").render({"items": [1, 2, 3]}) "1" >>> Template("{{ func() }}").render({"func": list}) "[]"
這個(gè)既可以說(shuō)是個(gè) BUG 也可以說(shuō)是個(gè)特性?, 看模板引擎是否打算支持這些功能了, 我們?cè)谶@里是打算支持這些功能 ;)。
既然支持了 {{ }} 那么支持注釋也就非常好實(shí)現(xiàn)了。
注釋打算支持的注釋模板語(yǔ)法是 {# comments #} ,有了上面實(shí)現(xiàn) {{ variable }} 的經(jīng)驗(yàn),實(shí)現(xiàn)注釋是類(lèi)似的代碼 (可以直接在 github 上下載 template1d.py ):
class Template: def __init__(self, raw_text, indent=0, default_context=None, func_name="__func_name", result_var="__result"): # ... self.buffered = [] self.re_variable = re.compile(r"{{ .*? }}") self.re_comment = re.compile(r"{# .*? #}") self.re_tokens = re.compile(r"""( (?:{{ .*? }}) |(?:{# .*? #}) )""", re.X) # 生成 def __func_name(): # ... def _parse_text(self): tokens = self.re_tokens.split(self.raw_text) for token in tokens: if self.re_variable.match(token): # ... # 注釋 {# ... #} elif self.re_comment.match(token): continue else: # ...
效果:
>>> from template1d import Template >>> template = Template("{{ title }} {# comment #}
") >>> template.code_builder def __func_name(): __result = [] __result.extend(["",str(title)," ","
"]) return "".join(__result) >>> template.render({"title": "Python"}) "Python
"
至此,我們的模板引擎已經(jīng)支持了變量和注釋功能。 那么如何實(shí)現(xiàn)支持 if 語(yǔ)句和 for 循環(huán)的標(biāo)簽語(yǔ)法呢:
{% if user.is_admin %} admin, {{ user.name }} {% elif user.is_staff %} staff {% else %} others {% endif %} {% for name in names %} {{ name }} {% endfor %}
我將在 第二篇文章 中向你詳細(xì)的講解。敬請(qǐng)期待。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/37826.html
摘要:在本文中我們將解決一些用于生成的模板引擎需要面對(duì)的一些安全問(wèn)題。整個(gè)系列的所有文章地址讓我們一起來(lái)構(gòu)建一個(gè)模板引擎一讓我們一起來(lái)構(gòu)建一個(gè)模板引擎二讓我們一起來(lái)構(gòu)建一個(gè)模板引擎三讓我們一起來(lái)構(gòu)建一個(gè)模板引擎四文章中涉及的代碼已經(jīng)放到上了 在 上篇文章 中我們的模板引擎實(shí)現(xiàn)了對(duì) include 和 extends 的支持, 到此為止我們已經(jīng)實(shí)現(xiàn)了模板引擎所需的大部分功能。 在本文中我們將解...
摘要:原作者唐斌騰訊什么原名是一個(gè)簡(jiǎn)單易用的前端模板預(yù)編譯工具。本文作者為來(lái)自騰訊團(tuán)隊(duì)的唐斌,他在本文中為我們分析了傳統(tǒng)前端模板內(nèi)嵌的弊端,如開(kāi)發(fā)調(diào)試效率低下自動(dòng)化構(gòu)建復(fù)雜度比較高等特點(diǎn),并針對(duì)目前現(xiàn)狀給出了較好的解決方案。 原作者: 唐斌(騰訊)| TmodJS什么 TmodJS(原名atc)是一個(gè)簡(jiǎn)單易用的前端模板預(yù)編譯工具。它通過(guò)預(yù)編譯技術(shù)讓前端模板突破瀏覽器限制,實(shí)現(xiàn)后端模板一樣的同...
摘要:在上篇文章中我們的模板引擎實(shí)現(xiàn)了對(duì)和對(duì)支持,同時(shí)在文章的最后我給大家留了一個(gè)問(wèn)題如何實(shí)現(xiàn)支持和的標(biāo)簽功能。在本篇文章中我們將一起來(lái)動(dòng)手實(shí)現(xiàn)這兩個(gè)功能。 在 上篇文章 中我們的模板引擎實(shí)現(xiàn)了對(duì) if 和 for 對(duì)支持,同時(shí)在文章的最后我給大家留了一個(gè) 問(wèn)題:如何實(shí)現(xiàn)支持 include 和 extends 的標(biāo)簽功能。 在本篇文章中我們將一起來(lái)動(dòng)手實(shí)現(xiàn)這兩個(gè)功能。 include in...
摘要:首先我們來(lái)實(shí)現(xiàn)對(duì)語(yǔ)句的支持。下面我們就一起來(lái)讓我們的模板引擎的語(yǔ)法支持和可以從上下載可以看到,其實(shí)也是只增加了兩行代碼。效果就這樣我們的模板引擎對(duì)的支持算是比較完善了。 在 上篇文章中我們的模板引擎實(shí)現(xiàn)了變量和注釋功能,同時(shí)在文章的最后我給大家留了一個(gè) 問(wèn)題:如何實(shí)現(xiàn)支持 if 和 for 的標(biāo)簽功能: {% if user.is_admin %} admin, {{ user...
摘要:回到純靜態(tài)頁(yè)面開(kāi)發(fā)階段,讓頁(yè)面不需要后端渲染也能跑起來(lái)。改造開(kāi)始本文著重介紹如何將靜態(tài)頁(yè)面改造成后端渲染需要的模板??偨Y(jié)在后端渲染的項(xiàng)目里使用多頁(yè)應(yīng)用架構(gòu)是絕對(duì)可行的,可不要給老頑固們嚇唬得又回到傳統(tǒng)前端架構(gòu)了。 本文首發(fā)于Array_Huang的技術(shù)博客——實(shí)用至上,非經(jīng)作者同意,請(qǐng)勿轉(zhuǎn)載。原文地址:https://segmentfault.com/a/119000000820338...
閱讀 2821·2021-11-16 11:44
閱讀 981·2021-10-09 09:58
閱讀 4507·2021-09-24 09:48
閱讀 4389·2021-09-23 11:56
閱讀 2416·2021-09-22 15:48
閱讀 1908·2021-09-07 10:07
閱讀 3213·2021-08-31 09:46
閱讀 519·2019-08-30 15:56