3. Jinja2模板引擎:模板基础、变量与表达式、控制结构(if/for)、模板继承

好,咱们今天聊聊Jinja2。说实话,这是Flask里我最喜欢的部分之一。为什么?因为它让后端开发变得特别有「手感」。你想想看,以前写后端返回HTML,要么拼字符串,要么用笨重的模板。Jinja2的出现,说白了就是让Python代码和HTML页面能优雅地「对话」。

3.1 模板基础:从零开始

先说说模板是什么。模板就是一个普通的HTML文件,只不过里面加了点「料」——Jinja2的语法标记。Flask默认会在项目的 templates/ 文件夹里找模板文件。

我个人习惯的项目结构是这样的:

myapp/
├── app.py
└── templates/
    ├── index.html
    └── user.html

app.py 里,用 render_template() 来渲染模板:

from flask import Flask, render_template

app = Flask(__name__)

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

嗯,这里要注意:render_template() 会自动去 templates/ 目录下找文件。我曾经见过有人把模板放在根目录,结果死活渲染不出来——其实就是路径不对。

3.2 变量与表达式:让模板「活」起来

模板里怎么传数据?用 {{ }} 双花括号。这是Jinja2的变量输出语法。

看个例子:

# app.py
@app.route('/user/<name>')
def user(name):
    return render_template('user.html', username=name, age=28)
<!-- user.html -->
<h1>欢迎你,{{ username }}!</h1>
<p>你的年龄是:{{ age }} 岁</p>

变量可以是任意Python对象。字典、列表、甚至自定义对象都行。访问属性用点号 .,访问字典键用中括号 [] 或者点号都行。

表达式也支持

  • {{ 1 + 2 }} → 输出 3
  • {{ "Hello " ~ name }} → 字符串拼接
  • {{ name|upper }} → 过滤器,把name转大写
  • {{ list|length }} → 获取列表长度

过滤器是Jinja2的一大亮点。我项目中用得最多的过滤器是 safe。为什么?因为Jinja2默认会转义HTML,防止XSS攻击。但有时候你确实想输出HTML标签,这时候就要用 |safe

小技巧:如果你确定内容安全,可以用 {{ content|safe }}。但千万别对用户输入的内容用safe,那是给自己挖坑。

3.3 控制结构:if/for 让模板有逻辑

模板里也能写逻辑?当然可以。用 {% %} 包裹控制语句。

3.3.1 if 判断

<!-- 根据用户状态显示不同内容 -->
{% if user.is_admin %}
    <p>管理员,你好!</p>
{% elif user.is_vip %}
    <p>尊贵的VIP用户,欢迎回来!</p>
{% else %}
    <p>普通用户,登录成功。</p>
{% endif %}

我在项目中遇到过这样一个坑:if 判断里,空列表、空字符串、None、0 都会被当作 False。所以 {% if list %} 这种写法是安全的,不用显式判断长度。

3.3.2 for 循环

遍历列表是最常见的场景:

<ul>
{% for item in items %}
    <li>{{ item.name }} - {{ item.price }} 元</li>
{% endfor %}
</ul>

Jinja2的for循环还内置了一些特殊变量,我经常用:

变量 说明
loop.index 当前循环次数,从1开始
loop.index0 当前循环次数,从0开始
loop.first 是否是第一次循环
loop.last 是否是最后一次循环
loop.length 循环的总长度

举个例子,给表格的奇偶行加不同背景色:

<table>
{% for row in rows %}
    <tr class="{{ 'even' if loop.index is even else 'odd' }}">
        <td>{{ loop.index }}. {{ row.title }}</td>
    </tr>
{% endfor %}
</table>

注意:Jinja2的for循环不支持 breakcontinue。如果你需要提前终止循环,应该在Python视图函数里处理好数据再传给模板。我曾经试图在模板里用 break,结果直接报错——嗯,这个坑我替你们踩过了。

3.4 模板继承:告别重复代码

这是Jinja2最强大的功能,没有之一。你想想看,一个网站通常有统一的头部、导航、底部。如果每个页面都写一遍,那维护起来简直是噩梦。

模板继承的思路很简单:

  1. 写一个「基模板」(base.html),定义整体框架
  2. 在需要填充的地方留出「块」(block)
  3. 子模板继承基模板,只填充自己特有的内容

先看基模板:

<!-- base.html -->
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}默认标题{% endblock %}</title>
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    <header>
        <h1>我的网站</h1>
        <nav>...</nav>
    </header>

    <main>
        {% block content %}
        <!-- 子模板会填充这里 -->
        {% endblock %}
    </main>

    <footer>
        <p>版权所有 &copy; 2024</p>
    </footer>
</body>
</html>

然后子模板继承它:

<!-- about.html -->
{% extends "base.html" %}

{% block title %}关于我们{% endblock %}

{% block content %}
    <h2>关于本站</h2>
    <p>这是一个用Flask构建的示例网站。</p>
{% endblock %}

渲染 about.html 时,Jinja2会自动把 block 里的内容填充到基模板的对应位置。头部、导航、底部都不用重复写。

继承的几点经验

  • 子模板里只能定义 block,不能有 block 之外的HTML内容(会被忽略)
  • 可以用 {{ super() }} 在子模板里引用父模板block的内容
  • 支持多层继承:A继承B,B继承C,但别超过三层,否则维护起来很痛苦

我曾经接手过一个项目,模板继承链长达五层。每次改个样式都要翻半天文件,最后我重构成了两层——基模板和页面模板。说白了,继承是为了简化,不是为了炫技。

3.5 实战小技巧

最后分享几个我常用的技巧:

  • 宏(macro):类似函数,可以复用HTML片段。比如渲染表单字段,定义一个宏,到处调用。
  • include:直接引入另一个模板文件。适合复用独立的组件,比如 {% include 'header.html' %}
  • set 赋值:在模板里定义变量,{% set title = "首页" %},适合临时存储数据。

嗯,Jinja2的内容就这些。说白了,它就是让HTML模板变得像Python一样灵活。你掌握了变量、控制结构、继承这三个核心,基本就能应付90%的场景了。剩下的,遇到问题查文档就行。