Flask 模板渲染新手实战指南
本文为「从零开始的技术教程」,面向完全没有 Flask 模板开发经验的新手,手把手带你掌握模板渲染的完整开发流程。
WEB项目地址:演示地址
① 零基础环境搭建与项目初始化
第一步:安装 Flask
在终端中执行以下命令安装 Flask:
bash
pip install flask
安装完成后,在 Python 终端输入 import flask 无报错即表示安装成功。
第二步:创建项目与模板文件夹
Flask 有一个默认约定:模板文件必须放在项目根目录下的 templates 文件夹中 (名称必须为 templates,首字母小写),否则 Flask 找不到模板。
创建如下项目结构:
my_flask_app/
├── app.py
└── templates/ # 模板文件夹,名称必须为 templates
└── index.html
第三步:编写第一个模板
在 templates/index.html 中写入:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>我的第一个模板</title>
</head>
<body>
<h1>欢迎来到 Flask!</h1>
<p>这是我的第一个模板页面。</p>
</body>
</html>
第四步:在视图函数中渲染模板
在 app.py 中写入:
python
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def home():
return render_template('index.html')
if __name__ == '__main__':
app.run(debug=True)
运行 python app.py,访问 http://127.0.0.1:5000,就能看到渲染后的 HTML 页面。
② Jinja2 模板语法核心概念速览
Flask 默认使用 Jinja2 模板引擎,它允许在 HTML 中嵌入 Python 风格的语法,实现动态内容生成。
Jinja2 有 三种核心语法,务必牢记:
| 语法 | 用途 | 示例 |
|---|---|---|
{``{ 变量名 }} |
输出变量的值 | {``{ name }} → 显示用户姓名 |
{% 语句 %} |
执行控制逻辑(循环、条件等) | {% for user in users %} |
{# 注释 #} |
模板中的注释,不会渲染到页面 | {# 这是个人信息模块 #} |
模板引擎的工作原理 :render_template() 函数在 templates 目录查找指定模板文件 → 解析模板中的变量和逻辑 → 将 Python 传入的数据注入模板 → 生成最终的 HTML 响应返回给浏览器。
💡 模板渲染的核心价值在于 "数据与页面分离" :HTML 代码放在模板文件中负责结构和样式,Python 视图函数负责准备数据,前后端各司其职、互不干扰。
③ 基础变量传递与页面动态渲染
传递单个变量
在视图函数中通过 render_template() 的关键字参数传递数据:
python
@app.route('/')
def home():
username = '张三'
return render_template('index.html', username=username)
在模板中用 {``{ username }} 接收并显示:
html
<!DOCTYPE html>
<html>
<head>
<title>Hello, {{ username }}</title>
</head>
<body>
<h1>Hello, {{ username }}!</h1>
</body>
</html>
传递多个变量
可以同时传递任意数量的变量:
python
@app.route('/user/<username>')
def user_profile(username):
user_data = {
'name': username,
'age': 25,
'email': f'{username}@example.com'
}
return render_template('user.html', user=user_data, title='个人资料')
在模板中访问字典属性有两种方式:
html
<p>用户名: {{ user['name'] }}</p>
<p>年龄: {{ user.get('age', 18) }}</p> <!-- 带默认值 -->
<p>邮箱: {{ user.email }}</p> <!-- 点号方式更简洁 -->
访问列表/元组中的元素:
html
<p>第一篇文章: {{ posts[0].title }}</p>
<p>最后一篇: {{ posts[-1].title }}</p>
访问 Flask 内置对象:
html
<p>调试模式: {{ config.DEBUG }}</p>
<p>请求方法: {{ request.method }}</p>
<p>用户ID: {{ session.get('user_id') }}</p>
④ 流程控制语句在模板中的实战应用
条件判断 (if 语句)
在模板中根据条件展示不同内容:
html
<h1>{{ user.name }} 的个人主页</h1>
{% if user.age >= 18 %}
<p>✅ 成年用户</p>
{% else %}
<p>🔞 未成年用户</p>
{% endif %}
{% if user.is_vip %}
<span>⭐ VIP 用户</span>
{% endif %}
循环遍历 (for 语句)
遍历列表并渲染每一项:
html
<h1>商品列表</h1>
{% if products %}
<ul>
{% for product in products %}
<li>
{{ product.name }} - ¥{{ product.price }}
{% if loop.first %}
<span>🔥 热门</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p>暂无商品</p>
{% endif %}
循环中的特殊变量(Jinja2 内置):
| 变量 | 说明 |
|---|---|
loop.index |
当前循环次数(从 1 开始) |
loop.index0 |
当前循环次数(从 0 开始) |
loop.first |
是否为第一次循环 |
loop.last |
是否为最后一次循环 |
loop.length |
序列的总长度 |
⑤ 模板继承机制与代码复用技巧
模板继承是 Jinja2 最强大的功能之一 。它允许你创建一个包含网站所有通用元素的 "骨架"模板,然后让其他模板继承并填充内容。
第一步:创建基础模板 templates/base.html
基础模板定义页面的整体结构,并用 {% block %} 标记可被覆盖的区域:
html
<!DOCTYPE html>
<html>
<head>
{% block head %}
<title>{% block title %}{% endblock %} - 我的网站</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
{% endblock %}
</head>
<body>
<nav>
<a href="/">首页</a>
<a href="/about">关于</a>
</nav>
<div id="content">
{% block content %}{% endblock %}
</div>
<div id="footer">
{% block footer %}
© 2026 我的网站
{% endblock %}
</div>
</body>
</html>
第二步:创建子模板继承基础模板
子模板通过 {% extends %} 声明继承关系,并用同名 {% block %} 填充内容:
html
<!-- templates/about.html -->
{% extends "base.html" %}
{% block title %}关于我们{% endblock %}
{% block content %}
<h1>关于我们</h1>
<p>这是关于页面的内容。</p>
{% endblock %}
⚠️ 关键规则:
{% extends %}必须是模板中的第一个标签- 如果要在子模板中保留 父模板 block 的原有内容,使用
{``{ super() }}
第三步:视图函数渲染子模板
python
@app.route('/about')
def about():
return render_template('about.html') # 渲染的是子模板!
访问 /about 时,Flask 会自动将子模板的内容填充到基础模板的对应 block 中,生成完整的 HTML 页面。
⑥ 静态资源加载与表单数据处理
6.1 静态资源加载(CSS、JavaScript、图片)
Flask 约定:所有静态文件(CSS、JavaScript、图片等)应放在项目根目录的 static 文件夹中。
项目结构:
my_flask_app/
├── app.py
├── templates/
│ ├── base.html
│ └── index.html
└── static/
├── css/
│ └── style.css
├── js/
│ └── script.js
└── images/
└── logo.png
在模板中加载静态文件 :使用 url_for('static', filename='...') 动态生成 URL:
html
<head>
<title>{{ title }}</title>
<!-- 加载 CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<!-- 加载图片 -->
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">
<!-- 加载 JavaScript -->
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
</body>
💡
url_for('static', filename='...')会智能生成正确的静态文件 URL,确保开发和生产环境都能正常加载。
6.2 模板中的表单数据处理
在模板中创建表单,指定提交的 URL 和方法:
html
<!-- templates/login.html -->
{% extends "base.html" %}
{% block content %}
<h1>登录</h1>
<form action="{{ url_for('login') }}" method="POST">
<label>用户名:</label>
<input type="text" name="username" required>
<label>密码:</label>
<input type="password" name="password" required>
<button type="submit">登录</button>
</form>
{% endblock %}
对应的视图函数处理 GET 和 POST 请求:
python
from flask import request, redirect, url_for
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
# 验证逻辑...
return f'欢迎, {username}!'
return render_template('login.html')
⑦ 自定义过滤器与全局上下文注入
7.1 自定义过滤器
过滤器可以对模板中的变量进行格式化处理。Jinja2 提供了丰富的内置过滤器,你也可以自定义。
内置过滤器示例:
html
<p>{{ message | upper }}</p> <!-- 转大写 -->
<p>{{ message | lower }}</p> <!-- 转小写 -->
<p>{{ price | round(2) }}</p> <!-- 四舍五入保留2位小数 -->
<p>{{ users | length }}</p> <!-- 计算列表长度 -->
<p>{{ content | truncate(50) }}</p> <!-- 截断到50个字符 -->
自定义过滤器 :使用 @app.template_filter() 装饰器:
python
@app.template_filter('reverse')
def reverse_filter(s):
"""字符串反转过滤器"""
return s[::-1]
@app.template_filter('format_date')
def format_date_filter(dt):
"""日期格式化过滤器"""
return dt.strftime('%Y年%m月%d日')
在模板中使用自定义过滤器:
html
<p>{{ 'hello' | reverse }}</p> <!-- 输出: olleh -->
<p>{{ created_at | format_date }}</p> <!-- 输出: 2026年07月30日 -->
7.2 全局上下文注入(上下文处理器)
如果某些数据需要在所有模板 中都能访问(如网站名称、当前年份、当前登录用户等),可以使用 上下文处理器。
python
@app.context_processor
def inject_globals():
return {
'site_name': '我的博客',
'current_year': 2026,
'is_logged_in': True
}
定义后,这些变量在所有模板中都可以直接使用,无需在每个视图函数中重复传递:
html
<footer>
<p>© {{ current_year }} {{ site_name }}. All rights reserved.</p>
</footer>
⑧ 典型报错分析与调试排查手册
报错 1:jinja2.exceptions.TemplateNotFound: index.html
原因 :Jinja2 在 templates 文件夹中找不到指定的模板文件。
排查步骤:
- 检查文件夹名称 :必须是
templates(首字母小写,复数形式) - 检查文件路径 :模板文件是否确实在
templates/目录下 - 检查文件名 :是否与
render_template()中引用的名称完全一致(包括大小写) - 检查文件是否存在:确认文件确实存在于项目中
解决方案 :如果模板不在默认的 templates 文件夹,可以手动指定:
python
app = Flask(__name__, template_folder='your_templates_folder')
报错 2:模板变量未显示(显示为空或原样输出)
原因:
- 变量名拼写错误(视图函数中传递的是
username,模板中写的是user_name) - 忘记在
render_template()中传递该变量
解决方案 :检查视图函数和模板中的变量名是否完全一致。
报错 3:静态文件 404 错误
原因:
static文件夹名称拼写错误- 静态文件路径不正确
- 使用了硬编码路径而非
url_for()
解决方案:
- 确认文件夹名称为
static - 始终使用
{``{ url_for('static', filename='...') }}生成 URL - 检查
filename参数中的路径是否正确(如css/style.css而非/static/css/style.css)
报错 4:模板语法错误
原因:
- 忘记闭合标签(如
{% if %}没有对应的{% endif %}) - 使用了错误的语法(如用
{``{ }}包裹控制语句)
调试技巧:
- 开启调试模式
app.run(debug=True)查看详细错误信息 - 启用模板自动重载
app.config['TEMPLATES_AUTO_RELOAD'] = True - 使用
try-except捕获渲染异常
⑨ 模板安全规范与 XSS 防护策略
9.1 自动转义机制(第一道防线)
Flask 默认使用 Jinja2 模板引擎,Jinja2 会自动对模板中的变量进行 HTML 转义,这是防御 XSS 攻击的第一道防线。
html
<!-- 假设 user_input = '<script>alert("XSS")</script>' -->
<p>{{ user_input }}</p>
<!-- 实际输出: <script>alert("XSS")</script> -->
<!-- 浏览器不会执行脚本,只会显示为普通文本 -->
9.2 安全地渲染 HTML 内容
如果确实需要渲染包含 HTML 标签的内容(如富文本编辑器输出的内容),有两种安全方式:
方式一:使用 |safe 过滤器(需确保内容绝对安全):
html
<p>{{ trusted_html_content | safe }}</p>
⚠️ 警告 :|safe 会禁用自动转义 ,仅当内容来源完全可信时才可使用。
方式二:使用 Markup 类:
python
from flask import Markup
@app.route('/')
def index():
# 仅在确认内容安全后使用
safe_html = Markup('<strong>这是安全的加粗文本</strong>')
return render_template('index.html', content=safe_html)
9.3 额外的安全措施
-
输入验证:在处理用户输入时,始终验证和过滤,只接受预期格式的数据
-
设置 CSP 头 :限制浏览器可加载的资源来源
python@app.after_request def apply_csp(response): response.headers['Content-Security-Policy'] = "default-src 'self'" return response -
使用 HttpOnly Cookie :防止 JavaScript 访问会话 Cookie
pythonapp.config['SESSION_COOKIE_HTTPONLY'] = True app.config['SESSION_COOKIE_SECURE'] = True
9.4 特殊注意事项
Jinja2 自动转义无法防御 a 标签 href 属性中的 javascript: URI 攻击:
html
<!-- 危险!即使用 |safe 也不应这样做 -->
<a href="{{ user_provided_link | safe }}">点击</a>
解决方案 :对 URL 进行验证,只允许 http:// 和 https:// 协议的链接。
⑩ 从 Demo 到实战:构建完整个人页
现在让我们综合运用所有知识,从零构建一个完整的个人主页。
第一步:创建项目结构
my_profile/
├── app.py
├── templates/
│ ├── base.html
│ └── profile.html
└── static/
├── css/
│ └── style.css
└── images/
└── avatar.png
第二步:创建基础模板 templates/base.html
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}个人主页{% endblock %} - {{ site_name }}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
{% block extra_head %}{% endblock %}
</head>
<body>
<nav class="navbar">
<div class="container">
<a href="/" class="brand">{{ site_name }}</a>
<ul class="nav-links">
<li><a href="/">首页</a></li>
<li><a href="/profile">个人资料</a></li>
</ul>
</div>
</nav>
<main class="container">
{% block content %}{% endblock %}
</main>
<footer class="footer">
<div class="container">
<p>© {{ current_year }} {{ site_name }}. 保留所有权利。</p>
</div>
</footer>
</body>
</html>
第三步:创建个人页模板 templates/profile.html
html
{% extends "base.html" %}
{% block title %}{{ user.name }}的个人资料{% endblock %}
{% block content %}
<div class="profile-card">
<div class="avatar">
<img src="{{ url_for('static', filename='images/avatar.png') }}" alt="头像">
</div>
<h1>{{ user.name }}</h1>
<p class="bio">{{ user.bio }}</p>
<div class="info-grid">
<div class="info-item">
<span class="label">年龄</span>
<span class="value">{{ user.age }} 岁</span>
</div>
<div class="info-item">
<span class="label">职业</span>
<span class="value">{{ user.job }}</span>
</div>
<div class="info-item">
<span class="label">邮箱</span>
<span class="value">{{ user.email }}</span>
</div>
</div>
<div class="skills">
<h3>技能专长</h3>
<ul>
{% for skill in user.skills %}
<li>{{ skill }}</li>
{% endfor %}
</ul>
</div>
{% if user.is_vip %}
<div class="vip-badge">⭐ VIP 会员</div>
{% endif %}
</div>
{% endblock %}
第四步:编写视图函数 app.py
python
from flask import Flask, render_template
from datetime import datetime
app = Flask(__name__)
# 上下文处理器:全局变量注入
@app.context_processor
def inject_globals():
return {
'site_name': '我的个人空间',
'current_year': datetime.now().year
}
# 自定义过滤器
@app.template_filter('format_date')
def format_date_filter(dt):
return dt.strftime('%Y年%m月%d日')
@app.route('/')
def home():
return render_template('profile.html', user=user_data)
@app.route('/profile')
def profile():
return render_template('profile.html', user=user_data)
# 模拟用户数据
user_data = {
'name': '李明',
'age': 28,
'job': 'Python 全栈开发工程师',
'email': 'liming@example.com',
'bio': '热爱编程、摄影与户外运动。致力于用技术创造有价值的产品。',
'skills': ['Python', 'Flask', 'JavaScript', 'Docker', 'Linux'],
'is_vip': True
}
if __name__ == '__main__':
app.run(debug=True)
第五步:添加 CSS 样式 static/css/style.css
css
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f7fa;
color: #333;
line-height: 1.6;
}
.container { max-width: 960px; margin: 0 auto; padding: 0 20px; }
/* 导航栏 */
.navbar {
background: #fff;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
padding: 15px 0;
position: sticky;
top: 0;
z-index: 100;
}
.navbar .container {
display: flex;
justify-content: space-between;
align-items: center;
}
.brand {
font-size: 24px;
font-weight: bold;
color: #4a6cf7;
text-decoration: none;
}
.nav-links {
list-style: none;
display: flex;
gap: 30px;
}
.nav-links a {
text-decoration: none;
color: #555;
font-weight: 500;
}
.nav-links a:hover { color: #4a6cf7; }
/* 个人卡片 */
.profile-card {
background: #fff;
border-radius: 16px;
padding: 40px;
margin: 40px 0;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
text-align: center;
}
.avatar img {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
border: 4px solid #4a6cf7;
}
.profile-card h1 {
margin: 20px 0 10px;
font-size: 28px;
}
.bio {
color: #777;
font-size: 16px;
max-width: 500px;
margin: 0 auto 30px;
}
/* 信息网格 */
.info-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin: 30px 0;
padding: 20px 0;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
}
.info-item .label {
display: block;
font-size: 12px;
color: #999;
text-transform: uppercase;
letter-spacing: 1px;
}
.info-item .value {
font-size: 18px;
font-weight: 600;
color: #222;
}
/* 技能标签 */
.skills { margin: 30px 0; }
.skills h3 { margin-bottom: 15px; color: #444; }
.skills ul {
list-style: none;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
}
.skills li {
background: #eef2ff;
color: #4a6cf7;
padding: 6px 18px;
border-radius: 20px;
font-size: 14px;
font-weight: 500;
}
/* VIP 徽章 */
.vip-badge {
display: inline-block;
background: linear-gradient(135deg, #f7971e, #ffd200);
color: #fff;
padding: 6px 24px;
border-radius: 20px;
font-weight: bold;
font-size: 14px;
margin-top: 10px;
}
/* 页脚 */
.footer {
text-align: center;
padding: 30px 0;
color: #999;
font-size: 14px;
border-top: 1px solid #eee;
}
运行与预览:
执行 python app.py,访问 http://127.0.0.1:5000,你将看到一个完整的、带有样式和动态数据的个人主页。