4. 模板引擎Jinja2基础

在 Flask 中,视图函数负责处理请求、准备数据,而真正把数据变成浏览器可读的 HTML,靠的是模板引擎 Jinja2。理解 Jinja2 的关键,不是记住一堆语法,而是先看清一次模板渲染到底发生了什么:Flask 如何找到模板文件、如何把上下文变量交给模板、Jinja2 如何解析并执行模板中的表达式与控制结构,最后如何把结果作为响应返回。

模板渲染流程:从视图到 HTML

当视图函数返回 render_template('index.html', name='Flask') 时,Flask 会按以下顺序工作:首先在应用配置的模板目录(默认是项目根目录下的 templates/)中查找 index.html;找到后,Jinja2 会先对模板进行词法与语法分析,生成内部可执行的模板对象;随后 Flask 把视图传入的关键字参数以及一些全局对象(如 request、session、g、url_for)组成渲染上下文;Jinja2 在这个上下文中执行模板,遇到 {{ }} 就求值输出,遇到 {% %} 就执行控制逻辑;最终生成一段完整的 HTML 字符串,由 Flask 包装成响应返回给客户端。

这里有一个容易被忽略的细节:模板查找是相对于应用的模板目录,而不是当前 Python 文件所在目录。如果模板放在 templates/admin/user.html,那么视图里必须写 render_template('admin/user.html'),路径分隔符统一使用正斜杠。模板不存在时,Flask 会抛出 TemplateNotFound,这通常意味着目录名拼写错误或模板没有放在正确位置。

Jinja2 模板渲染流程 视图函数 render_template( 'index.html', name=…) 模板查找 templates/ 目录 index.html 语法解析 生成模板对象 缓存复用 上下文注入 变量 + 全局对象 request / session 模板执行 {{ 变量 }} 求值输出 | {% 控制结构 %} 执行逻辑 过滤器 | 继承 | block 替换 生成 HTML 字符串 → 响应返回浏览器 Content-Type: text/html; charset=utf-8 常见异常 TemplateNotFound 路径拼写 / 目录位置

变量输出与过滤器

Jinja2 使用 {{ 变量名 }} 输出变量。变量可以来自视图传入的关键字参数,也可以是上下文中的全局对象。例如视图传入 user={'name':'Alice','age':28},模板中写 {{ user.name }} 或 {{ user['name'] }} 都能输出 Alice。如果变量不存在,Jinja2 默认输出空字符串而不是报错,这一点与 Python 直接访问不存在的键不同,需要特别注意。

过滤器用于在输出前对变量做转换,语法是 {{ 变量 | 过滤器名(参数) }}。常用过滤器包括:upper 转大写、lower 转小写、title 首字母大写、length 求长度、default('未知') 提供默认值、join(', ') 连接列表、round(2) 保留小数、safe 标记为安全 HTML。多个过滤器可以链式使用,例如 {{ name | trim | title }}。需要强调的是,Jinja2 默认开启自动转义,{{ 'hi' }} 会原样输出标签文本,只有明确使用 | safe 才会作为 HTML 渲染,这是防止 XSS 的重要机制。

控制结构:条件与循环

控制结构使用 {% %} 包裹。条件判断的写法是 {% if 条件 %} … {% elif 条件 %} … {% else %} … {% endif %}。循环使用 {% for item in items %} … {% endfor %},在循环内部可以访问 loop.index(从 1 开始)、loop.index0(从 0 开始)、loop.first、loop.last 等变量。例如渲染一个用户列表:

<ul>
{% for user in users %}
  <li>{{ loop.index }}. {{ user.name | title }}</li>
{% else %}
  <li>暂无用户</li>
{% endfor %}
</ul>

注意 {% for %} 也支持 {% else %} 分支,当序列为空时执行,这比在模板里写额外的 if 判断更简洁。循环中还可以使用 {% break %} 和 {% continue %}(Jinja2 2.10+ 支持)来提前结束或跳过迭代。

模板继承与 block 机制

模板继承是 Jinja2 最强大的复用机制。做法是先定义一个基础模板(通常叫 base.html),在其中用 {% block 名称 %}{% endblock %} 声明可被子模板覆盖的区域。子模板第一行写 {% extends 'base.html' %},然后通过 {% block 名称 %} 新内容 {% endblock %} 替换对应区域。如果子模板想在父模板 block 内容的基础上追加而不是完全替换,可以使用 {{ super() }}。

一个典型的基础模板结构如下:

<!DOCTYPE html>
<html>
<head>
  <title>{% block title %}默认标题{% endblock %}</title>
</head>
<body>
  <header>{% block header %}<h1>站点标题</h1>{% endblock %}</header>
  <main>{% block content %}{% endblock %}</main>
  <footer>{% block footer %}&copy; 2025</footer>
</body>
</html>

子模板 index.html 只需写:

{% extends 'base.html' %}
{% block title %}首页 - {{ super() }}{% endblock %}
{% block content %}
  <p>欢迎,{{ username }}</p>
{% endblock %}

这样,所有页面共享同一套 HTML 骨架,只需关注各自的内容区域。block 可以嵌套,子模板也可以继续被更具体的模板继承,形成多层模板体系。实际项目中常见的做法是:base.html 定义全局布局,admin/base.html 继承它并增加后台侧边栏,具体页面再继承 admin/base.html。

实践建议:模板中尽量只做展示逻辑,复杂的数据处理放在视图函数或模型层完成。过滤器适合做轻量格式化,不要在模板里写大段业务判断。使用 url_for() 生成链接而不是硬编码路径,这样在路由变化时模板无需修改。
注意:Jinja2 的 {{ }} 输出默认自动转义,但 {% %} 中的代码不会自动转义。如果使用 | safe 或 Markup,必须确保内容来自可信来源,否则可能引入 XSS 风险。

掌握以上五点后,就可以用 Jinja2 组织出结构清晰、复用性强的 Flask 页面。下一步是在实际项目中把模板目录、静态文件、路由和模板继承结合起来,形成完整的页面渲染链路。