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

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


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

Jinja2 是 Flask 生态下默认集成、开箱即用的现代模板引擎,专门负责把后端数据「揉」进 HTML 骨架里。

和纯 Python 字符串拼接写 HTML 相比,它用了清晰的两种分隔符区分「展示内容」和「执行逻辑」,可读性、可维护性直接拉满:

  • {{ 变量/表达式 }} → 输出转义后的变量值
  • {% 逻辑/控制结构 %} → 执行条件、循环、宏这些
# Flask 会自动在根目录下的 `templates/` 文件夹找同名模板
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>
  <!-- 方括号字典索引(适合带特殊字符的键) -->
  <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>
    <li>默认值:{{ bio|default("这位博主还没写简介") }}</li>
    <li>数字四舍五入:{{ price|round(1) }}</li>
    <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.xxx

Jinja2 自动给每个循环生成了方便的内置变量,帮你处理序号、首尾判断这些

<section>
  <h2>带专属效果的文章列表</h2>
  <div class="article-cards">
    {% for article in articles %}
      <div class="card {% if loop.first %}highlight{% endif %}">
        <!-- 序号类 -->
        <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>
    <p>{{ article.summary|truncate(100) }}</p> {# truncate 也是常用过滤器:截断文本到 100 字符,默认加省略号 #}
    {% 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 默认会自动把输出的 HTML 特殊字符(比如 <>&")转义成实体,防止恶意用户输入的 <script> 等代码被浏览器执行

<!-- 假设后端传了: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 解析,直接原封不动输出

<section>
  <h2>Jinja2 语法示例</h2>
  <pre>
    {% raw %}
      这里的 {{ 变量 }} 和 {% if %} 控制结构不会被解析
      原封不动显示给用户看
    {% endraw %}
  </pre>
</section>

7. 快速小结

Jinja2 核心语法(基础篇):

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

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


🔗 扩展阅读