> For the complete documentation index, see [llms.txt](https://meetbill.gitbook.io/butterfly-project-doc/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://meetbill.gitbook.io/butterfly-project-doc/project-framework/how/butterfly-template.md).

# 基础公共库之模板引擎

### 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 实现

> * [英文原文](http://aosabook.org/en/500L/a-template-engine.html)
> * [中文翻译](https://www.jianshu.com/p/b5d4aa45e771)
> * [源码](https://github.com/aosabook/500lines/tree/master/template-engine/code)

### 6 输出精美的 HTML 页面

> ECharts
