1.4 模板 Templates 与 Jinja2


1.4 模板 Templates 与 Jinja2

问题与直觉:为什么 HTML 不能写死在代码里

如果每个页面都在视图函数里拼 HTML 字符串,代码会变成灾难:HTML 和 Python 逻辑纠缠、无法复用、难以维护。模板(Templates)解决这个问题:把"页面长什么样"(HTML)和"页面显示什么"(数据)分离——模板是 HTML 骨架,数据由视图函数注入。

Jinja2 是 Flask 默认的模板引擎,语法类似 Python 但更简洁。模板文件放在 templates/ 目录。

💡 关键直觉:模板 = 印刷厂的"版式",数据 = 待印的"内容"。同一版式(模板)可以印无数份内容(数据),改版式只改一处。

核心原理:Jinja2 基础语法

渲染模板

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,把 nameusers 注入模板,渲染后返回 HTML。

模板语法(index.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 模板)避免重复:

base.html(父模板)

<!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>

about.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 }}

模板中生成 URL

<a href="{{ url_for('index') }}">首页</a> <a href="{{ url_for('show_user', username='bob') }}">Bob</a>

url_for 在模板中同样适用——动态生成链接,路由改名自动更新。

FAQ:模板高频问题

问:模板目录必须叫 templates 吗?
默认是,但可自定义:Flask(__name__, template_folder='views')

问:模板能访问所有视图变量吗?
不能。render_template 里传什么,模板才能用什么(加上 context_processor 注入的全局变量)。

问:|safe 什么时候用?
确认内容安全时(自己生成的 HTML、可信的富文本)。用户输入的内容永远不要用 |safe。

问:一个页面能用多个 block 吗?
能。base 模板可定义多个 block(title/content/sidebar 等),子模板选择性覆盖。

深入理解:Jinja2 语法全解

三种语法块

语法 用途 示例
{{ 表达式 }} 输出变量/表达式结果 {{ 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) }}

模板组织最佳实践

  1. 分层目录templates/ 下按蓝图分目录(templates/auth/login.html),避免文件重名冲突;
  2. base.html 做整体框架,页面模板继承它,只覆写 block;
  3. 公共片段用 include/宏,避免复制粘贴;
  4. 模板里少写复杂逻辑:复杂计算放视图,模板只做展示——模板保持"薄"。

常见模板问题排查

现象 原因 解法
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,看到循环渲染的列表——数据与展示分离的威力就在这里:视图只管给数据,模板管怎么展示。

本节速览

  • 三种语法:{{ 表达式 }} 输出、{% 语句 %} 控制流、{# 注释 #} 不输出。
  • 过滤器:default/length/upper/join/truncate/tojson 等,注意 |safe 慎用。
  • 模板继承:extends + block 复用页面框架;宏(macro)复用片段。
  • 组织实践:templates 下按蓝图分目录,base.html 做框架,模板保持"薄"。
  • 传参:render_template 的键值对应模板变量,名字必须一致。
  • 调试:TemplateNotFound/UndefinedError 是最常见两类模板报错。

服务端渲染 vs 客户端渲染

学会模板后,你会接触到一个架构层面的概念:服务端渲染(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 就是问题模板。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U