从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

1.2 实验内容

模版引擎使得用户界面能够与业务数据分离,前端与后端分离,它通常用于渲染页面文件。本课程将使用 Python 实现一个具备基础功能的模板引擎。

1.3 实验知识点

本课程项目完成过程中,我们将学习:

  1. 实现模版引擎的原理与方法
  2. 如何编写程序生成代码
从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

先从一个简单的例子开始,我们想要生成以下 HTML 文本:

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

然这段代码奏效了,但是看上去有点混乱。静态文本被分成了 PAGE_HTML 与 PRODUCT_HTML两部分,而且动态数据格式化的细节操作都在 Python 代码中,使得前端设计师不得不去修改 Python 文件。随着代码量的增多,这段代码也会渐渐变得难以维护。

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

2.2 模板使用的语法

扫描二维码关注公众号,回复: 2004473 查看本文章

模板引擎因其支持的语法而异,本课程中使用的引擎语法基于 Django - 一个非常流行的web框架。

代入数据使用双花括号:

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

2.3 引擎的实现方法

大方向上,模板的处理流程可以分为两部分:解析阶段与渲染阶段。

渲染模板需要考虑以下几方面:

  • 管理数据来源(即上下文环境)
  • 处理逻辑(条件判断、循环)的部分
  • 实现点取得成员属性或者键值的功能、实现过滤器调用

问题的关键在于从解析阶段到渲染阶段是如何过渡的。解析得到了什么?渲染又是在渲染什么?解析阶段可能有两种不同的做法:解释或者是编译,这正对应了我们的程序语言的实现方式。

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

每一个模版都会被转换为 render_function 函数,其中 context 上下文环境存储导入的数据词典。 do_dots 存储用来取得对象属性或者词典键值的函数。

我们从头开始分析这段代码,最开始是对输入的数据词典进行解包,得到的每个变量都使用 c_作为前缀。先使用队列来存储结果, append 与 extend 可能会在在代码中多次用到,所以使用append_result 与 extend_result 来引用它们,这样会比平时直接使用 append 少一次检索的开销。之后就是使用 append_result 与 extend_result 把结果串起来,其中需要替换的部分就用输入数据替换,最后把队列合成一个字符串作为结果文本返回。

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

Templite 类会在之后实现,我们需要先完成代码构建器的编写。

四、代码构建器

代码构建器是为了方便 Templite 生成代码而编写的小工具,它的工作主要有添加代码、控制缩进、返回完整的代码字符串等。

我们创建 CodeBuilder 类作为代码构建器。

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

__str__ 返回生成的代码字符串,遍历 code 的内容时如果遇到 CodeBuilder 对象,则会递归调用该对象的 __str__ 方法。

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

五、实现 Templite 类

Templite 类的代码分为编译与渲染两个阶段。

5.1 编译

首先保存上下文:

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

创建 CodeBuilder 对象,开始往里面添加代码。

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

为什么提取变量的代码要在之后实现呢?因为输入的变量不一定全都用得上,我们只提取要用的部分,这得通过分析完模板才能确定下来。

为什么添这些代码?可以参照之前的那个例子:

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

那么正则后会得到:

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

if token.startswith('{#'):

continue

处理数据替换 {{ ... }}

我们将花括号切掉,然后调用 _expr_code() 得到结果, _expr_code() 的作用是将模板内的表达式转换成 Python 表达式,它会在之后实现:

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

这里的 if 只能支持一个表达式,所以如果 words 大于2就直接报错。我们往栈中弹入一个 if标签,直到找到一个 endif 标签才弹出。 _syntax_error 是一个报语法错误异常的函数,它会在之后实现。注意 words[1] 同样需要利用 _expr_code 将其从模板表达式转换成 Python 表达式。

处理 for 标签:

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

如果弹出的不是以上任意一种则报错:

else:

self._syntax_error("Don't understand tag", words[0])

处理文本

文本追加进 buffered 就行。

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

可以看到模板中存在三个变量 user_name 、 product_list 与 product , all_vars 会包含以上所有的变量名, loop_vars 只会包含 product 。所以我们根据它们的差集提取数据, vars_code 就是之前 add_section 得到的 CodeBuilder 对象:

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

def _expr_code(self, expr):

"""根据`expr`生成Python表达式."""

首先需要考虑模板表达式中有没有管道符号 | ,如果有则需要将表达式拆解放入队列,第一个管道前的表达式就是数据源,我们递归调用 _expr_code 得到数据源的 Python 表达式。

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

实现 _variable 函数帮助我们将变量存入指定的变量集中,同时帮我们验证变量名的有效性:

def _variable(self, name, vars_set):

if not re.match(r"[_a-zA-Z][_a-zA-Z0-9]*$", name):

self._syntax_error("Not a valid name", name)

vars_set.add(name)

5.2 渲染

渲染阶段几乎没什么工作,只需要实现 render 与 _do_dots 就可以了。

实现 render 几乎就是把 _render_function 封装一下

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

六、完整代码

完整代码如下:

"""A simple Python template renderer, for a nano-subset of Django syntax."""

# Coincidentally named the same as http://code.activestate.com/recipes/496702/

import re

class TempliteSyntaxError(ValueError):

"""Raised when a template has a syntax error."""

pass

class CodeBuilder(object):

"""Build source code conveniently."""

def __init__(self, indent=0):

self.code = []

self.indent_level = indent

def __str__(self):

return "".join(str(c) for c in self.code)

def add_line(self, line):

"""Add a line of source to the code.

Indentation and newline will be added for you, don't provide them.

"""

self.code.extend([" " * self.indent_level, line, " "])

def add_section(self):

"""Add a section, a sub-CodeBuilder."""

section = CodeBuilder(self.indent_level)

self.code.append(section)

return section

INDENT_STEP = 4 # PEP8 says so!

def indent(self):

"""Increase the current indent for following lines."""

self.indent_level += self.INDENT_STEP

def dedent(self):

"""Decrease the current indent for following lines."""

self.indent_level -= self.INDENT_STEP

def get_globals(self):

"""Execute the code, and return a dict of globals it defines."""

# A check that the caller really finished all the blocks they started.

assert self.indent_level == 0

# Get the Python source as a single string.

python_source = str(self)

# Execute the source, defining globals, and return them.

global_namespace = {}

exec(python_source, global_namespace)

return global_namespace

class Templite(object):

"""A simple template renderer, for a nano-subset of Django syntax.

Supported constructs are extended variable access::

{{var.modifer.modifier|filter|filter}}

loops::

{% for var in list %}...{% endfor %}

and ifs::

{% if var %}...{% endif %}

Comments are within curly-hash markers::

{# This will be ignored #}

Construct a Templite with the template text, then use `render` against a

dictionary context to create a finished string::

templite = Templite('''

<h1>Hello {{name|upper}}!</h1>

{% for topic in topics %}

<p>You are interested in {{topic}}.</p>

{% endif %}

''',

{'upper': str.upper},

)

text = templite.render({

'name': "Ned",

'topics': ['Python', 'Geometry', 'Juggling'],

})

"""

def __init__(self, text, *contexts):

"""Construct a Templite with the given `text`.

`contexts` are dictionaries of values to use for future renderings.

These are good for filters and global values.

"""

self.context = {}

for context in contexts:

self.context.update(context)

self.all_vars = set()

self.loop_vars = set()

# We construct a function in source form, then compile it and hold onto

# it, and execute it to render the template.

code = CodeBuilder()

code.add_line("def render_function(context, do_dots):")

code.indent()

vars_code = code.add_section()

code.add_line("result = []")

code.add_line("append_result = result.append")

code.add_line("extend_result = result.extend")

code.add_line("to_str = str")

buffered = []

def flush_output():

"""Force `buffered` to the code builder."""

if len(buffered) == 1:

code.add_line("append_result(%s)" % buffered[0])

elif len(buffered) > 1:

code.add_line("extend_result([%s])" % ", ".join(buffered))

del buffered[:]

ops_stack = []

# Split the text to form a list of tokens.

tokens = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text)

for token in tokens:

if token.startswith('{#'):

# Comment: ignore it and move on.

continue

elif token.startswith('{{'):

# An expression to evaluate.

expr = self._expr_code(token[2:-2].strip())

buffered.append("to_str(%s)" % expr)

elif token.startswith('{%'):

# Action tag: split into words and parse further.

flush_output()

words = token[2:-2].strip().split()

if words[0] == 'if':

# An if statement: evaluate the expression to determine if.

if len(words) != 2:

self._syntax_error("Don't understand if", token)

ops_stack.append('if')

code.add_line("if %s:" % self._expr_code(words[1]))

code.indent()

elif words[0] == 'for':

# A loop: iterate over expression result.

if len(words) != 4 or words[2] != 'in':

self._syntax_error("Don't understand for", token)

ops_stack.append('for')

self._variable(words[1], self.loop_vars)

code.add_line(

"for c_%s in %s:" % (

words[1],

self._expr_code(words[3])

)

)

code.indent()

elif words[0].startswith('end'):

# Endsomething. Pop the ops stack.

if len(words) != 1:

self._syntax_error("Don't understand end", token)

end_what = words[0][3:]

if not ops_stack:

self._syntax_error("Too many ends", token)

start_what = ops_stack.pop()

if start_what != end_what:

self._syntax_error("Mismatched end tag", end_what)

code.dedent()

else:

self._syntax_error("Don't understand tag", words[0])

else:

# Literal content. If it isn't empty, output it.

if token:

buffered.append(repr(token))

if ops_stack:

self._syntax_error("Unmatched action tag", ops_stack[-1])

flush_output()

for var_name in self.all_vars - self.loop_vars:

vars_code.add_line("c_%s = context[%r]" % (var_name, var_name))

code.add_line("return ''.join(result)")

code.dedent()

self._render_function = code.get_globals()['render_function']

def _expr_code(self, expr):

"""Generate a Python expression for `expr`."""

if "|" in expr:

pipes = expr.split("|")

code = self._expr_code(pipes[0])

for func in pipes[1:]:

self._variable(func, self.all_vars)

code = "c_%s(%s)" % (func, code)

elif "." in expr:

dots = expr.split(".")

code = self._expr_code(dots[0])

args = ", ".join(repr(d) for d in dots[1:])

code = "do_dots(%s, %s)" % (code, args)

else:

self._variable(expr, self.all_vars)

code = "c_%s" % expr

return code

def _syntax_error(self, msg, thing):

"""Raise a syntax error using `msg`, and showing `thing`."""

raise TempliteSyntaxError("%s: %r" % (msg, thing))

def _variable(self, name, vars_set):

"""Track that `name` is used as a variable.

Adds the name to `vars_set`, a set of variable names.

Raises an syntax error if `name` is not a valid name.

"""

if not re.match(r"[_a-zA-Z][_a-zA-Z0-9]*$", name):

self._syntax_error("Not a valid name", name)

vars_set.add(name)

def render(self, context=None):

"""Render this template by applying it to `context`.

`context` is a dictionary of values to use in this rendering.

"""

# Make the complete context we'll use.

render_context = dict(self.context)

if context:

render_context.update(context)

return self._render_function(render_context, self._do_dots)

def _do_dots(self, value, *dots):

"""Evaluate dotted expressions at runtime."""

for dot in dots:

try:

value = getattr(value, dot)

except AttributeError:

value = value[dot]

if callable(value):

value = value()

return value

使用以下代码看一下效果:

from templite import Templite

from collections import namedtuple

template_text = """

<p>Welcome, {{user_name}}!</p>

<p>Products:</p>

<ul>

{% for product in product_list %}

<li>{{ product.name }}:

{{ product.price|format_price}}</li>

{% endfor %}

</ul>

"""

Product = namedtuple("Product",["name", "price"])

product_list = [Product("Apple", 1), Product("Fig", 1.5), Product("Pomegranate", 3.25)]

def format_price(price):

return "$%.2f" % price

t = Templite(template_text, {"user_name":"Charlie", "product_list":product_list}, {"format_price":format_price})

运行结果:

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

七、测试

我们的测试文件覆盖了所有的行为与条件边界,它甚至比模版引擎的代码还长。但这在实际生产中也是很典型很常见的,说明代码有被好好测试过。这里给出测试文件。

wget http://labfile.oss.aliyuncs.com/courses/583/test_templite.py

测试命令:

python -m unittest test_templite

测试结果:

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

进群:125240963  即可获取数十套PDF哦!

从零到搭建一个模板引擎需要多久?一个星期?一个小时就够了!

猜你喜欢

转载自blog.csdn.net/qq_42156420/article/details/80969086