基础公共库之模板引擎

1 引言

Web 框架把我们从 WSGI 中拯救出来了。现在,我们只需要不断地编写函数,就可以继续 Web App 的开发了。

但是,Web App 不仅仅是处理逻辑,展示给用户的页面也非常重要。在函数中返回一个包含 HTML 的字符串,简单的页面还可以,但是如果页面复杂,就有点头疼了

今天要说的【模版引擎】可以使得用户 UI 界面与业务数据分离

1.1 前端渲染 vs 后端渲染

前端渲染是通过 AJAX 请求数据,然后通过 js 语法将数据展示到页面中,称之为前端渲染
后端渲染是通过后端语言 + 后端模板将 页面整个发送给前端

1.2 什么是模板引擎

模板引擎是 Web 应用中用来生成动态 HTML 的工具, 它负责将数据模型与 HTML 模板结合(模板渲染),生成最终的 HTML。 编写 HTML 模板的语法称为模板语法,模板语法的表达能力和可扩展性决定了模板引擎的易用性。

如果你还不确定什么是模板引擎,这里做一个简单的类比:

printf

在 C++ 的 printf("Name: %s", str) 中,printf() 函数便是模板引擎, 它负责将格式化字符串与上下文数据结合生成最终的字符串。 其中

(1) "Name: %s"是模板
(2) %s 是一种模板语法
(3) str 则为上下文数据。

1.3 常见模板引擎

模板引擎分为后端模板引擎和前端模板引擎

后端模板引擎

  • Jinja2: 用 {% ... %} 和 {{ xxx }} 的模板

  • Mako:用 <% ... %> 和 ${xxx} 的一个模板;

  • Cheetah:也是用 <% ... %> 和 ${xxx} 的一个模板;

  • Django:Django 是一站式框架,内置一个用 {% ... %} 和 {{ xxx }} 的模板。

1.4 后端模板引擎的实现方法

后端模板引擎具有两个主要的阶段:

  • (1) 解析模板

  • (2) 渲染模板。

渲染模板具体包括:

  • 管理动态上下文和数据源

  • 执行逻辑元素

  • 实现点访问和过滤器执行

两种模型: 解释模型和编译模型

从解析阶段向渲染阶段传递什么东西是问题的关键。解析生产出什么来供渲染?有两个主要的选择,我们叫它们解释和编译,使用了和其他语言实现相关的术语。

  • 在一个解释模型中,解析产生一个数据结构表示模板的结构。渲染阶段遍历那个数据结构,基于找到的指令装配结果文本。一个真实的例子是 Django 模板引擎使用这种方法。

  • 在一个编译模型中,解析产生某种形式的可直接执行的代码(比如将模板解析成一个 python 函数)。渲染阶段执行那个代码,产生结果。Jinja2 和 Mako 都是使用编译方法的模板引擎。

我们的实现使用的编译模型:我们将模板编译为 python 代码,执行时,代码将结果组装起来。

2 实现

2.1 Templite 类

模板引擎的控制核心在于 Templite 类。(Templite(轻模板) = Template(模板) + Lite(轻))

分析模板

我们使用正则表达式将模板文本分解成一系列 token。

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

简单解释一下,括起来的标签或者表达式会把字符串分割,括起来的部分本身也是保留下来的。

(?s) 即 Singleline(单行模式)。表示更改。的含义,使它与每一个字符匹配(包括换行 符、n)

这里举个例子,如果输入以下文本:

<p>Topics for {{name}}: <div data-gb-custom-block data-tag="for">{{t}}, </div></p>

那么正则后会得到:

[
    '<p>Topics for ',               # 文本
    '{{name}}',                     # 表达式
    ': ',                           # 文本
    '<div data-gb-custom-block data-tag="for">',        # 标签
    '',                             # 文本 (空)
    '{{t}}',                        # 表达式
    ', ',                           # 文本
    '</div>',                 # 标签
    '</p>'                          # 文本
]

一旦文本被被分割成了一系列的 token,我们就能遍历 token 一段一段处理了:

for token in tokens:

注意得到的 token 类型可能有四种:

  • 文本

  • 注释:{# ... #}

  • 数据替换:{{ ... }}

    • 字典{"user":{"name":"meetbill"}} ==> {{user.name}} (备注:不能使用 {{user["name"]}})

  • 控制结构:{% ... %}

3 demo

from collections import namedtuple

from xlib.template import Templite


def demo():
    """
    template demo
    """
    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})
    # 渲染模板
    print t.render()


if __name__ == "__main__":
    demo()

其中渲染模板的时候,还可以输入新的 context,可谓是:一次解析,多次渲染

3.1 模版文本

<p>Welcome, {{user_name}}!</p>
<p>Products:</p>
<ul>
    {% for product in product_list %}
        <li>{{ product.name }}:{{ product.price|format_price}}</li>
    {% endfor %}
</ul>

3.2 模板编译后生成的 Python 函数

def render_function(context, do_dots):
    c_user_name = context['user_name']
    c_product_list = context['product_list']
    c_format_price = context['format_price']

    result = []
    append_result = result.append
    extend_result = result.extend
    to_str = str

    extend_result([
        '<p>Welcome, ',
        to_str(c_user_name),
        '!</p>\n<p>Products:</p>\n<ul>\n'
    ])
    for c_product in c_product_list:
        extend_result([
            '\n    <li>',
            to_str(do_dots(c_product, 'name')),
            ':\n        ',
            to_str(c_format_price(do_dots(c_product, 'price'))),
            '</li>\n'
        ])
    append_result('\n</ul>\n')
    return ''.join(result)

注:其中的微优化

  • append 与 extend 可能会在在代码中多次用到,所以使用 append_result 与 extend_result 来引用它们,这样会比平时直接使用 append 少一次检索的开销。

  • to_str = str 这句,它也是一种微优化,Python 检索局部空间比检索内置空间早,所以把 str 存储在局部变量中也是一种优化。

4 Http 网页相关

http 采用请求 / 相应模型;

请求消息和响应消息都可以包含实体信息,实体信息一般由实体头域和实体组成。实体头域包含关于实体的原信息,实体头包括 Allow、Content- Base、Content-Encoding、Content-Language、 Content-Length、Content-Location、Content-MD5、Content-Range、Content-Type、 Etag、Expires、Last-Modified、extension-header。

content-type 决定如何展示返回的消息体内容;

常遇到下面的几种情况:

  • 1、 服务端需要返回一段普通文本给客户端,Content-Type="text/plain"

  • 2 、服务端需要返回一段 HTML 代码给客户端 ,Content-Type="text/html"

  • 3 、服务端需要返回一段 XML 代码给客户端 ,Content-Type="text/xml"

  • 4 、服务端需要返回一段 javascript 代码给客户端

  • 5 、服务端需要返回一段 json 串给客户端

5 从零开始一个模板引擎的 python 实现

6 输出精美的 HTML 页面

ECharts

Last updated