Python Jinja2 模板引擎完全指南

2024-08-03 编程开发

一、什么是 Jinja2?

Jinja2 是 Python 生态中最流行的模板引擎之一,由 Flask 框架的作者 Armin Ronacher 开发。它允许你在 HTML、XML、Markdown 等文本文件中嵌入动态内容,实现数据与展示分离。

核心特性

  • 沙箱执行 — 模板中不能随意执行任意 Python 代码,安全可控
  • 模板继承 — 通过 extends 和 block 实现布局复用
  • 强大的过滤器系统 — 对变量进行格式化、截断、转义等处理
  • 自动 HTML 转义 — 默认开启,防止 XSS 攻击
  • 高性能 — 编译成 Python 字节码缓存执行
  • 可扩展 — 自定义过滤器、测试器、全局函数

你正在阅读的这个网站就是基于 Jinja2 搭建的——本站的导航栏、页脚、文章列表、工具页全部通过 Jinja2 模板渲染生成。

二、安装

Jinja2 是独立库,不依赖 Web 框架:

pip install Jinja2

如果你用 Flask,Jinja2 已经是内置依赖,无需单独安装:

pip install flask  # 自带 Jinja2

验证安装:

import jinja2
print(jinja2.__version__)  # 3.1.x

三、快速上手

3.1 最简单的示例

from jinja2 import Environment, BaseLoader

# 创建环境
env = Environment()

# 渲染模板
template = env.from_string("Hello, {{ name }}!")
result = template.render(name="World")
print(result)  # Hello, World!

3.2 从文件加载模板

from jinja2 import Environment, FileSystemLoader

# 指定模板目录
env = Environment(
    loader=FileSystemLoader("templates/"),
    autoescape=True  # 自动 HTML 转义
)

# 渲染
template = env.get_template("index.html")
html = template.render(title="首页", users=["Alice", "Bob"])

目录结构:

project/
├── templates/
│   ├── base.html       # 基础布局
│   └── index.html      # 首页
└── app.py              # Python 脚本

四、模板语法

Jinja2 使用三种定界符:

定界符 用途 示例
{{ ... }} 输出变量/表达式 {{ user.name }}
{% ... %} 控制结构(判断、循环等) {% if user %}
{# ... #} 注释(不出现在输出中) {# 这是注释 #}

4.1 变量输出

<p>{{ username }}</p>
<p>{{ article.title }}</p>
<p>{{ articles[0].title }}</p>
<p>{{ dict_value["key"] }}</p>

4.2 过滤器

过滤器用管道符 | 连接,可以对变量进行各种处理:

<!-- 字符串大写 -->
{{ "hello"|upper }}           <!-- HELLO -->

<!-- 首字母大写 -->
{{ "hello world"|title }}     <!-- Hello World -->

<!-- 默认值(变量为 None 或未定义时使用) -->
{{ undefined_var|default("默认值") }}

<!-- 截断 -->
{{ long_text|truncate(50) }}  <!-- 截取前50字符 -->

<!-- JSON 序列化 -->
{{ data|tojson }}

<!-- 日期格式化 -->
{{ date|datetimeformat }}     <!-- 需要自定义过滤器 -->

链式调用:

{{ name|striptags|title|truncate(30) }}

常用内置过滤器

过滤 说明
safe 标记为安全 HTML,不转义
escape / e HTML 转义
capitalize 首字母大写
lower / upper 大小写转换
trim 去除首尾空白
striptags 去除 HTML 标签
length 返回长度
join(", ") 连接列表
first / last 取首/尾元素
sort / reverse 排序
batch(n) 分批(每 n 个一组)
slice(n) 切片
format 格式化字符串
replace(a, b) 替换
wordcount 单词计数

4.3 条件判断

{% if score >= 90 %}
    <p>优秀</p>
{% elif score >= 60 %}
    <p>及格</p>
{% else %}
    <p>不及格</p>
{% endif %}

支持的运算符:and、or、not、==、!=、<、>、in、is

{% if user.is_admin and user.is_active %}
    <a href="/admin">管理后台</a>
{% endif %}

{% if "admin" in user.roles %}
    <p>你有管理员权限</p>
{% endif %}

4.4 循环

<ul>
{% for item in items %}
    <li>{{ loop.index }}. {{ item.name }}</li>
{% else %}
    <li>列表为空</li>
{% endfor %}
</ul>

loop 变量在循环内部自动可用:

变量 说明
loop.index 当前迭代次数(从 1 开始)
loop.index0 当前迭代次数(从 0 开始)
loop.revindex 到结尾剩余次数(从 1 开始)
loop.first 是否为第一项
loop.last 是否为最后一项
loop.length 序列总长度
loop.cycle("a", "b") 循环取值(奇偶行变色常用)

奇偶行变色:

{% for item in items %}
    <tr class="{{ loop.cycle('even', 'odd') }}">
        <td>{{ item.name }}</td>
    </tr>
{% endfor %}

字典循环:

{% for key, value in my_dict.items() %}
    <dt>{{ key }}</dt>
    <dd>{{ value }}</dd>
{% endfor %}

4.5 变量赋值

{% set title = "首页" %}
{% set nums = [1, 2, 3] %}
{% set fullname = user.first_name ~ " " ~ user.last_name %}

~ 是字符串连接运算符。

4.6 空白控制

默认 Jinja2 会保留标签外的空白。可以用 - 去除:

{% for item in items -%}
    {{ item }}
{%- endfor %}
  • {%- 去掉标签前的空白
  • -%} 去掉标签后的空白

五、模板继承

模板继承是 Jinja2 最强大的特性,通过 extends 和 block 实现布局复用。

5.1 基础布局 base.html

<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}默认标题{% endblock %}</title>
    <link rel="stylesheet" href="/style.css">
    {% block extra_head %}{% endblock %}
</head>
<body>
    <header>
        {% block header %}站点标题{% endblock %}
    </header>

    <main>
        {% block content %}{% endblock %}
    </main>

    <footer>
        {% block footer %}
            &copy; 2026 版权所有
        {% endblock %}
    </footer>

    {% block scripts %}{% endblock %}
</body>
</html>

5.2 子模板

{% extends "base.html" %}

{% block title %}首页 - 我的站点{% endblock %}

{% block content %}
    <h1>欢迎</h1>
    <p>这是首页内容</p>
{% endblock %}

{% block scripts %}
    <script src="/home.js"></script>
{% endblock %}

5.3 super() 保留父块内容

{% block footer %}
    {{ super() }}  <!-- 保留父级的 © 版权 -->
    <p><a href="/about">关于我们</a></p>
{% endblock %}

5.4 block 嵌套与作用域

{% block outer %}
    外层内容
    {% block inner %}
        内层内容
    {% endblock %}
{% endblock %}

子模板可以覆盖任意层级的 block,也可以只覆盖内层:

{% extends "base.html" %}
{% block inner %}仅覆盖内层{% endblock %}

六、宏(Macro)

宏类似于函数,用于复用 HTML 片段:

6.1 定义宏

{# macros.html #}
{% macro input(name, value="", type="text", size=20) %}
    <input type="{{ type }}" name="{{ name }}"
           value="{{ value|e }}" size="{{ size }}">
{% endmacro %}

{% macro render_card(title, body, class="") %}
    <div class="card {{ class }}">
        <div class="card-header">{{ title }}</div>
        <div class="card-body">{{ body }}</div>
    </div>
{% endmacro %}

6.2 使用宏

{% import "macros.html" as macros %}

{{ macros.input("username", type="text") }}
{{ macros.input("password", type="password", size=40) }}
{{ macros.render_card("欢迎", "这是卡片内容", class="highlight") }}

也可以从模板中直接导入特定宏:

{% from "macros.html" import input, render_card %}
{{ input("email") }}

6.3 宏的 caller 回调

{% macro card(title) %}
    <div class="card">
        <h3>{{ title }}</h3>
        <div class="card-body">{{ caller() }}</div>
    </div>
{% endmacro %}
{% call card("通知") %}
    <p>这是卡片内部的内容</p>
{% endcall %}

七、包含(Include)

{% include "header.html" %}
{% include "sidebar.html" ignore missing %}
{% include "ad.html" ignore missing with context %}
  • ignore missing — 文件不存在时静默忽略
  • with context — 传入当前上下文(默认行为)
  • without context — 不传入当前上下文

八、自定义扩展

8.1 自定义过滤器

from jinja2 import Environment

def datetimeformat(value, format="%Y-%m-%d"):
    return value.strftime(format)

def pluralize(count, singular="", plural="s"):
    return singular if count == 1 else plural

# 注册
env = Environment()
env.filters["datetimeformat"] = datetimeformat
env.filters["pluralize"] = pluralize

模板中使用:

{{ article.created_at|datetimeformat("%Y年%m月%d日") }}
{{ items|length }} item{{ items|length|pluralize("", "s") }}

8.2 自定义测试器

def is_admin(user):
    return user.role == "admin"

env.tests["admin"] = is_admin
{% if user is admin %}
    <a href="/admin">管理后台</a>
{% endif %}

8.3 自定义全局函数

def static(path):
    return f"/static/{path}"

env.globals["static"] = static
<img src="{{ static('logo.png') }}" alt="Logo">

九、本站实战案例

本站(markdown-cms)大量使用了 Jinja2 的特性:

9.1 基础布局

# templates/base.html
# 包含了导航栏、搜索面板、主题切换、页脚
# 用 {% block content %} 让子页面填充内容

9.2 文章列表渲染

# article_service.py
env = Environment(loader=FileSystemLoader("templates/"))
content = f'''
<div class="page-header">
    <h2>最新文章</h2>
    <span class="page-count">共 {total} 篇</span>
</div>
<div class="article-list">
    {cards_html}
</div>
'''
return env.get_template("base.html").render(title="首页", content=content)

9.3 工具页模板继承

所有工具页(密码生成器、打包工具、Token 计算器等)都通过 env.get_template('base.html').render(...) 渲染,自动继承导航栏和页脚。

十、性能优化

10.1 模板缓存

env = Environment(
    loader=FileSystemLoader("templates/"),
    auto_reload=False,       # 生产环境关闭自动重载
    cache_size=50,           # 最多缓存 50 个模板
)

10.2 字节码缓存

from jinja2 import Environment, FileSystemLoader
from jinja2.bccache import FileSystemBytecodeCache

bccache = FileSystemBytecodeCache("cache/", "%s.cache")
env = Environment(
    loader=FileSystemLoader("templates/"),
    bytecode_cache=bccache,  # 缓存编译后的字节码
)

10.3 预编译模板

# 批量编译所有模板,减少首次渲染延迟
templates = env.list_templates()
for name in templates:
    env.get_template(name)

十一、常见问题与陷阱

11.1 UndefinedError 未定义变量

<!-- 访问不存在的变量不会报错,返回空字符串 -->
{{ not_exist }}  <!-- 输出空 -->

<!-- 但如果访问不存在的属性/方法会报错 -->
{{ user.not_exist_attr }}  <!-- UndefinedError -->

解决方案:使用 default 过滤器或自定义 undefined 类:

env = Environment(undefined=DebugUndefined)   # 开发模式
env = Environment(undefined=StrictUndefined)  # 严格模式
env = Environment(undefined=ChainableUndefined)  # 链式安全

11.2 HTML 转义

# 默认 autoescape=True 时,{{ }} 自动转义
# 如果内容是安全 HTML,用 safe 过滤器
{{ "<b>粗体</b>"|safe }}

11.3 XSS 防护

永远不要对用户输入使用 |safe,除非你 100% 确定内容是安全的。使用 bleach 库清理用户输入的 HTML:

import bleach
cleaned = bleach.clean(user_input, tags=["b", "i", "a"], attributes={"a": ["href"]})

11.4 模板中调用 Python 方法

<!-- 支持调用无参数方法 -->
{{ user.get_full_name() }}

<!-- 不支持带参数方法(除非自定义全局函数) -->

11.5 循环中修改变量无效

{% set total = 0 %}
{% for item in items %}
    {% set total = total + item.price %}  <!-- 不会生效! -->
{% endfor %}
{{ total }}  <!-- 输出 0 -->

解决方案:在 Python 端处理好数据再传给模板。

十二、与 Web 框架集成

Flask

Flask 内置 Jinja2,自动配置:

from flask import Flask, render_template

app = Flask(__name__)

@app.route("/")
def index():
    return render_template("index.html", title="首页", items=items)

@app.template_filter("datetimeformat")
def datetimeformat(value, format="%Y-%m-%d"):
    return value.strftime(format)

Django(需安装第三方库)

pip install django-jinja
# settings.py
TEMPLATES = [
    {
        "BACKEND": "django_jinja.backend.Jinja2Backend",
        "DIRS": [BASE_DIR / "jinja2"],
        "APP_DIRS": True,
    },
]

FastAPI / Starlette

from fastapi import FastAPI
from fastapi.templating import Jinja2Templates

app = FastAPI()
templates = Jinja2Templates(directory="templates/")

@app.get("/")
async def index(request):
    return templates.TemplateResponse("index.html", {
        "request": request,
        "title": "首页"
    })

十三、总结

Jinja2 是一个功能完善、性能优秀的模板引擎,核心优势在于:

  1. 模板继承 — 避免重复编写公共布局
  2. 过滤器系统 — 灵活的数据格式化
  3. 沙箱安全 — 默认 HTML 转义防止 XSS
  4. 高性能 — 字节码缓存 + 编译执行
  5. 生态完善 — Flask/FastAPI 等框架原生支持

在实际项目中,建议:

  • 始终开启 autoescape
  • 善用模板继承减少重复代码
  • 复杂逻辑放在 Python 端,模板只做展示
  • 自定义过滤器/全局函数可以让模板更简洁

掌握 Jinja2,是 Python Web 开发的必修课 🎯