2. 模板引擎基础:Jinja2 语法入门

Jinja2 是 Python 生态中最流行的模板引擎,被 Flask、Django(自 1.8 起内置)、Sanic 等框架广泛采用。它通过 模板 + 上下文数据 的分离机制,将业务逻辑与展示逻辑解耦。本章聚焦 Jinja2 的核心语法:变量插值、过滤器链式处理、控制结构,为后续“模板渲染性能优化”和“异步导出”打下基础。

2.1 变量与表达式

Jinja2 使用 {{ ... }} 作为变量输出标记,支持 Python 基本类型、列表、字典、对象属性访问。

语法 示例 输出结果
{{ var }} {{ name }} Alice
{{ list[idx] }} {{ users[0] }} Alice
{{ dict.key }} {{ config.host }} 127.0.0.1
{{ obj.attr }} {{ user.name }} Alice
{{ 'string' }} {{ 'Hello' }} Hello
避坑提示: 当变量不存在时,Jinja2 默认输出空字符串(''),不会抛出异常。若需严格校验,可在渲染时设置 undefined=StrictUndefined。

2.2 过滤器:数据变换的管道

过滤器通过 | 符号连接,支持链式调用。常用过滤器如下:

过滤器 作用 示例 输出
safe 标记为安全 HTML,不转义 {{ '<b>Bold</b>' | safe }} Bold
escape / e HTML 转义 {{ '<b>Bold</b>' | e }} &lt;b&gt;Bold&lt;/b&gt;
default 变量为空时使用默认值 {{ nickname | default('匿名') }} 匿名
length 返回序列长度 {{ items | length }} 5
join 列表拼接为字符串 {{ tags | join(', ') }} python, flask, jinja2
upper / lower 大小写转换 {{ 'hello' | upper }} HELLO
int / float 类型转换 {{ '3.14' | float }} 3.14
# 链式过滤器示例
{{ "  Hello World  " | trim | upper | replace(" ", "_") }}
# 输出: HELLO_WORLD

2.3 控制结构:条件与循环

控制结构使用 {% ... %} 标记,必须配合 {% endif %}、{% endfor %} 等结束标签。

2.3.1 条件判断

{% if user.is_admin %}
  <span class="badge badge-admin">管理员</span>
{% elif user.is_editor %}
  <span class="badge badge-editor">编辑</span>
{% else %}
  <span class="badge badge-user">普通用户</span>
{% endif %}

2.3.2 循环遍历

<ul>
{% for item in items %}
  <li>{{ loop.index }}. {{ item.name }} - {{ item.price | int }} 元</li>
{% else %}
  <li>暂无数据</li>
{% endfor %}
</ul>

loop 是 Jinja2 内置的循环辅助对象,常用属性:

属性 说明 示例值
loop.index 当前迭代次数(从 1 开始) 1, 2, 3
loop.index0 当前迭代次数(从 0 开始) 0, 1, 2
loop.first 是否为第一个元素 True / False
loop.last 是否为最后一个元素 True / False
loop.length 序列总长度 10

2.4 模板继承与宏

Jinja2 通过 {% extends %} 和 {% block %} 实现模板复用,通过 {% macro %} 定义可复用的函数式片段。

<!-- base.html -->
<!DOCTYPE html>
<html>
<head>
  <title>{% block title %}默认标题{% endblock %}</title>
</head>
<body>
  <header>{% block header %}通用头部{% endblock %}</header>
  <main>{% block content %}{% endblock %}</main>
  <footer>{% block footer %}通用底部{% endblock %}</footer>
</body>
</html>
<!-- page.html -->
{% extends "base.html" %}
{% block title %}用户列表{% endblock %}
{% block content %}
  <h1>用户管理</h1>
  {% include "_user_table.html" %}
{% endblock %}
性能要点: 模板继承在编译阶段会合并为单一模板,不会产生多次文件 IO。但 {% include %} 每次渲染都会重新加载子模板,在热点路径上应避免过多嵌套 include。

2.5 Jinja2 渲染流程与性能关键点

下图展示了 Jinja2 从模板源码到最终 HTML 输出的完整生命周期,其中 编译缓存 和 字节码复用 是影响渲染性能的核心环节。

Jinja2 模板渲染生命周期 模板源码 .j2 / .html 词法/语法分析 AST 生成 编译 Python 字节码 编译缓存 内存 / 文件 渲染执行 上下文注入 + 字节码执行 HTML 输出 字符串 / 流 关键优化点: • 编译缓存避免重复解析,生产环境应启用 bytecode_cache(如 Redis 或文件缓存) • 渲染阶段瓶颈在上下文变量查找,减少深层嵌套属性访问可提升性能

2.6 实战:渲染一个用户列表

结合变量、过滤器、循环和条件判断,实现一个带分页状态的用户表格:

from jinja2 import Environment, FileSystemLoader

env = Environment(
    loader=FileSystemLoader('templates/'),
    autoescape=True,
    trim_blocks=True,
    lstrip_blocks=True
)

template = env.get_template('user_list.html')

context = {
    'users': [
        {'name': 'Alice', 'role': 'admin', 'active': True},
        {'name': 'Bob', 'role': 'editor', 'active': False},
        {'name': 'Charlie', 'role': 'user', 'active': True},
    ],
    'page': 1,
    'total_pages': 5
}

output = template.render(context)
print(output)
<!-- user_list.html -->
<table>
  <thead>
    <tr>
      <th>#</th>
      <th>用户名</th>
      <th>角色</th>
      <th>状态</th>
    </tr>
  </thead>
  <tbody>
  {% for user in users %}
    <tr class="{{ 'active' if user.active else 'inactive' }}">
      <td>{{ loop.index }}</td>
      <td>{{ user.name | e }}</td>
      <td>{{ user.role | capitalize }}</td>
      <td>{{ '在线' if user.active else '离线' }}</td>
    </tr>
  {% else %}
    <tr><td colspan="4">暂无用户数据</td></tr>
  {% endfor %}
  </tbody>
</table>
<p>第 {{ page }} / {{ total_pages }} 页</p>
安全警告: 默认情况下 Jinja2 会对所有变量进行 HTML 转义(autoescape=True)。如果渲染用户提交的富文本内容,务必使用 bleach 或 nh3 库进行白名单过滤,切勿直接使用 | safe 过滤器。

2.7 本章小结

  • 变量插值:{{ }} 支持点号、下标、方法调用,注意 Undefined 行为。
  • 过滤器:链式管道处理数据,safe、default、join 是高频使用项。
  • 控制结构:{% if %} / {% for %} 配合 loop 对象实现复杂逻辑。
  • 继承与宏:{% extends %} + {% block %} 实现布局复用,{% macro %} 封装可复用片段。
  • 性能:编译缓存是渲染性能的基石,后续章节将结合 Redis 实现分布式模板缓存。