Skip to content

Jinja2 学习指南

Python 最强大的模板引擎,Web 开发必备技能


📖 目录

  1. Jinja2 简介
  2. 安装与配置
  3. 基础语法
  4. 控制结构
  5. 模板继承
  6. 过滤器
  7. 测试器
  8. 宏与调用
  9. Python 集成
  10. 实战示例
  11. 最佳实践

1. Jinja2 简介

1.1 什么是 Jinja2?

Jinja2 是一个现代的、设计优雅的 Python 模板引擎。它由 Pocoo 团队开发(也是 Flask 框架的创造者),广泛应用于 Web 开发、配置文件生成、代码模板等场景。

核心特点:

  • 🚀 快速高效 - 编译为 Python 字节码,执行速度快
  • 🔒 安全可靠 - 沙箱执行环境,防止恶意模板代码
  • 🎨 语法简洁 - 类似 Django 模板,易于学习
  • 🔧 功能强大 - 支持继承、宏、过滤器、自定义函数
  • 📦 零依赖 - 仅需 Python 标准库

1.2 应用场景

场景说明
Web 开发Flask、Django 等框架的 HTML 模板渲染
代码生成批量生成配置文件、源代码文件
报告生成HTML/PDF 报告、邮件模板
文档站点VitePress、MkDocs 等静态站点生成器
自动化运维批量生成服务器配置文件

1.3 模板语法示例

jinja2
{# 这是一个注释 #}
<!DOCTYPE html>
<html>
<head>
    <title>{{ page_title }}</title>
</head>
<body>
    <h1>欢迎,{{ user.name }}!</h1>
    
    {% if user.is_vip %}
        <p>尊贵的 VIP 用户,您好!</p>
    {% endif %}
    
    <ul>
    {% for item in items %}
        <li>{{ loop.index }}. {{ item.name }}</li>
    {% endfor %}
    </ul>
</body>
</html>

2. 安装与配置

2.1 安装 Jinja2

bash
# 使用 pip 安装
pip install jinja2

# 验证安装
python -c "import jinja2; print(jinja2.__version__)"

2.2 基础使用

python
from jinja2 import Template

# 创建模板对象
template = Template("Hello, {{ name }}!")

# 渲染模板
result = template.render(name="World")
print(result)  # 输出:Hello, World!

2.3 使用 FileSystemLoader

python
from jinja2 import Environment, FileSystemLoader

# 创建环境,指定模板目录
env = Environment(
    loader=FileSystemLoader('templates'),
    autoescape=True  # 启用自动转义,防止 XSS
)

# 加载模板
template = env.get_template('index.html')

# 渲染
html = template.render(title="首页", user="老李")

2.4 常用配置选项

python
from jinja2 import Environment, FileSystemLoader

env = Environment(
    loader=FileSystemLoader('templates'),
    
    # 模板语法定界符(可自定义)
    block_start_string='{%',
    block_end_string='%}',
    variable_start_string='{{',
    variable_end_string='}}',
    comment_start_string='{#',
    comment_end_string='#}',
    
    # 安全选项
    autoescape=True,        # 自动转义 HTML 特殊字符
    undefined=Undefined,    # 未定义变量的处理方式
    
    # 性能选项
    cache_size=400,         # 模板缓存数量
    auto_reload=True,       # 模板文件变更时自动重载
    
    # 空白控制
    trim_blocks=True,       # 删除块后的第一个换行
    lstrip_blocks=True,     # 删除块前的空白
)

3. 基础语法

3.1 变量输出

jinja2
{# 基本变量输出 #}
{{ username }}
{{ user.email }}
{{ items[0] }}
{{ config['site_name'] }}

{# 使用点号访问属性和字典 #}
{{ user.name }}      {# 对象属性 #}
{{ user['name'] }}   {# 字典键值 #}
{{ user.get_name() }} {# 方法调用 #}

3.2 数学运算

jinja2
{{ 1 + 2 }}        {# 3 #}
{{ 10 - 3 }}       {# 7 #}
{{ 2 * 5 }}        {# 10 #}
{{ 15 / 3 }}       {# 5.0 #}
{{ 15 // 3 }}      {# 5 - 整除 #}
{{ 17 % 5 }}       {# 2 - 取余 #}
{{ 2 ** 3 }}       {# 8 - 幂运算 #}

3.3 字符串操作

jinja2
{{ 'Hello' ~ ' ' ~ 'World' }}  {# 字符串连接:Hello World #}
{{ 'Python'[0:2] }}            {# 字符串切片:Py #}
{{ 'test' in 'this is a test' }} {# 成员测试:true #}

3.4 比较运算

jinja2
{{ 5 == 5 }}       {# true #}
{{ 5 != 3 }}       {# true #}
{{ 5 > 3 }}        {# true #}
{{ 5 < 10 }}       {# true #}
{{ 5 >= 5 }}       {# true #}
{{ 5 <= 10 }}      {# true #}

3.5 逻辑运算

jinja2
{{ true and true }}   {# true #}
{{ true or false }}   {# true #}
{{ not false }}       {# true #}

{# 条件表达式(三元运算符)#}
{{ '成年' if age >= 18 else '未成年' }}

3.6 注释

jinja2
{# 这是单行注释 #}

{# 
   这是多行注释
   可以跨越多行
#}

{# 注释中可以包含 {{ 变量 }} 但不会被渲染 #}

4. 控制结构

4.1 If 条件语句

jinja2
{# 基本 if #}
{% if user.is_logged_in %}
    <p>欢迎回来,{{ user.name }}!</p>
{% endif %}

{# if-else #}
{% if items %}
    <p>共有 {{ items|length }} 个项目</p>
{% else %}
    <p>暂无项目</p>
{% endif %}

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

{# 复合条件 #}
{% if user.is_vip and user.balance > 100 %}
    <p>尊贵 VIP 用户,余额充足</p>
{% endif %}

4.2 For 循环

jinja2
{# 基本循环 #}
<ul>
{% for item in items %}
    <li>{{ item }}</li>
{% endfor %}
</ul>

{# 带索引的循环 #}
{% for user in users %}
    {{ loop.index }}. {{ user.name }}  {# 从 1 开始的索引 #}
    {{ loop.index0 }}. {{ user.email }} {# 从 0 开始的索引 #}
{% endfor %}

{# 循环信息变量 #}
{% for item in items %}
    {% if loop.first %}
        <li class="first">首个项目:{{ item }}</li>
    {% elif loop.last %}
        <li class="last">末尾项目:{{ item }}</li>
    {% else %}
        <li>{{ item }}</li>
    {% endif %}
{% endfor %}

{# 嵌套循环 #}
{% for category in categories %}
    <h2>{{ category.name }}</h2>
    <ul>
    {% for product in category.products %}
        <li>{{ product.name }}</li>
    {% else %}
        <li>该分类下无产品</li>
    {% endfor %}
    </ul>
{% endfor %}

{# 循环控制 #}
{% for i in range(10) %}
    {% if i == 5 %}
        {% break %}  {# 跳出循环 #}
    {% endif %}
    {{ i }}
{% endfor %}

{% for i in range(5) %}
    {% if i == 2 %}
        {% continue %}  {# 跳过本次迭代 #}
    {% endif %}
    {{ i }}
{% endfor %}

4.3 范围循环

jinja2
{# 使用 range 函数 #}
{% for i in range(5) %}      {# 0, 1, 2, 3, 4 #}
    {{ i }}
{% endfor %}

{% for i in range(1, 6) %}   {# 1, 2, 3, 4, 5 #}
    {{ i }}
{% endfor %}

{% for i in range(0, 10, 2) %} {# 0, 2, 4, 6, 8 #}
    {{ i }}
{% endfor %}

{% for i in range(10, 0, -1) %} {# 10, 9, 8, ..., 1 #}
    {{ i }}
{% endfor %}

4.4 字典遍历

jinja2
{% for key, value in config.items() %}
    <p>{{ key }}: {{ value }}</p>
{% endfor %}

5. 模板继承

5.1 基础模板(base.html)

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}默认标题{% endblock %}</title>
    
    {% block head %}
    <link rel="stylesheet" href="/static/css/style.css">
    {% endblock %}
    
    {% block extra_head %}{% endblock %}
</head>
<body>
    <header>
        <nav>
            {% block navigation %}
            <a href="/">首页</a>
            <a href="/about">关于</a>
            <a href="/contact">联系</a>
            {% endblock %}
        </nav>
    </header>
    
    <main>
        {% block content %}{% endblock %}
    </main>
    
    <footer>
        {% block footer %}
        <p>&copy; 2026 我的网站</p>
        {% endblock %}
    </footer>
    
    {% block scripts %}
    <script src="/static/js/main.js"></script>
    {% endblock %}
</body>
</html>

5.2 子模板继承(index.html)

html
{% extends "base.html" %}

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

{% block extra_head %}
<link rel="stylesheet" href="/static/css/home.css">
{% endblock %}

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

{% for article in articles %}
    <article>
        <h2>{{ article.title }}</h2>
        <p>{{ article.summary }}</p>
    </article>
{% endfor %}
{% endblock %}

{% block scripts %}
{{ super() }}  {# 保留父模板的 scripts #}
<script src="/static/js/home.js"></script>
{% endblock %}

5.3 Include 包含

jinja2
{# 包含另一个模板 #}
{% include 'sidebar.html' %}

{# 条件包含(如果文件不存在则忽略)#}
{% include 'sidebar.html' ignore missing %}

{# 从多个模板中选择第一个存在的 #}
{% include ['sidebar_user.html', 'sidebar_default.html'] %}

5.4 Import 导入宏

jinja2
{# 导入整个模板作为模块 #}
{% import 'forms.html' as forms %}

{{ forms.input_field('username', '用户名') }}
{{ forms.textarea('content', '内容') }}

{# 只导入特定的宏 #}
{% from 'forms.html' import input_field, textarea %}

{{ input_field('email', '邮箱') }}
{{ textarea('message', '留言') }}

{# 导入并重命名 #}
{% from 'forms.html' import input_field as field %}

{{ field('phone', '电话') }}

6. 过滤器

6.1 常用内置过滤器

jinja2
{# 字符串处理 #}
{{ name|lower }}              {# 转小写 #}
{{ name|upper }}              {# 转大写 #}
{{ name|title }}              {# 首字母大写 #}
{{ name|capitalize }}         {# 首字符大写 #}
{{ text|truncate(50) }}       {# 截断文本 #}
{{ text|striptags }}          {# 移除 HTML 标签 #}
{{ text|escape }}             {# HTML 转义 #}
{{ text|replace('old', 'new') }} {# 替换字符串 #}

{# 列表处理 #}
{{ items|length }}            {# 列表长度 #}
{{ items|first }}             {# 第一个元素 #}
{{ items|last }}              {# 最后一个元素 #}
{{ items|reverse }}           {# 反转列表 #}
{{ items|sort }}              {# 排序 #}
{{ items|unique }}            {# 去重 #}
{{ items|join(', ') }}        {# 连接为字符串 #}

{# 数值处理 #}
{{ price|round(2) }}          {# 四舍五入 #}
{{ price|abs }}               {# 绝对值 #}
{{ count|default(0) }}        {# 默认值 #}

{# 字典处理 #}
{{ config|dictsort }}         {# 按键排序 #}
{{ config|items }}            {# 获取键值对 #}

{# HTML 相关 #}
{{ content|safe }}            {# 标记为安全 HTML #}
{{ html|escape }}             {# HTML 转义 #}

6.2 过滤器链式调用

jinja2
{{ name|lower|title }}
{{ items|reverse|first }}
{{ text|striptags|truncate(100)|escape }}

6.3 带参数的过滤器

jinja2
{{ text|truncate(50, true, '...') }}  {# 截断,保持完整单词,自定义后缀 #}
{{ items|join(', ', '和') }}          {# 连接,指定分隔符 #}
{{ price|round(2, 'floor') }}         {# 向下取整 #}
{{ date|date('Y-m-d') }}              {# 日期格式化 #}

6.4 自定义过滤器

python
# Python 代码中定义过滤器
def format_currency(value):
    """将数值格式化为货币形式"""
    return f{value:,.2f}"

def format_phone(value):
    """将手机号格式化为 138-0000-0000"""
    if len(value) == 11:
        return f"{value[:3]}-{value[3:7]}-{value[7:]}"
    return value

# 注册到 Jinja2 环境
env.filters['currency'] = format_currency
env.filters['phone'] = format_phone
jinja2
{# 模板中使用自定义过滤器 #}
{{ price|currency }}      {# ¥1,234.56 #}
{{ phone|phone }}         {# 138-0000-0000 #}

6.5 在模板中定义过滤器

jinja2
{% filter upper %}
    这段文字会变成大写
{% endfilter %}

{% filter truncate(50) %}
    这是一段很长的文字,会被截断处理...
{% endfilter %}

7. 测试器

7.1 常用内置测试器

jinja2
{# 类型测试 #}
{% if number is defined %}...{% endif %}
{% if value is none %}...{% endif %}
{% if flag is sameas true %}...{% endif %}

{% if age is integer %}...{% endif %}
{% if price is float %}...{% endif %}
{% if text is string %}...{% endif %}
{% if flag is boolean %}...{% endif %}
{% if items is iterable %}...{% endif %}
{% if data is mapping %}...{% endif %}
{% if func is callable %}...{% endif %}

{# 数值测试 #}
{% if num is even %}...{% endif %}
{% if num is odd %}...{% endif %}
{% if num is divisibleby 3 %}...{% endif %}

{# 字符串测试 #}
{% if name is lower %}...{% endif %}
{% if title is upper %}...{% endif %}
{% if text is startingwith 'Hello' %}...{% endif %}
{% if url is endingwith '.html' %}...{% endif %}

{# 集合测试 #}
{% if item in items %}...{% endif %}
{% if 'admin' in user.roles %}...{% endif %}

7.2 自定义测试器

python
# Python 代码中定义测试器
def is_valid_email(value):
    """测试是否为有效的邮箱格式"""
    import re
    pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
    return bool(re.match(pattern, value))

def is_adult(value):
    """测试是否成年"""
    return value >= 18

# 注册到 Jinja2 环境
env.tests['valid_email'] = is_valid_email
env.tests['adult'] = is_adult
jinja2
{# 模板中使用自定义测试器 #}
{% if email is valid_email %}
    <p>邮箱格式正确</p>
{% endif %}

{% if age is adult %}
    <p>已成年</p>
{% else %}
    <p>未成年</p>
{% endif %}

8. 宏与调用

8.1 定义宏

jinja2
{# 基本宏定义 #}
{% macro input_field(name, type='text', value='', placeholder='') %}
<input type="{{ type }}" 
       name="{{ name }}" 
       value="{{ value }}"
       placeholder="{{ placeholder }}">
{% endmacro %}

{# 带默认值的宏 #}
{% macro button(text='提交', type='submit', class='btn') %}
<button type="{{ type }}" class="{{ class }}">
    {{ text }}
</button>
{% endmacro %}

{# 复杂宏 - 表单字段 #}
{% macro form_field(label, name, type='text', required=False, error=None) %}
<div class="form-group {% if error %}has-error{% endif %}">
    <label for="{{ name }}">
        {{ label }}
        {% if required %}<span class="required">*</span>{% endif %}
    </label>
    <input type="{{ type }}" 
           id="{{ name }}" 
           name="{{ name }}"
           {% if required %}required{% endif %}>
    {% if error %}
        <span class="error">{{ error }}</span>
    {% endif %}
</div>
{% endmacro %}

8.2 调用宏

jinja2
{# 同一模板内调用 #}
{{ input_field('username', placeholder='请输入用户名') }}
{{ button('立即注册', class='btn-primary') }}

{# 调用带 required 的字段 #}
{{ form_field('邮箱', 'email', type='email', required=True) }}
{{ form_field('密码', 'password', type='password', required=True, error='密码不能为空') }}

8.3 宏的导入与导出

forms.html

jinja2
{% macro input(name, type='text') %}
<input type="{{ type }}" name="{{ name }}">
{% endmacro %}

{% macro textarea(name, rows=5) %}
<textarea name="{{ name }}" rows="{{ rows }}"></textarea>
{% endmacro %}

{% macro select(name, options) %}
<select name="{{ name }}">
    {% for value, label in options %}
    <option value="{{ value }}">{{ label }}</option>
    {% endfor %}
</select>
{% endmacro %}

使用模板

jinja2
{# 导入整个模块 #}
{% import 'forms.html' as forms %}

{{ forms.input('username') }}
{{ forms.textarea('content', rows=10) }}
{{ forms.select('category', [('1', '分类 1'), ('2', '分类 2')]) }}

{# 只导入特定宏 #}
{% from 'forms.html' import input, textarea %}

{{ input('email', type='email') }}
{{ textarea('message') }}

8.4 可变参数宏

jinja2
{% macro list_items(*items) %}
<ul>
{% for item in items %}
    <li>{{ item }}</li>
{% endfor %}
</ul>
{% endmacro %}

{# 调用 #}
{{ list_items('苹果', '香蕉', '橙子') }}

{% macro tag_cloud(**tags) %}
<div class="tag-cloud">
{% for name, count in tags.items() %}
    <span class="tag" data-count="{{ count }}">{{ name }}</span>
{% endfor %}
</div>
{% endmacro %}

9. Python 集成

9.1 基础集成

python
from jinja2 import Environment, FileSystemLoader, select_autoescape

# 创建环境
env = Environment(
    loader=FileSystemLoader('templates'),
    autoescape=select_autoescape(['html', 'xml']),
    trim_blocks=True,
    lstrip_blocks=True
)

# 加载模板
template = env.get_template('index.html')

# 渲染
html = template.render(
    title="首页",
    user={"name": "老李", "email": "li@example.com"},
    items=["项目 1", "项目 2", "项目 3"]
)

# 输出到文件
with open('output.html', 'w', encoding='utf-8') as f:
    f.write(html)

9.2 Flask 集成

python
from flask import Flask, render_template

app = Flask(__name__)

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

@app.route('/user/<username>')
def user_profile(username):
    user = get_user(username)
    return render_template('profile.html', user=user)

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

9.3 自定义全局函数

python
from jinja2 import Environment, FileSystemLoader

def format_date(date, format='%Y-%m-%d'):
    """格式化日期"""
    return date.strftime(format)

def gravatar_url(email, size=80):
    """生成 Gravatar 头像 URL"""
    import hashlib
    hash_email = hashlib.md5(email.lower().encode()).hexdigest()
    return f"https://www.gravatar.com/avatar/{hash_email}?s={size}"

# 创建环境并注册全局函数
env = Environment(loader=FileSystemLoader('templates'))
env.globals['format_date'] = format_date
env.globals['gravatar_url'] = gravatar_url

# 或者使用 dict 一次性注册
env.globals.update({
    'format_date': format_date,
    'gravatar_url': gravatar_url,
    'site_name': '我的网站',
    'version': '1.0.0'
})
jinja2
{# 模板中使用全局函数 #}
<img src="{{ gravatar_url(user.email, 100) }}" alt="头像">
<p>发布日期:{{ format_date(article.created_at) }}</p>
<footer>{{ site_name }} v{{ version }}</footer>

9.4 模板继承与上下文

python
# 基础上下文
base_context = {
    'site_name': '我的网站',
    'current_year': 2026,
    'navigation': [
        {'name': '首页', 'url': '/'},
        {'name': '关于', 'url': '/about'},
        {'name': '联系', 'url': '/contact'}
    ]
}

# 渲染时合并上下文
template = env.get_template('page.html')
html = template.render(**base_context, title="具体页面", content="页面内容")

9.5 模板字符串渲染

python
from jinja2 import Template

# 直接从字符串创建模板
template = Template("""
Hello, {{ name }}!

{% for item in items %}
- {{ item }}
{% endfor %}
""")

result = template.render(
    name="老李",
    items=["苹果", "香蕉", "橙子"]
)
print(result)

9.6 批量生成文件

python
from jinja2 import Environment, FileSystemLoader
import os

env = Environment(loader=FileSystemLoader('templates'))

# 配置文件模板
config_template = env.get_template('nginx.conf.j2')

# 批量生成服务器配置
servers = [
    {'name': 'web1', 'ip': '192.168.1.10', 'port': 80},
    {'name': 'web2', 'ip': '192.168.1.11', 'port': 80},
    {'name': 'api1', 'ip': '192.168.1.20', 'port': 8080},
]

for server in servers:
    config = config_template.render(server=server)
    
    # 写入配置文件
    filename = f"configs/{server['name']}.conf"
    os.makedirs('configs', exist_ok=True)
    with open(filename, 'w') as f:
        f.write(config)
    
    print(f"生成配置:{filename}")

10. 实战示例

10.1 HTML 邮件模板

templates/email/welcome.html

html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <style>
        body { font-family: Arial, sans-serif; line-height: 1.6; }
        .container { max-width: 600px; margin: 0 auto; padding: 20px; }
        .header { background: #4CAF50; color: white; padding: 20px; text-align: center; }
        .content { padding: 20px; background: #f9f9f9; }
        .button { 
            display: inline-block; 
            padding: 12px 24px; 
            background: #4CAF50; 
            color: white; 
            text-decoration: none; 
            border-radius: 4px;
        }
        .footer { text-align: center; padding: 20px; color: #666; font-size: 12px; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>欢迎加入 {{ site_name }}!</h1>
        </div>
        
        <div class="content">
            <p>亲爱的 {{ user.name }},</p>
            
            <p>感谢您注册 {{ site_name }}!我们很高兴您能成为我们社区的一员。</p>
            
            <p>您的账号信息:</p>
            <ul>
                <li>用户名:{{ user.username }}</li>
                <li>邮箱:{{ user.email }}</li>
                <li>注册时间:{{ register_date }}</li>
            </ul>
            
            <p style="text-align: center; margin: 30px 0;">
                <a href="{{ activate_url }}" class="button">激活账号</a>
            </p>
            
            <p>如果按钮无法点击,请复制以下链接到浏览器:</p>
            <p><small>{{ activate_url }}</small></p>
        </div>
        
        <div class="footer">
            <p>&copy; {{ current_year }} {{ site_name }}. 保留所有权利.</p>
            <p>如果您没有注册此账号,请忽略此邮件。</p>
        </div>
    </div>
</body>
</html>

Python 渲染代码

python
from datetime import datetime

html = template.render(
    site_name="我的网站",
    user={
        'name': '老李',
        'username': 'liyong',
        'email': 'li@example.com'
    },
    register_date=datetime.now().strftime('%Y-%m-%d %H:%M'),
    activate_url="https://example.com/activate?token=abc123",
    current_year=2026
)

10.2 代码生成器

templates/python/class.py.j2

python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
{{ class_name }} - {{ description }}

自动生成于:{{ generation_date }}
"""

{% for import in imports %}
{{ import }}
{% endfor %}


class {{ class_name }}:
    """{{ class_name }} 类说明"""
    
    def __init__(self{% for param in constructor_params %}, {{ param.name }}{% if param.default %}={{ param.default }}{% endif %}{% endfor %}):
        """
        初始化 {{ class_name }}
        
        {% for param in constructor_params %}
        :param {{ param.name }}: {{ param.description }}
        {% endfor %}
        """
        {% for param in constructor_params %}
        self.{{ param.name }} = {{ param.name }}
        {% endfor %}
    
    {% for method in methods %}
    def {{ method.name }}(self{% for param in method.params %}, {{ param }}{% endfor %}):
        """{{ method.description }}"""
        {{ method.body|indent(8) }}
    
    {% endfor %}

    def __str__(self):
        return f"{{ class_name }}({% for param in constructor_params[:3] %}{{ self.{{ param.name }}}}, {% endfor %}...)"

Python 生成代码

python
from jinja2 import Environment, FileSystemLoader
from datetime import datetime

env = Environment(loader=FileSystemLoader('templates/python'))
template = env.get_template('class.py.j2')

code = template.render(
    class_name="UserService",
    description="用户服务类",
    generation_date=datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
    imports=[
        "from typing import Optional, List",
        "from datetime import datetime"
    ],
    constructor_params=[
        {'name': 'db_connection', 'description': '数据库连接'},
        {'name': 'cache_ttl', 'description': '缓存时间', 'default': '3600'}
    ],
    methods=[
        {
            'name': 'get_user',
            'params': ['user_id: int'],
            'description': '获取用户信息',
            'body': 'pass  # TODO: 实现获取用户逻辑'
        },
        {
            'name': 'create_user',
            'params': ['username: str', 'email: str'],
            'description': '创建新用户',
            'body': 'pass  # TODO: 实现创建用户逻辑'
        }
    ]
)

with open('user_service.py', 'w', encoding='utf-8') as f:
    f.write(code)

10.3 配置文件生成

templates/docker-compose.yml.j2

yaml
version: '3.8'

services:
{% for service in services %}
  {{ service.name }}:
    image: {{ service.image }}
    container_name: {{ service.container_name|default(service.name) }}
    {% if service.ports %}
    ports:
    {% for port in service.ports %}
      - "{{ port }}"
    {% endfor %}
    {% endif %}
    {% if service.volumes %}
    volumes:
    {% for volume in service.volumes %}
      - {{ volume }}
    {% endfor %}
    {% endif %}
    {% if service.environment %}
    environment:
    {% for key, value in service.environment.items() %}
      - {{ key }}={{ value }}
    {% endfor %}
    {% endif %}
    {% if service.networks %}
    networks:
    {% for network in service.networks %}
      - {{ network }}
    {% endfor %}
    {% endif %}
    {% if service.depends_on %}
    depends_on:
    {% for dep in service.depends_on %}
      - {{ dep }}
    {% endfor %}
    {% endif %}

{% endfor %}
networks:
{% for network in networks %}
  {{ network.name }}:
    driver: {{ network.driver|default('bridge') }}
{% endfor %}

Python 生成配置

python
config = template.render(
    services=[
        {
            'name': 'web',
            'image': 'nginx:latest',
            'ports': ['80:80', '443:443'],
            'volumes': ['./html:/usr/share/nginx/html'],
            'networks': ['frontend'],
            'depends_on': ['api']
        },
        {
            'name': 'api',
            'image': 'python:3.11-slim',
            'container_name': 'myapp-api',
            'ports': ['8000:8000'],
            'volumes': ['./app:/app'],
            'environment': {
                'DATABASE_URL': 'postgresql://user:pass@db:5432/mydb',
                'REDIS_URL': 'redis://cache:6379/0',
                'DEBUG': 'false'
            },
            'networks': ['frontend', 'backend'],
            'depends_on': ['db', 'cache']
        },
        {
            'name': 'db',
            'image': 'postgres:15',
            'environment': {
                'POSTGRES_USER': 'user',
                'POSTGRES_PASSWORD': 'pass',
                'POSTGRES_DB': 'mydb'
            },
            'volumes': ['postgres_data:/var/lib/postgresql/data'],
            'networks': ['backend']
        },
        {
            'name': 'cache',
            'image': 'redis:7-alpine',
            'networks': ['backend']
        }
    ],
    networks=[
        {'name': 'frontend'},
        {'name': 'backend'}
    ]
)

10.4 博客文章列表

templates/blog/index.html

html
{% extends "base.html" %}

{% block title %}博客文章 - {{ site_name }}{% endblock %}

{% block content %}
<div class="blog-container">
    <h1>博客文章</h1>
    
    {# 分类筛选 #}
    <div class="filters">
        <a href="/blog" class="{% if not current_category %}active{% endif %}">全部</a>
        {% for category in categories %}
        <a href="/blog?category={{ category.slug }}" 
           class="{% if current_category == category.slug %}active{% endif %}">
            {{ category.name }}
        </a>
        {% endfor %}
    </div>
    
    {# 文章列表 #}
    <div class="articles">
        {% if articles %}
            {% for article in articles %}
            <article class="article-card">
                <header>
                    <h2>
                        <a href="/blog/{{ article.slug }}">{{ article.title }}</a>
                    </h2>
                    <div class="meta">
                        <span class="author">{{ article.author.name }}</span>
                        <span class="date">{{ article.published_at|date('Y-m-d') }}</span>
                        <span class="category">{{ article.category.name }}</span>
                    </div>
                </header>
                
                <div class="excerpt">
                    {{ article.excerpt|truncate(200) }}
                </div>
                
                <footer>
                    <a href="/blog/{{ article.slug }}" class="read-more">阅读全文 →</a>
                    <span class="tags">
                        {% for tag in article.tags[:5] %}
                        <span class="tag">{{ tag.name }}</span>
                        {% endfor %}
                    </span>
                </footer>
            </article>
            {% endfor %}
            
            {# 分页 #}
            <nav class="pagination">
                {% if pagination.has_prev %}
                <a href="/blog?page={{ pagination.prev_num }}">← 上一页</a>
                {% endif %}
                
                <span class="page-info">
                    第 {{ pagination.page }} / {{ pagination.pages }} 页
                </span>
                
                {% if pagination.has_next %}
                <a href="/blog?page={{ pagination.next_num }}">下一页 →</a>
                {% endif %}
            </nav>
        {% else %}
            <div class="no-articles">
                <p>暂无文章</p>
            </div>
        {% endif %}
    </div>
</div>
{% endblock %}

11. 最佳实践

11.1 安全建议

python
# ✅ 启用自动转义
env = Environment(
    loader=FileSystemLoader('templates'),
    autoescape=True  # 防止 XSS 攻击
)

# ✅ 使用沙箱环境执行不可信模板
from jinja2.sandbox import SandboxedEnvironment

sandbox = SandboxedEnvironment()

# ❌ 不要直接渲染用户输入的模板
# 危险:user_template = Template(user_input)

# ✅ 如果必须渲染用户内容,使用沙箱
try:
    result = sandbox.from_string(user_input).render()
except Exception as e:
    # 处理不安全操作
    result = "模板渲染失败"

11.2 性能优化

python
# ✅ 启用模板缓存
env = Environment(
    loader=FileSystemLoader('templates'),
    cache_size=400,      # 缓存 400 个模板
    auto_reload=False    # 生产环境关闭自动重载
)

# ✅ 预编译模板
template = env.get_template('index.html')
# 模板会被缓存,后续调用直接使用

# ✅ 批量渲染时使用相同的 Environment
# 避免重复创建 Environment 实例

# ✅ 使用 trim_blocks 和 lstrip_blocks 减少输出体积
env = Environment(
    trim_blocks=True,    # 删除块标签后的第一个换行
    lstrip_blocks=True   # 删除块标签前的空白
)

11.3 代码组织

templates/
├── base.html              # 基础模板
├── components/            # 可复用组件
│   ├── header.html
│   ├── footer.html
│   ├── navigation.html
│   └── forms.html
├── pages/                 # 页面模板
│   ├── index.html
│   ├── about.html
│   └── contact.html
├── emails/                # 邮件模板
│   ├── welcome.html
│   └── reset_password.html
└── macros/                # 宏模板
    ├── forms.html
    └── ui.html

11.4 命名规范

jinja2
{# ✅ 好的命名 #}
{% block page_title %}{% endblock %}
{% macro form_input(name, type) %}{% endmacro %}
{% if user.is_authenticated %}{% endif %}

{# ❌ 避免的命名 #}
{% block t %}{% endblock %}              {# 含义不明 #}
{% macro f(n, t) %}{% endmacro %}        {# 缩写难懂 #}
{% if u.a %}{% endif %}                  {# 过度简化 #}

11.5 错误处理

python
from jinja2 import Environment, FileSystemLoader, TemplateNotFound, UndefinedError

env = Environment(loader=FileSystemLoader('templates'))

try:
    template = env.get_template('index.html')
    html = template.render(user=user)
except TemplateNotFound:
    # 模板不存在
    html = "<h1>404 - 模板未找到</h1>"
except UndefinedError as e:
    # 未定义的变量
    print(f"模板错误:{e}")
    html = "<h1>渲染错误</h1>"
except Exception as e:
    # 其他错误
    print(f"未知错误:{e}")
    html = "<h1>服务器错误</h1>"

11.6 调试技巧

python
# ✅ 启用调试模式
env = Environment(
    loader=FileSystemLoader('templates'),
    auto_reload=True,    # 模板变更自动重载
    undefined=DebugUndefined  # 未定义变量时抛出详细错误
)

# ✅ 使用 StrictUndefined 捕获所有未定义变量
from jinja2 import StrictUndefined

env = Environment(undefined=StrictUndefined)

# ✅ 打印模板语法树
template = env.get_template('index.html')
print(template.render.__code__.co_consts)

# ✅ 记录模板渲染日志
import logging

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger('jinja2')

11.7 常见陷阱

jinja2
{# ❌ 陷阱 1:忘记转义用户输入 #}
{{ user_input }}  {# 可能包含 XSS 攻击 #}

{# ✅ 正确做法 #}
{{ user_input|escape }}
{# 或启用 autoescape=True #}

{# ❌ 陷阱 2:在循环中修改列表 #}
{% for item in items %}
    {% if item.should_remove %}
        {% set items = items|reject('equalto', item) %}
    {% endif %}
{% endfor %}

{# ✅ 正确做法:在 Python 中预处理数据 #}

{# ❌ 陷阱 3:过度复杂的模板逻辑 #}
{% if a and b or c and not d or e and f %}...{% endif %}

{# ✅ 正确做法:将复杂逻辑移到 Python 代码中 #}
{% if should_show_content %}...{% endif %}

📚 参考资源


文档版本:1.0最后更新:2026-03-29作者:小蛋蛋 🦞