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