如果每个页面都在视图函数里拼 HTML 字符串,代码会变成灾难:HTML 和 Python 逻辑纠缠、无法复用、难以维护。模板(Templates)解决这个问题:把"页面长什么样"(HTML)和"页面显示什么"(数据)分离——模板是 HTML 骨架,数据由视图函数注入。
Jinja2 是 Flask 默认的模板引擎,语法类似 Python 但更简洁。模板文件放在 templates/ 目录。
💡 关键直觉:模板 = 印刷厂的"版式",数据 = 待印的"内容"。同一版式(模板)可以印无数份内容(数据),改版式只改一处。
from flask import Flask, render_template app = Flask(__name__) @app.route('/') def index(): name = "Flask" users = ["Alice", "Bob", "Charlie"] return render_template('index.html', name=name, users=users)
render_template 找到 templates/index.html,把 name、users 注入模板,渲染后返回 HTML。
<!DOCTYPE html> <html> <head><title>{{ name }} 教程</title></head> <body> <h1>欢迎使用 {{ name }}</h1> {# 控制流:if #} {% if users %} <p>共有 {{ users|length }} 位用户:</p> {% else %} <p>暂无用户</p> {% endif %} {# 控制流:for 循环 #} <ul> {% for user in users %} <li>{{ user }}</li> {% endfor %} </ul> </body> </html>
语法速查:
| 语法 | 作用 |
|---|---|
{{ 变量 }} |
输出变量值 |
{% if %}{% endif %} |
条件判断 |
{% for x in list %}{% endfor %} |
循环 |
| `{{ 变量 | filter }}` |
{# 注释 #} |
注释(不输出) |
# 视图传一个带 HTML 的变量 html_content = "<b>加粗文字</b>"
{{ html_content }} <!-- 自动转义,显示为文本 <b> --> {{ html_content|safe }} <!-- 不转义,显示为加粗 --> {{ name|upper }} <!-- 转大写 --> {{ name|default("访客") }} <!-- 为空时用默认值 --> {{ users|length }} <!-- 长度 --> {{ price|round(2) }} <!-- 四舍五入 -->
⚠️ 常见坑:自动转义是安全机制,别轻易用 |safe。Jinja2 默认转义 HTML(防 XSS),
|safe会关闭转义——只有确信内容是安全的(如自己生成的 HTML)才用。
真实网站大量页面共用头部、导航、底部——模板继承(base 模板)避免重复:
<!DOCTYPE html> <html> <head> <title>{% block title %}默认标题{% endblock %}</title> <link rel="stylesheet" href="/static/style.css"> </head> <body> <nav> <a href="/">首页</a> | <a href="/about">关于</a> </nav> <main> {% block content %}{% endblock %} </main> <footer>© 2026 Flask 教程</footer> </body> </html>
{% extends 'base.html' %} {% block title %}关于我们{% endblock %} {% block content %} <h1>关于我们</h1> <p>这里是关于页面。</p> {% endblock %}
继承的价值:导航、页脚写一次;每个子页面只写自己的内容块(block)。改导航只改 base.html,全站生效。
from flask import Flask, render_template app = Flask(__name__) @app.route('/') def index(): users = [ {"name": "Alice", "age": 25}, {"name": "Bob", "age": 30}, {"name": "Charlie", "age": 22}, ] return render_template('index.html', users=users)
<!-- templates/index.html --> {% extends 'base.html' %} {% block content %} <h1>用户列表</h1> <table> <tr><th>姓名</th><th>年龄</th></tr> {% for user in users %} <tr> <td>{{ user.name }}</td> <td>{{ user.age }}</td> <td>{% if user.age >= 25 %}成年{% else %}青年{% endif %}</td> </tr> {% endfor %} </table> {% endblock %}
运行后访问首页,看到表格渲染——数据与 HTML 完全分离,改样式不动 Python,改数据不动 HTML。
# 在视图中注册模板全局函数 @app.context_processor def inject_now(): return {'now': '2026-08-15'} # 模板里可直接用 {{ now }}
<a href="{{ url_for('index') }}">首页</a> <a href="{{ url_for('show_user', username='bob') }}">Bob</a>
url_for 在模板中同样适用——动态生成链接,路由改名自动更新。
问:模板目录必须叫 templates 吗?
默认是,但可自定义:Flask(__name__, template_folder='views')。
问:模板能访问所有视图变量吗?
不能。render_template 里传什么,模板才能用什么(加上 context_processor 注入的全局变量)。
问:|safe 什么时候用?
确认内容安全时(自己生成的 HTML、可信的富文本)。用户输入的内容永远不要用 |safe。
问:一个页面能用多个 block 吗?
能。base 模板可定义多个 block(title/content/sidebar 等),子模板选择性覆盖。
| 语法 | 用途 | 示例 |
|---|---|---|
{{ 表达式 }} |
输出变量/表达式结果 | {{ user.name }} |
{% 语句 %} |
控制流(if/for/宏) | {% if user %} |
{# 注释 #} |
注释(不输出到 HTML) | {# 这是注释 #} |
过滤器是"处理变量的管道":{{ 变量 | 过滤器 }}
| 过滤器 | 作用 | 示例 |
|---|---|---|
default |
空值给默认 | {{ name | default('游客') }} |
length |
长度 | {{ items | length }} |
upper / lower |
大小写 | {{ name | upper }} |
join |
拼接列表 | {{ tags | join(', ') }} |
truncate |
截断文本 | {{ body | truncate(100) }} |
safe |
标记为安全不转义 | {{ html | safe }}(慎用!) |
tojson |
转 JSON(传给 JS) | var data = {{ data | tojson }} |
{# 条件 #} {% if user.is_admin %} <a href="/admin">管理</a> {% elif user %} <a href="/profile">个人中心</a> {% else %} <a href="/login">登录</a> {% endif %} {# 循环 #} {% for post in posts %} <div class="post"> <h2>{{ post.title }}</h2> <p>{{ post.body | truncate(80) }}</p> </div> {% else %} <p>暂无文章</p> {# 列表为空时显示 #} {% endfor %}
模板继承——页面框架复用:
{# base.html:父模板 #} <html> <body> {% block content %}{% endblock %} </body> </html> {# child.html:子模板 #} {% extends "base.html" %} {% block content %} <h1>这是我的页面</h1> {% endblock %}
宏(macro)——复用模板片段:
{# macros.html #} {% macro render_field(field) %} <label>{{ field.label }}</label>{{ field() }} {% endmacro %} {# 使用 #} {% import "macros.html" as macros %} {{ macros.render_field(form.username) }}
templates/ 下按蓝图分目录(templates/auth/login.html),避免文件重名冲突;| 现象 | 原因 | 解法 |
|---|---|---|
TemplateNotFound |
路径/文件名不对 | 检查文件名与 render_template 参数一致 |
| 变量显示为空 | 视图没传或名字拼错 | 核对 render_template 的变量名 |
UndefinedError |
模板里访问了不存在的属性 | 先 {% if %} 判空或用 default |
页面直接显示 {{ }} |
文件后缀不是 .html 或缓存 | 确认模板在 templates 目录 |
| 循环不输出 | 数据是空列表 | 用 {% else %} 块提示空状态 |
# 视图 @app.route('/blog') def blog(): posts = [ {'title': '第一篇', 'body': '你好,Flask!', 'tags': ['flask']}, {'title': '第二篇', 'body': '模板真好用', 'tags': ['jinja2']}, ] return render_template('blog.html', posts=posts, site_name='我的博客')
{# templates/blog.html #} {% extends "base.html" %} {% block content %} <h1>{{ site_name }}</h1> {% for post in posts %} <article> <h2>{{ post.title }}</h2> <p>{{ post.body | truncate(50) }}</p> <span>标签:{{ post.tags | join('、') }}</span> </article> {% else %} <p>还没有文章</p> {% endfor %} {% endblock %}
浏览器访问 /blog,看到循环渲染的列表——数据与展示分离的威力就在这里:视图只管给数据,模板管怎么展示。
学会模板后,你会接触到一个架构层面的概念:服务端渲染(SSR)与客户端渲染(CSR)的区别——这决定了"页面在哪生成"。
服务端渲染(Flask 的方式):服务器用模板把数据拼成完整 HTML,返回给浏览器直接显示。优点:首屏快、SEO 友好(搜索引擎能抓到内容)、逻辑简单(全在一个语言里)。缺点:每次交互都要刷新页面(除非配合 AJAX)。
客户端渲染(前端框架的方式):服务器只返回数据和空壳页面,浏览器用 JavaScript(Vue/React)动态生成内容。优点:交互流畅(局部更新)、后端前端职责清晰。缺点:首屏慢、SEO 需要额外处理、开发复杂度高。
Flask 应用的典型组合:用 Jinja2 渲染主要页面,用 API(第二章 2.8)返回 JSON 数据,前端用少量 AJAX 做局部交互——两边各取所长。这是小型到中型应用最务实的架构。
模板的"薄"与"厚"之争:新手容易把业务逻辑写进模板(在 {% %} 里算价格、拼字符串)。实践共识是模板保持"薄"——只做展示。复杂计算、数据加工放视图函数,模板只负责"把给定的数据画出来"。这样:模板改动不影响业务逻辑、视图可测试、模板可复用。
一个模板复用的实战场景:一个电商站,商品列表页、搜索结果页、推荐页都用同一个"商品卡片"片段——用宏或 include 封装成 _product_card.html,三处引用。改一次卡片样式,三处同步生效。这就是模板复用的价值:改动一处,处处生效。
模板调试技巧:页面空白时先确认视图有没有传数据(print 一下);变量未定义时用 {% if var is defined %} 或 default 过滤器兜底;怀疑模板缓存时重启 Flask(debug 模式自动重载)。记住:模板错误和 Python 错误一样,都会给出文件与行号——报错信息里 templates/xxx.html 就是问题模板。