Flask 模板渲染新手实战指南

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>&copy; {{ current_year }} {{ site_name }}. All rights reserved.</p>
</footer>

⑧ 典型报错分析与调试排查手册

报错 1:jinja2.exceptions.TemplateNotFound: index.html

原因 :Jinja2 在 templates 文件夹中找不到指定的模板文件。

排查步骤

  1. 检查文件夹名称 :必须是 templates(首字母小写,复数形式)
  2. 检查文件路径 :模板文件是否确实在 templates/ 目录下
  3. 检查文件名 :是否与 render_template() 中引用的名称完全一致(包括大小写)
  4. 检查文件是否存在:确认文件确实存在于项目中

解决方案 :如果模板不在默认的 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>
<!-- 实际输出: &lt;script&gt;alert("XSS")&lt;/script&gt; -->
<!-- 浏览器不会执行脚本,只会显示为普通文本 -->
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

    python 复制代码
    app.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>&copy; {{ 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,你将看到一个完整的、带有样式和动态数据的个人主页。

相关推荐
IT_陈寒15 小时前
Vite静态资源路径这个坑差点让我加班到凌晨
前端·人工智能·后端
神经蛙199615 小时前
🌍 别再硬编码中文了!Python Web 项目国际化(i18n)完全指南
后端·python
二月龙15 小时前
Spring 事务失效的 8 种场景,很多老手依然频繁踩雷
后端
掘金酱15 小时前
「TRAE Work 实战帮」征文启动!你沉淀的经验,值得被看见!
前端·人工智能·后端
颜酱15 小时前
14 | 验证并修正 LLM 生成的 SQL
人工智能·python
长大198815 小时前
MyBatis 常见性能陷阱:N+1 查询、一级缓存踩坑解决方案
后端
用户18615580086016 小时前
MinIO Java 对接试用:从连接、上传到下载的完整示例
后端
颜酱16 小时前
13 | 使用 LangChain 生成 SQL
人工智能·python·langchain
爱勇宝16 小时前
DeepSeek V4-Flash 更新:代码与 Agent 能力全面增强
前端·后端·deepseek