4、Jinja2 模板初探:用 HTML 模板渲染动态内容,告别字符串拼接

说实话,我刚开始写 Web 应用的时候,也干过那种「傻事」——直接在 Python 代码里拼 HTML 字符串。

你想想看,一个简单的用户列表,我得这么写:

html = "<ul>"
for user in users:
    html += f"<li>{user['name']} - {user['email']}</li>"
html += "</ul>"
return html

看着还行?等页面复杂起来,你就知道有多痛苦了。条件判断、循环嵌套、特殊字符转义……代码乱成一锅粥。我在一个老项目里接手过这种代码,改一个样式得翻半天,简直想哭。

后来我遇到了 Jinja2,嗯,世界清净了。

Jinja2 是什么?

说白了,Jinja2 就是一个模板引擎。它让你把 HTML 结构和 Python 数据分开管理。

模板文件长这样:

<!-- user_list.html -->
<ul>
{% for user in users %}
    <li>{{ user.name }} - {{ user.email }}</li>
{% endfor %}
</ul>

Python 代码里只需要传数据:

from flask import render_template

@app.route('/users')
def users():
    users_data = [
        {'name': '张三', 'email': 'zhangsan@example.com'},
        {'name': '李四', 'email': 'lisi@example.com'}
    ]
    return render_template('user_list.html', users=users_data)

干净、清爽、好维护。我个人习惯把模板文件放在 templates/ 目录下,Flask 会自动找到它们。

核心语法:三分钟上手

Jinja2 的语法其实就三样东西,记住它们就够了:

语法 作用 示例
{{ 变量 }} 输出变量值 {{ username }}
{% 语句 %} 控制结构(if/for) {% if user.is_admin %}
{# 注释 #} 模板注释,不会输出 {# 这里是注释 #}

举个例子,假设我们要渲染一个话题列表:

<!DOCTYPE html>
<html>
<head>
    <title>话题列表</title>
</head>
<body>
    <h1>最新话题</h1>
    
    {% if topics %}
        <ul>
        {% for topic in topics %}
            <li>
                <strong>{{ topic.title }}</strong>
                <span>作者:{{ topic.author }}</span>
                <span>时间:{{ topic.created_at }}</span>
            </li>
        {% endfor %}
        </ul>
    {% else %}
        <p>暂无话题,快来发布第一个吧!</p>
    {% endif %}
</body>
</html>

你看,{% if topics %} 判断列表是否为空,{% for topic in topics %} 循环输出每个话题。逻辑清晰,一目了然。

小技巧:Jinja2 里可以用 loop.index 获取当前循环的序号(从1开始)。比如 {{ loop.index }}. {{ topic.title }} 就能生成带编号的列表。

过滤器:给数据加点「魔法」

有时候我们想对变量做点处理,比如把时间格式化成更友好的样子,或者把文本截断。Jinja2 的过滤器就是干这个的。

用法很简单:{{ 变量 | 过滤器 }}

<p>{{ content | truncate(50) }}</p>  <!-- 截断到50个字符 -->
<p>{{ created_at | datetimeformat('%Y-%m-%d') }}</p>  <!-- 格式化日期 -->
<p>{{ username | upper }}</p>  <!-- 转大写 -->

常用的内置过滤器有这些:

  • safe — 标记为安全 HTML,不转义
  • capitalize — 首字母大写
  • length — 获取长度
  • default('默认值') — 变量不存在时使用默认值
  • join(', ') — 把列表拼接成字符串
注意:默认情况下,Jinja2 会自动转义 HTML 特殊字符(比如 < 变成 &lt;)。这是为了防止 XSS 攻击。如果你确定内容是安全的,可以用 | safe 过滤器。我曾经见过有人滥用 | safe,结果用户提交的脚本直接执行了……嗯,别学他。

模板继承:告别重复代码

每个页面都有相同的头部、导航栏、底部版权信息。如果每个模板都复制一遍,改个链接就得改所有文件——太傻了。

Jinja2 的模板继承就是解决这个问题的。

先写一个基础模板 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="/topics">话题列表</a>
        <a href="/publish">发布话题</a>
    </nav>
    
    <main>
        {% block content %}{% endblock %}
    </main>
    
    <footer>
        <p>&copy; 2025 话题发布器</p>
    </footer>
</body>
</html>

然后子模板只需要填充 block 里的内容:

{% extends "base.html" %}

{% block title %}发布新话题 - {{ super() }}{% endblock %}

{% block content %}
    <h1>发布新话题</h1>
    <form method="POST">
        <input type="text" name="title" placeholder="话题标题">
        <textarea name="content" placeholder="话题内容"></textarea>
        <button type="submit">发布</button>
    </form>
{% endblock %}

{% extends "base.html" %} 告诉 Jinja2 继承哪个模板。{% block 名称 %} 定义可替换的区域。{{ super() }} 可以保留父模板中 block 的原有内容。

这样一来,改导航栏只需要改 base.html,所有页面自动更新。我在项目中一直用这个模式,省了不知道多少时间。

实战:渲染话题发布页面

结合我们之前学的 Flask 路由,来看看完整的流程:

# app.py
from flask import Flask, render_template, request, redirect, url_for

app = Flask(__name__)

# 模拟数据存储
topics = []

@app.route('/')
def index():
    return render_template('index.html', topics=topics)

@app.route('/publish', methods=['GET', 'POST'])
def publish():
    if request.method == 'POST':
        title = request.form.get('title')
        content = request.form.get('content')
        topics.append({
            'title': title,
            'content': content,
            'author': '匿名用户',
            'created_at': '2025-01-15'
        })
        return redirect(url_for('index'))
    return render_template('publish.html')

if __name__ == '__main__':
    app.run(debug=True)

对应的 publish.html 模板:

{% extends "base.html" %}

{% block content %}
    <h2>发布新话题</h2>
    <form method="POST">
        <div>
            <label>标题:</label>
            <input type="text" name="title" required>
        </div>
        <div>
            <label>内容:</label>
            <textarea name="content" rows="5" required></textarea>
        </div>
        <button type="submit">发布</button>
    </form>
{% endblock %}

你看,模板里没有一行 Python 代码,全是干净的 HTML。数据通过 render_template 的第二个参数传进去,Jinja2 自动渲染。

本章知识体系

下面这张图帮你理清 Jinja2 的核心脉络:

Jinja2 模板引擎 三种核心语法 {{ 变量 }} {% 语句 %} {# 注释 #} 过滤器 | safe | truncate(50) | default('无') 模板继承 {% extends %} {% block 名称 %} {{ super() }} 与 Flask 结合:render_template() 告别字符串拼接,拥抱模板渲染

核心要点:

  • 用 {{ }} 输出变量,用 {% %} 写逻辑
  • 过滤器让数据展示更灵活
  • 模板继承消除重复代码
  • Flask 的 render_template() 自动加载模板目录下的文件

好了,现在你已经掌握了 Jinja2 的核心用法。从今天开始,别再在 Python 里拼 HTML 字符串了——那真的是在给自己挖坑。

可可精品资料库