Flask 模板渲染
在 Python 代码中拼接 HTML 字符串既繁琐又危险(容易产生 XSS 漏洞),Flask 集成了 Jinja2 模板引擎来解决这个问题。
模板让你将业务逻辑和页面展示分离,写出更安全、更易维护的代码。
render_template 基础
render_template() 是 Flask 中最常用的模板渲染函数。
它的第一个参数是模板文件名,后面的关键字参数会作为变量传递给模板。
实例
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def index():
# 渲染 templates/index.html 模板,传递 title 和 name 变量
return render_template("index.html", title="RUNOOB 首页", name="World")
@app.route("/user/<username>")
def profile(username):
# 将 URL 中的 username 传递给模板
return render_template("user.html", username=username, posts=[
{"title": "Flask 入门", "date": "2026-05-01"},
{"title": "Jinja2 模板", "date": "2026-05-10"},
])
模板文件放在项目目录下的 templates/ 文件夹中:
runoob-flask-test/
├── app.py
└── templates/
├── index.html
└── user.html
index.html 内容为:
实例( index.html )
<!DOCTYPE html>
<html>
<head>
<title>{{ title }} - RUNOOB</title>
</head>
<body>
<h1>{{ title }} </h1>
<p>Hello {{ name }}。</p>
</body>
</html>
访问 http://127.0.0.1:5000/ ,输出结果为:

Jinja2 基础语法
Jinja2 模板中三种核心语法:
| 语法 | 用途 | 示例 |
|---|---|---|
| {{ ... }} | 输出变量值,自动转义 HTML | {{ username }} |
| {% ... %} | 控制语句(if、for、block 等) | {% if user %} |
| {# ... #} | 注释,不会出现在渲染结果中 | {# 这是注释 #} |
模板示例
实例( user.html )
<!DOCTYPE html>
<html>
<head>
<title>{{ username }} - RUNOOB</title>
</head>
<body>
<h1>{{ username }} 的个人主页</h1>
<!-- 条件判断:如果 posts 列表为空,显示提示信息 -->
{% if posts %}
<h2>最近文章:</h2>
<ul>
<!-- 循环遍历 posts 列表 -->
{% for post in posts %}
<!-- loop.index 是 Jinja2 内置变量,从 1 开始的序号 -->
<li>{{ loop.index }}. {{ post.title }} ({{ post.date }})</li>
{% endfor %}
</ul>
{% else %}
<p>暂无文章</p>
{% endif %}
<!-- 使用过滤器:upper 将文本转为大写 -->
<p>用户名大写:{{ username|upper }}</p>
</body>
</html>
常用过滤器
过滤器用于修改变量的输出格式,使用管道符 | 调用。
| 过滤器 | 作用 | 示例 | 输出 |
|---|---|---|---|
| upper | 转大写 | {{ "hello"|upper }} | HELLO |
| lower | 转小写 | {{ "HELLO"|lower }} | hello |
| title | 首字母大写 | {{ "hello world"|title }} | Hello World |
| length | 获取长度 | {{ [1,2,3]|length }} | 3 |
| default | 设置默认值 | {{ name|default("匿名") }} | 匿名(当 name 为空时) |
| safe | 标记为安全 HTML(不转义) | {{ "粗体"|safe }} | 渲染为粗体文字 |
| join | 连接列表 | {{ ["a","b"]|join(",") }} | a,b |
安全警告:safe 过滤器会关闭自动转义,只在绝对信任数据来源时使用。对于用户输入的内容,永远不要使用 safe。
自动转义——XSS 防护
Jinja2 默认会对所有变量输出进行 HTML 转义,这是 Web 安全的一道关键防线。
例如,如果用户提交的用户名是 <script>alert("xss")</script>,模板渲染时会自动转换为安全文本:
<!-- 模板中直接使用变量 -->
<p>{{ username }}</p>
<!-- 渲染结果(HTML 特殊字符已被转义) -->
<p>&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;</p>
转义适用于 .html、.htm、.xml、.xhtml、.svg 结尾的模板文件。
模板继承——消除重复代码
模板继承是 Jinja2 最强大的功能之一,它让你可以定义一个「基础布局」,然后通过子模板填充内容。
这避免了在每个页面中重复编写头部、导航、页脚等公共部分。
基础模板(父模板)
实例
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}RUNOOB Flask 教程{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<header>
<h1>RUNOOB Flask 教程</h1>
<nav>
<a href="/">首页</a> |
<a href="/about">关于</a>
</nav>
</header>
<main>
<!-- block 是占位区域,子模板可以填充或覆盖 -->
{% block content %}{% endblock %}
</main>
<footer>
<p>© 2026 RUNOOB.com</p>
</footer>
</body>
</html>
子模板(继承父模板)
实例
{% extends "base.html" %}
<!-- 覆盖父模板的 title block -->
{% block title %}RUNOOB 教程 - 首页{% endblock %}
<!-- 填充父模板的 content block -->
{% block content %}
<h2>欢迎来到 RUNOOB</h2>
<p>{{ greeting }}</p>
{% if user %}
<p>当前用户:{{ user }}</p>
{% else %}
<p><a href="/login">请先登录</a></p>
{% endif %}
{% endblock %}
| 标签 | 作用 |
|---|---|
| {% extends "base.html" %} | 声明当前模板继承自 base.html(必须放在第一行) |
| {% block name %}...{% endblock %} | 定义一个可被子模板覆盖的区域 |
| {{ super() }} | 在子模板 block 中调用父模板同名 block 的内容 |
使用模板继承后,你只需要修改 base.html 一处,所有页面的公共部分(如导航、页脚)都会自动更新。
模板中的内置对象
以下 Flask 对象在模板中可以直接使用,无需通过 render_template() 传递:
| 对象 | 说明 | 模板中使用示例 |
|---|---|---|
| request | 当前请求对象 | {{ request.path }} |
| session | 当前会话数据 | {{ session.get("username") }} |
| g | 请求级全局变量 | {{ g.user }} |
| config | 应用配置 | {{ config["APP_NAME"] }} |
| url_for() | URL 生成函数 | {{ url_for("index") }} |
| get_flashed_messages() | 获取 flash 消息 | {% for msg in get_flashed_messages() %}... |
包含其他模板——include
使用 {% include %} 可以在一个模板中嵌入另一个模板:
实例
<nav style="background:#f0f0f0;padding:10px;">
<a href="/">首页</a> |
<a href="/posts">文章</a> |
<a href="/about">关于</a>
</nav>
实例
<!DOCTYPE html>
<html>
<head><title>页面</title></head>
<body>
{% include "_navbar.html" %}
<h1>页面内容</h1>
<p>这是页面主体内容。</p>
</body>
</html>
最佳实践:模板继承(extends)用于页面级的布局复用,include 用于组件级的代码复用。导航栏、侧边栏等可复用组件适合用 include。
完整示例:博客首页
将以上知识整合到一个实际场景中:
实例
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def index():
# 模拟数据库中的文章数据
articles = [
{"id": 1, "title": "Flask 入门指南", "author": "runoob", "views": 1024},
{"id": 2, "title": "Jinja2 模板详解", "author": "admin", "views": 512},
{"id": 3, "title": "RESTful API 设计", "author": "runoob", "views": 256},
]
return render_template("blog.html", articles=articles)
实例
{% extends "base.html" %}
{% block title %}RUNOOB 博客{% endblock %}
{% block content %}
<h2>最新文章</h2>
{% if articles %}
{% for article in articles %}
<!-- 根据浏览数添加不同的样式 -->
<div class="article" {% if article.views > 1000 %}style="background:#fff3cd;"{% endif %}>
<h3>{{ article.title }}</h3>
<p>作者:{{ article.author }} | 浏览:{{ article.views }}</p>
<!-- 使用 loop 变量实现分隔线 -->
{% if not loop.last %}
<hr>
{% endif %}
</div>
{% endfor %}
{% else %}
<p>暂时没有文章,请稍后再来。</p>
{% endif %}
{% endblock %}
Jinja2 特殊变量
| 变量 | 说明 |
|---|---|
| loop.index | 当前循环索引,从 1 开始 |
| loop.index0 | 当前循环索引,从 0 开始 |
| loop.first | 是否为第一次循环 |
| loop.last | 是否为最后一次循环 |
| loop.length | 序列总长度 |
