Jinja2 模板引擎(上):变量渲染、循环与条件判断

📂 所属阶段:第一阶段 — 破冰启航(基础篇)
🔗 相关章节:路由(Routing)艺术 · Jinja2 模板引擎(下)


1. 先搞懂:Jinja2 是个啥?为啥要用?

Jinja2 是 Flask 生态里默认集成、开箱即用的模板引擎。简单说,它的任务就是帮你把后端 Python 拿到的数据「塞」进 HTML 骨架里,生成用户最终看到的网页。

用传统方式在 Python 里直接拼 HTML 字符串,不仅代码乱成一团,还容易出 XSS 安全问题。Jinja2 用两套清晰的标记,把「要展示的内容」和「控制逻辑」彻底分开:

  • {{ 变量 / 表达式 }} — 告诉引擎:这里输出一个值(自动转义,防攻击)
  • {% 逻辑 / 控制结构 %} — 告诉引擎:这里执行条件/循环/宏定义

下面这段 Flask 代码,展示了最典型的用法:

# Flask 会自动去项目根目录下的 templates/ 文件夹找同名 HTML 文件
from flask import Flask, render_template

app = Flask(__name__)

@app.route("/")
def index():
    # 用关键字参数把数据传给模板,也可以用 **context 解包字典
    return render_template(
        "index.html",
        page_title="我的 Flask 博客首页",
        current_user={"name": "Alice", "is_logged_in": True}
    )

2. 第一步:基础变量渲染

把后端数据「贴」到页面上,就是模板最基础的任务。

2.1 简单变量、属性、字典索引

Jinja2 访问数据的方式和 Python 一样直观,无论属性还是字典键,点号或者方括号都行:

<!-- templates/index.html -->
<header>
  <h1>{{ page_title }}</h1>
</header>
<main>
  <!-- 点号访问属性或字典键(两者通用) -->
  <p>Hi, {{ current_user.name }} 👋</p>
  <!-- 方括号语法适合带特殊字符的键,比如 "user_id" -->
  <p>邮箱前缀:{{ current_user["user_id"] }}</p>
</main>

2.2 实用过滤器(Filters):给变量「一键加工」

过滤器,就是 Jinja2 内置的一套变量处理小工具,像 Linux 管道符 | 一样可以链式叠加。

假设后端传了这些数据:
name=" alice ", price=29.99, bio=None, tags=["Flask", "Jinja2"]

<section>
  <h2>变量处理小工具</h2>
  <ul>
    <li>全大写:{{ name|upper }}</li>
    <!-- 先去首尾空格 → 再首字母大写 -->
    <li>首字母大写:{{ name|trim|capitalize }}</li>
    <!-- 为空时设置默认值 -->
    <li>默认值:{{ bio|default("这位博主还没写简介") }}</li>
    <li>数字四舍五入:{{ price|round(1) }}</li>
    <!-- ⚠️ safe 仅用于绝对可信内容,比如你自己写的静态文案 -->
    <li>安全 HTML:{{ "<b>这是加粗的安全内容</b>"|safe }}</li>
    <li>去除 HTML 标签:{{ "<i>我是有HTML标签内容</i>"|striptags }}</li>
    <li>列表连接成字符串:{{ tags|join(" · ") }}</li>
    <li>变量长度:{{ tags|length }}</li>
  </ul>
</section>

2.3 高频内置过滤器速查表

过滤器作用说明示例与预期结果
upper / lower全转大写 / 小写`"hello"
trim去除首尾空格、换行、制表符`" hi "
capitalize首字母大写,其余小写`"hELLO"
default(val)变量未定义/为空/None 时返回默认值`None
safe⚠️ 标记为「安全 HTML」,跳过 XSS 防护仅用于自己或经过审核的内容
striptags移除所有 HTML 标签`""
join(delimiter)用指定分隔符连接列表、元组或集合`[1,2,3]
round(n)四舍五入到 n 位小数`3.14159
length获取字符串、列表、字典的长度`["a","b"]
tojson⚠️ 把 Python 数据转成 JSON 字符串(给前端用)`{"a":1}

3. 逻辑控制第一步:条件判断 {% if %}

根据后端数据的不同,让模板渲染出不同的 HTML 片段。

3.1 完整分支 if / elif / else

可以嵌套使用,但为了可读性,强烈建议控制在 2-3 层以内

<!-- 假设后端传了 current_user,包含 is_logged_in、role -->
<nav>
  {% if current_user.is_logged_in %}
    <span>👤 {{ current_user.name }}</span>
    {% if current_user.role == "admin" %}
      <a href="/admin">管理后台</a>
    {% elif current_user.role == "editor" %}
      <a href="/editor">编辑后台</a>
    {% else %}
      <a href="/profile">个人中心</a>
    {% endif %}
    <a href="/logout">退出登录</a>
  {% else %}
    <a href="/login">登录</a>
    <a href="/register">注册</a>
  {% endif %}
</nav>

3.2 常用判断运算符

Jinja2 的运算符几乎和 Python 一模一样,还额外支持一些方便的「模板测试」。

<!-- 假设后端传了 articles=[...] -->
<section>
  <h2>运算符测试</h2>
  <ul>
    <!-- 普通比较和逻辑运算 -->
    {% if current_user.age is defined and current_user.age >= 18 %}
      <li>✅ 成年用户,可访问全部内容</li>
    {% endif %}
    {% if articles[0].views > 1000 and "Python" in articles[0].tags %}
      <li>🔥 Python 热门文章推荐</li>
    {% endif %}
    {% if "banned" not in current_user.roles %}
      <li>💬 可以发言评论</li>
    {% endif %}

    <!-- 模板测试用 is / is not -->
    {% if current_user.bio is none %}
      <li>ℹ️ 个人简介待完善</li>
    {% endif %}
    {% if articles is iterable %}
      <li>📝 文章列表已准备好渲染</li>
    {% endif %}
  </ul>
</section>

4. 逻辑控制核心:循环 {% for %}

遍历后端传过来的列表、字典,把数据一条条渲染出来。

4.1 基础循环:遍历列表

<!-- 假设后端传了 articles 列表 -->
<section>
  <h2>最新文章</h2>
  <ul class="article-list">
    {% for article in articles %}
      <li>
        <a href="{{ url_for('show_article', id=article.id) }}">
          {{ article.title }}
        </a>
        <span class="meta">{{ article.published_at }} · {{ article.views }} 阅读</span>
      </li>
    {% endfor %}
  </ul>
</section>

4.2 循环专属变量 loop

每个循环里 Jinja2 会悄悄给你一组 loop 变量,帮你轻松处理序号、首尾判断等常见需求。

<section>
  <h2>带专属效果的文章列表</h2>
  <div class="article-cards">
    {% for article in articles %}
      <div class="card {% if loop.first %}highlight{% endif %}">
        <!-- 从 1 开始的序号 -->
        <span class="index">#{{ loop.index }}</span>
        <!-- 反向序号(最后一篇是 #1) -->
        <span class="rev-index">(倒数第{{ loop.revindex }})</span>
        <!-- 总篇数 -->
        <span class="total">共{{ loop.length }}篇</span>
        <h3>{{ article.title }}</h3>
        <p>{{ article.summary }}</p>
      </div>
    {% endfor %}
  </div>
</section>

4.3 遍历字典:items() / keys() / values()

和 Python 一样,三种方式随你选。

<section>
  <h2>用户信息表</h2>
  <table>
    <thead>
      <tr>
        <th>属性名</th>
        <th>属性值</th>
      </tr>
    </thead>
    <tbody>
      {% for key, value in current_user.items() %}
        <tr>
          <!-- 把下划线替换成空格,让显示更友好 -->
          <td>{{ key|replace("_", " ") }}</td>
          <td>{{ value }}</td>
        </tr>
      {% endfor %}
    </tbody>
  </table>
</section>

4.4 空循环兜底:{% else %}

这里的 else 是紧跟在 for 后面的,只有当循环一次都没执行(比如列表为空)时才会触发

<section>
  <h2>草稿箱</h2>
  <ul class="draft-list">
    {% for draft in drafts %}
      <li>{{ draft.title }}</li>
    {% else %}
      <p>🎉 暂无草稿!</p>
    {% endfor %}
  </ul>
</section>

5. 代码复用利器:宏 {% macro %}

宏就像 Python 里的函数,把反复出现的 HTML 片段封装起来,一处定义,到处调用,避免复制粘贴。

5.1 定义宏

建议新建一个 templates/macros.html,把全局通用的宏统一放在里面。

<!-- templates/macros.html -->

{# 宏:渲染单个文章卡片 #}
{% macro render_article_card(article, show_published=False) %}
  <div class="card">
    <h3>{{ article.title }}</h3>
    <!-- truncate 是一个常用过滤器,截断文本并自动加省略号 -->
    <p>{{ article.summary|truncate(100) }}</p>
    {% if show_published %}
      <span class="published-at">{{ article.published_at }}</span>
    {% endif %}
    <a href="{{ url_for('show_article', id=article.id) }}" class="btn">阅读更多</a>
  </div>
{% endmacro %}

{# 宏:渲染单个表单字段,适合和 Flask-WTF 配合 #}
{% macro render_wtf_field(field) %}
  <div class="form-group">
    {{ field.label(class="form-label") }}
    {{ field(class="form-control") }}
    {% if field.errors %}
      <small class="text-danger">{{ field.errors[0] }}</small>
    {% endif %}
  </div>
{% endmacro %}

5.2 导入宏

可以导入整个文件,也可以只导入需要的宏(推荐后者,更轻量)。

<!-- templates/articles.html -->
{# 只导入需要的宏 #}
{% from "macros.html" import render_article_card %}

<section>
  <h2>精选文章</h2>
  <div class="grid">
    {% for article in featured_articles %}
      {{ render_article_card(article, show_published=True) }}
    {% endfor %}
  </div>
</section>

6. 安全第一:自动转义与块级处理

6.1 Flask 自动转义:默认防 XSS 攻击

Flask 下的 Jinja2 会自动把 HTML 特殊字符(<>&" 等)转义成安全的实体形式。这样即使后端不小心传入了恶意脚本,浏览器也不会执行,只会以文本形式显示。

<!-- 假设后端传了:malicious_content="<script>alert('恶意弹窗!')</script>" -->
<p>{{ malicious_content }}</p>
<!-- 实际渲染结果:&lt;script&gt;alert('恶意弹窗!')&lt;/script&gt;,完全不会弹窗! -->

如果你输出的内容绝对可信(例如自己写的静态文案),可以通过 |safe 过滤器手动标记:

<p>{{ "<b>Flask 真好用!</b>"|safe }}</p>
<!-- 浏览器会显示加粗文字 -->

6.2 块级免解析:{% raw %}

不想让 Jinja2 动一整块内容,只想原样输出给用户看?用 {% raw %} 包起来就行。

<section>
  <h2>Jinja2 语法示例</h2>
  <pre>
    {% raw %}
      这里的 {{ 变量 }} 和 {% if %} 都不会被解析
      完全原封不动地显示给用户
    {% endraw %}
  </pre>
</section>

7. 快速小结

Jinja2 核心语法速览(基础篇):

{{ variable }}                      输出转义后的变量
{{ name|upper }}                    变量过滤器(可以链式:name|trim|capitalize)
{% if cond %} ... {% endif %}       条件判断
{% for item in list %}              循环
{% for ... %} {% else %} {% endfor %} 循环兜底(列表为空时触发 else)
{% macro name() %}                  宏定义(代码复用)
{% from "file.html" import ... %}   导入宏
{% raw %} {% endraw %}              块级免解析
{{ url_for('func') }}               Flask 内置生成 URL 的函数
{{ session['key'] }}                 直接访问 session
{{ request.args.get('q') }}         直接访问请求参数

💡 核心最佳实践:模板里只做「展示相关的逻辑」(简单的判断、循环、变量过滤)。所有复杂的业务逻辑、数据处理,全部交给后端视图函数,或者自定义 Jinja2 扩展。这样前后端职责划分清晰,代码也更容易维护。


🔗 扩展阅读