Jinja2 模板引擎详细教程

33次阅读
没有评论

共计 14828 个字符,预计需要花费 38 分钟才能阅读完成。

目录

  1. Jinja2 简介
  2. 基础语法
  3. 控制结构
  4. 过滤器
  5. 模板继承
  6. 包含
  7. 高级特性
  8. 在 Flask 中使用 Jinja2
  9. 最佳实践

Jinja2 简介

Jinja2 是一个现代的、设计友好的 Python 模板引擎,它是 Flask 框架的默认模板引擎。Jinja2 模板允许你在 HTML 中嵌入动态内容,使用类似 Python 的语法。

主要特性

  • 安全的 HTML 转义:自动转义 HTML 特殊字符,防止 XSS 攻击
  • 模板继承:支持模板的继承和块重写
  • 强大的过滤器系统:内置丰富的过滤器,支持自定义过滤器
  • 宏系统:类似函数的功能,可重用模板代码
  • 高性能:编译模板为 Python 字节码,执行效率高

基础语法

变量输出

{# 基本变量输出 #}
<h1>Hello, {{name}}!</h1>
<p>Your age is: {{age}}</p>

{# 对象属性访问 #}
<p>User: {{user.username}}</p>
<p>Email: {{user.email}}</p>

{# 字典访问 #}
<p>Value: {{my_dict.key}}</p>
<p>Value: {{my_dict['key'] }}</p>

{# 列表访问 #}
<p>First item: {{my_list[0] }}</p>

{# 方法调用 #}
<p>Length: {{my_string.upper() }}</p>

注释

{# 这是单行注释 #}

{#
  这是
  多行
  注释
#}

表达式

{# 数学运算 #}
<p>Total: {{price * quantity}}</p>
<p>Average: {{(a + b) / 2 }}</p>

{# 字符串连接 #}
<p>{{"Hello" + name + "!"}}</p>

{# 比较运算 #}
{% if count > 10 %}
    <p>More than 10 items</p>
{% endif %}

{# 逻辑运算 #}
{% if user and user.is_active %}
    <p>Active user</p>
{% endif %}

控制结构

条件语句

{# 基本 if 语句 #}
{% if user %}
    <p>Hello, {{user.name}}!</p>
{% endif %}

{# if-else 语句 #}
{% if temperature > 30 %}
    <p>It's hot outside</p>
{% else %}
    <p>It's not too hot</p>
{% endif %}

{# if-elif-else 语句 #}
{% if score >= 90 %}
    <p>Grade: A</p>
{% elif score >= 80 %}
    <p>Grade: B</p>
{% elif score >= 70 %}
    <p>Grade: C</p>
{% else %}
    <p>Grade: F</p>
{% endif %}

{# 复杂条件 #}
{% if user.role in ['admin', 'moderator'] and user.is_active %}
    <p>You have special privileges</p>
{% endif %}

循环语句

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

{# 带索引的循环 #}
<table>
{% for user in users %}
    <tr>
        <td>{{loop.index}}</td>
        <td>{{user.name}}</td>
        <td>{{user.email}}</td>
    </tr>
{% endfor %}
</table>

{# 循环控制变量 #}
<ul>
{% for item in items %}
    <li class="{% if loop.first %}first{% endif %} {% if loop.last %}last{% endif %}">
        {{item}}
        {% if not loop.last %},{% endif %}
    </li>
{% endfor %}
</ul>

{# 循环字典 #}
<dl>
{% for key, value in my_dict.items() %}
    <dt>{{key}}</dt>
    <dd>{{value}}</dd>
{% endfor %}
</dl>

{# 空循环处理 #}
{% for user in users %}
    <p>{{user.name}}</p>
{% else %}
    <p>No users found.</p>
{% endfor %}

{# 循环中断和继续 #}
{% for number in numbers %}
    {% if number > 100 %}
        {% break %}
    {% endif %}
    {% if number % 2 == 0 %}
        {% continue %}
    {% endif %}
    <p>{{number}}</p>
{% endfor %}

循环控制变量

变量 描述
loop.index 当前迭代的索引(从 1 开始)
loop.index0 当前迭代的索引(从 0 开始)
loop.revindex 反向迭代的索引(从 1 开始)
loop.revindex0 反向迭代的索引(从 0 开始)
loop.first 如果是第一次迭代,则为 True
loop.last 如果是最后一次迭代,则为 True
loop.length 序列中的项目数量
loop.cycle 在序列值之间循环的辅助函数
loop.depth 当前在递归循环中的深度
loop.depth0 当前在递归循环中的深度(从 0 开始)

过滤器

内置过滤器

{# 字符串过滤器 #}
<p>{{text|upper}}</p>
<p>{{text|lower}}</p>
<p>{{text|title}}</p>
<p>{{text|capitalize}}</p>
<p>{{text|trim}}</p>
<p>{{text|replace('old', 'new') }}</p>
<p>{{text|truncate(50) }}</p>

{# 数字过滤器 #}
<p>{{number|round(2) }}</p>
<p>{{number|abs}}</p>
<p>{{percentage|format('0.2%') }}</p>

{# 列表过滤器 #}
<p>{{list|length}}</p>
<p>{{list|first}}</p>
<p>{{list|last}}</p>
<p>{{list|sort}}</p>
<p>{{list|join(',') }}</p>

{# 日期过滤器 #}
<p>{{date|datetimeformat}}</p>
<p>{{date|datetimeformat('%Y-%m-%d') }}</p>

{# 默认值过滤器 #}
<p>{{value|default('N/A') }}</p>
<p>{{value|default('N/A', true) }}</p>  {# 对于 false 值也使用默认值 #}

{# 安全过滤器 #}
<p>{{html_content|safe}}</p>          {# 标记为安全 HTML #}
<p>{{text|escape}}</p>               {# HTML 转义 #}
<p>{{text|e}}</p>                    {# escape 的简写 #}

{# 链式过滤器 #}
<p>{{text|trim|upper|truncate(100) }}</p>

常用过滤器示例

{# 格式化货币 #}
<p>Price: {{price|format_currency}}</p>

{# 相对时间 #}
<p>Posted {{post_date|timesince}}</p>

{# URL 编码 #}
<a href="/search?q={{query|urlencode}}">Search</a>

{# 单词计数 #}
<p>{{text|wordcount}} words</p>

{# 随机排序 #}
<ul>
{% for item in items|shuffle %}
    <li>{{item}}</li>
{% endfor %}
</ul>

{# 分组操作 #}
{% for group in users|groupby('department') %}
    <h3>{{group.grouper}}</h3>
    <ul>
    {% for user in group.list %}
        <li>{{user.name}}</li>
    {% endfor %}
    </ul>
{% endfor %}

模板继承

基础模板 (base.html)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}My Website{% endblock %}</title>

    {# CSS 块 #}
    {% block styles %}
    <link rel="stylesheet" href="{{url_for('static', filename='css/main.css') }}">
    {% endblock %}

    {# 额外的头部内容 #}
    {% block head_extra %}{% endblock %}
</head>
<body>
    {# 导航栏 #}
    {% block navbar %}
    <nav class="navbar">
        <div class="nav-container">
            <a href="{{url_for('index') }}" class="nav-logo">MySite</a>
            <ul class="nav-menu">
                <li><a href="{{url_for('about') }}">About</a></li>
                <li><a href="{{url_for('contact') }}">Contact</a></li>
            </ul>
        </div>
    </nav>
    {% endblock %}

    {# 主要内容区域 #}
    <main class="container">
        {# 闪存消息 #}
        {% with messages = get_flashed_messages() %}
            {% if messages %}
                <div class="flash-messages">
                    {% for message in messages %}
                        <div class="flash-message">{{message}}</div>
                    {% endfor %}
                </div>
            {% endif %}
        {% endwith %}

        {# 页面内容 #}
        {% block content %}{% endblock %}
    </main>

    {# 页脚 #}
    {% block footer %}
    <footer class="footer">
        <p>&copy; 2024 My Website. All rights reserved.</p>
    </footer>
    {% endblock %}

    {# JavaScript 块 #}
    {% block scripts %}
    <script src="{{url_for('static', filename='js/main.js') }}"></script>
    {% endblock %}

    {# 额外的脚本 #}
    {% block scripts_extra %}{% endblock %}
</body>
</html>

子模板使用

{# 继承基础模板 #}
{% extends "base.html" %}

{# 设置页面标题 #}
{% block title %}Home Page - My Website{% endblock %}

{# 添加额外的 CSS #}
{% block styles %}
    {{super() }}  {# 保留父模板的样式 #}
    <link rel="stylesheet" href="{{url_for('static', filename='css/home.css') }}">
{% endblock %}

{# 重写导航栏 #}
{% block navbar %}
<nav class="navbar home-navbar">
    <div class="nav-container">
        <a href="{{url_for('index') }}" class="nav-logo">HomeSite</a>
        <ul class="nav-menu">
            <li><a href="{{url_for('index') }}">Home</a></li>
            <li><a href="{{url_for('products') }}">Products</a></li>
            <li><a href="{{url_for('about') }}">About</a></li>
        </ul>
    </div>
</nav>
{% endblock %}

{# 主要内容 #}
{% block content %}
<div class="hero-section">
    <h1>Welcome to Our Website</h1>
    <p>This is the home page content.</p>

    {# 使用宏 #}
    {{macros.render_feature_cards(features) }}
</div>

<div class="content-section">
    <h2>Latest News</h2>
    {% for news_item in news %}
        <article class="news-article">
            <h3>{{news_item.title}}</h3>
            <p class="meta">Posted on {{news_item.date|datetimeformat}}</p>
            <p>{{news_item.content|truncate(200) }}</p>
            <a href="{{url_for('news_detail', id=news_item.id) }}">Read more</a>
        </article>
    {% else %}
        <p>No news available at the moment.</p>
    {% endfor %}
</div>
{% endblock %}

{# 添加页面特定的 JavaScript #}
{% block scripts_extra %}
<script>
    // 首页特定的 JavaScript 代码
    document.addEventListener('DOMContentLoaded', function() {console.log('Home page loaded');
    });
</script>
{% endblock %}

定义和使用宏

{# 在单独的宏文件中定义 #}
{# macros.html #}
{% macro render_form_field(field, class='form-control') %}
<div class="form-group">
    <label for="{{field.id}}">{{field.label.text}}</label>
    {{field(class=class, **kwargs) }}
    {% if field.errors %}
        <div class="errors">
            {% for error in field.errors %}
                <span class="error">{{error}}</span>
            {% endfor %}
        </div>
    {% endif %}
</div>
{% endmacro %}

{% macro render_button(text, type='button', class='btn btn-primary') %}
<button type="{{type}}" class="{{class}}">{{text}}</button>
{% endmacro %}

{% macro render_card(title, content, footer=None) %}
<div class="card">
    <div class="card-header">
        <h3>{{title}}</h3>
    </div>
    <div class="card-body">
        {{content}}
    </div>
    {% if footer %}
    <div class="card-footer">
        {{footer}}
    </div>
    {% endif %}
</div>
{% endmacro %}

{% macro render_pagination(pagination, endpoint) %}
{% if pagination.pages > 1 %}
<nav class="pagination">
    <ul>
        {% if pagination.has_prev %}
        <li><a href="{{url_for(endpoint, page=pagination.prev_num) }}">Previous</a></li>
        {% endif %}

        {% for page in pagination.iter_pages() %}
            {% if page %}
                <li class="{% if page == pagination.page %}active{% endif %}">
                    <a href="{{url_for(endpoint, page=page) }}">{{page}}</a>
                </li>
            {% else %}
                <li class="disabled"><span>…</span></li>
            {% endif %}
        {% endfor %}

        {% if pagination.has_next %}
        <li><a href="{{url_for(endpoint, page=pagination.next_num) }}">Next</a></li>
        {% endif %}
    </ul>
</nav>
{% endif %}
{% endmacro %}

在模板中使用宏

{# 导入宏文件 #}
{% from "macros.html" import render_form_field, render_button, render_card, render_pagination %}

{# 使用表单字段宏 #}
<form method="POST">
    {{form.hidden_tag() }}
    {{render_form_field(form.username) }}
    {{render_form_field(form.email) }}
    {{render_form_field(form.password) }}
    {{render_button('Submit', type='submit') }}
</form>

{# 使用卡片宏 #}
<div class="card-grid">
    {{ render_card(
        "Feature One", 
        "This is the description of feature one.", 
        footer=render_button('Learn More')
    ) }}

    {{ render_card(
        "Feature Two", 
        "This is the description of feature two."
    ) }}
</div>

{# 使用分页宏 #}
{{render_pagination(posts, 'blog.index') }}

包含

使用包含组织模板

{# header.html #}
<header class="site-header">
    <div class="container">
        <div class="logo">
            <a href="{{url_for('index') }}">
                <img src="{{url_for('static', filename='images/logo.png') }}" alt="Logo">
            </a>
        </div>
        <nav class="main-nav">
            <ul>
                <li><a href="{{url_for('index') }}">Home</a></li>
                <li><a href="{{url_for('about') }}">About</a></li>
                <li><a href="{{url_for('services') }}">Services</a></li>
                <li><a href="{{url_for('contact') }}">Contact</a></li>
            </ul>
        </nav>
    </div>
</header>

{# footer.html #}
<footer class="site-footer">
    <div class="container">
        <div class="footer-content">
            <div class="footer-section">
                <h4>Company</h4>
                <ul>
                    <li><a href="{{url_for('about') }}">About Us</a></li>
                    <li><a href="{{url_for('careers') }}">Careers</a></li>
                    <li><a href="{{url_for('contact') }}">Contact</a></li>
                </ul>
            </div>
            <div class="footer-section">
                <h4>Resources</h4>
                <ul>
                    <li><a href="{{url_for('blog') }}">Blog</a></li>
                    <li><a href="{{url_for('docs') }}">Documentation</a></li>
                    <li><a href="{{url_for('support') }}">Support</a></li>
                </ul>
            </div>
        </div>
        <div class="footer-bottom">
            <p>&copy; 2024 My Company. All rights reserved.</p>
        </div>
    </div>
</footer>

{# 在主模板中使用包含 #}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}My Site{% endblock %}</title>
</head>
<body>
    {% include 'header.html' %}

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

    {% include 'footer.html' %}
</body>
</html>

高级特性

自定义过滤器

# 在 Flask 应用中注册自定义过滤器
from flask import Flask
import jinja2

app = Flask(__name__)

@app.template_filter('datetimeformat')
def datetimeformat(value, format='%Y-%m-%d %H:%M:%S'):
    if value is None:
        return ""
    return value.strftime(format)

@app.template_filter('format_currency')
def format_currency(value):
    if value is None:
        return "$0.00"
    return f"${value:,.2f}"

@app.template_filter('timesince')
def timesince(dt):
    now = datetime.now()
    diff = now - dt

    if diff.days > 365:
        years = diff.days // 365
        return f"{years} year{'s'if years > 1 else''} ago"
    elif diff.days > 30:
        months = diff.days // 30
        return f"{months} month{'s'if months > 1 else''} ago"
    elif diff.days > 0:
        return f"{diff.days} day{'s'if diff.days > 1 else''} ago"
    elif diff.seconds > 3600:
        hours = diff.seconds // 3600
        return f"{hours} hour{'s'if hours > 1 else''} ago"
    elif diff.seconds > 60:
        minutes = diff.seconds // 60
        return f"{minutes} minute{'s'if minutes > 1 else''} ago"
    else:
        return "just now"

在模板中使用自定义过滤器

{# 使用自定义过滤器 #}
<p>Published: {{post.created_at|datetimeformat}}</p>
<p>Price: {{product.price|format_currency}}</p>
<p>Last active: {{user.last_seen|timesince}}</p>

上下文处理器

# 添加全局变量到所有模板上下文
@app.context_processor
def inject_global_variables():
    return {
        'site_name': 'My Awesome Site',
        'current_year': datetime.now().year,
        'config': app.config
    }

@app.context_processor
def utility_processor():
    def format_phone(number):
        return f"({number[:3]}) {number[3:6]}-{number[6:]}"

    return dict(format_phone=format_phone)

在模板中使用上下文变量

<footer>
    <p>&copy; {{current_year}} {{site_name}}</p>
    <p>Contact: {{format_phone('1234567890') }}</p>
</footer>

在 Flask 中使用 Jinja2

基本配置

from flask import Flask, render_template, url_for

app = Flask(__name__)

# 自定义模板过滤器
@app.template_filter('reverse')
def reverse_filter(s):
    return s[::-1]

# 上下文处理器
@app.context_processor
def utility_processor():
    def format_price(amount):
        return f"${amount:.2f}"
    return dict(format_price=format_price)

# 路由和视图
@app.route('/')
def index():
    return render_template('index.html', 
                         title='Home Page',
                         user={'name': 'John', 'role': 'admin'})

@app.route('/users')
def users():
    users = [
        {'name': 'Alice', 'email': 'alice@example.com'},
        {'name': 'Bob', 'email': 'bob@example.com'},
        {'name': 'Charlie', 'email': 'charlie@example.com'}
    ]
    return render_template('users.html', users=users)

@app.route('/products')
def products():
    products = [
        {'name': 'Product 1', 'price': 19.99, 'in_stock': True},
        {'name': 'Product 2', 'price': 29.99, 'in_stock': False},
        {'name': 'Product 3', 'price': 39.99, 'in_stock': True}
    ]
    return render_template('products.html', products=products)

完整的 Flask 应用示例

# app.py
from flask import Flask, render_template, request, flash, redirect, url_for
from datetime import datetime
import os

app = Flask(__name__)
app.secret_key = 'your-secret-key-here'

# 示例数据
blog_posts = [
    {
        'id': 1,
        'title': 'First Blog Post',
        'content': 'This is the content of the first blog post.',
        'author': 'John Doe',
        'created_at': datetime(2024, 1, 15),
        'category': 'General'
    },
    {
        'id': 2,
        'title': 'Second Blog Post',
        'content': 'This is the content of the second blog post.',
        'author': 'Jane Smith',
        'created_at': datetime(2024, 1, 20),
        'category': 'Technology'
    }
]

@app.route('/')
def index():
    return render_template('index.html', 
                         posts=blog_posts[:3],  # 最新 3 篇文章
                         featured=True)

@app.route('/blog')
def blog():
    category = request.args.get('category')
    if category:
        filtered_posts = [p for p in blog_posts if p['category'] == category]
    else:
        filtered_posts = blog_posts

    return render_template('blog.html', 
                         posts=filtered_posts,
                         categories=set(p['category'] for p in blog_posts))

@app.route('/post/<int:post_id>')
def post_detail(post_id):
    post = next((p for p in blog_posts if p['id'] == post_id), None)
    if not post:
        flash('Post not found!', 'error')
        return redirect(url_for('blog'))

    return render_template('post_detail.html', post=post)

# 自定义过滤器
@app.template_filter('excerpt')
def excerpt_filter(text, length=100):
    if len(text) <= length:
        return text
    return text[:length] + '...'

# 上下文处理器
@app.context_processor
def inject_now():
    return {'now': datetime.now()}

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

对应的模板文件

{# templates/blog.html #}
{% extends "base.html" %}

{% block title %}Blog - My Site{% endblock %}

{% block content %}
<div class="blog-container">
    <h1>Blog Posts</h1>

    {# 分类筛选 #}
    <div class="category-filter">
        <strong>Filter by category:</strong>
        <a href="{{url_for('blog') }}" class="{% if not request.args.get('category') %}active{% endif %}">All</a>
        {% for category in categories %}
            <a href="{{url_for('blog', category=category) }}" 
               class="{% if request.args.get('category') == category %}active{% endif %}">
                {{category}}
            </a>
        {% endfor %}
    </div>

    {# 文章列表 #}
    <div class="posts-grid">
        {% for post in posts %}
            <article class="post-card">
                <h2><a href="{{url_for('post_detail', post_id=post.id) }}">{{post.title}}</a></h2>
                <div class="post-meta">
                    <span>By {{post.author}}</span>
                    <span>on {{post.created_at|datetimeformat('%B %d, %Y') }}</span>
                    <span class="category">{{post.category}}</span>
                </div>
                <p>{{post.content|excerpt(150) }}</p>
                <a href="{{url_for('post_detail', post_id=post.id) }}" class="read-more">Read more</a>
            </article>
        {% else %}
            <p>No posts found.</p>
        {% endfor %}
    </div>
</div>
{% endblock %}

最佳实践

1. 模板组织

templates/
├── base.html              # 基础模板
├── includes/              # 包含片段
│   ├── header.html
│   ├── footer.html
│   ├── navigation.html
│   └── sidebar.html
├── macros/                # 宏文件
│   ├── forms.html
│   ├── ui.html
│   └── pagination.html
├── auth/                  # 认证相关模板
│   ├── login.html
│   └── register.html
├── blog/                  # 博客相关模板
│   ├── blog.html
│   ├── post_detail.html
│   └── post_form.html
└── errors/               # 错误页面
    ├── 404.html
    └── 500.html

2. 安全最佳实践

{# 始终对用户输入进行转义 #}
<p>{{user_input|e}}</p>

{# 谨慎使用 safe 过滤器 #}
{% if user.is_trusted %}
    {{user_content|safe}}
{% else %}
    {{user_content|e}}
{% endif %}

{# 使用 url_for 生成 URL,避免硬编码 #}
<a href="{{url_for('user_profile', username=user.username) }}">Profile</a>

{# 不要在模板中执行敏感操作 #}
{# 错误示例 #}
{% if user.password == 'secret' %}  {# 不要这样做!#}

3. 性能优化

{# 使用循环优化 #}
{% for item in items %}
    {# 在循环外计算固定值 #}
    {% set item_class = 'even' if loop.index0 % 2 == 0 else 'odd' %}
    <div class="{{item_class}}">{{item}}</div>
{% endfor %}

{# 避免在循环中进行复杂计算 #}
{# 错误示例 #}
{% for user in users %}
    <p>{{calculate_user_score(user) }}</p>  {# 复杂计算应在视图函数中完成 #}
{% endfor %}

{# 使用缓存 #}
{% cache 300, 'recent_posts' %}
    {# 这部分内容将被缓存 5 分钟 #}
    {% for post in recent_posts %}
        {{render_post_summary(post) }}
    {% endfor %}
{% endcache %}

4. 可维护性技巧

{# 使用有意义的块名称 #}
{% block page_styles %}{% endblock %}
{% block page_scripts %}{% endblock %}
{% block sidebar_content %}{% endblock %}

{# 保持模板简洁 #}
{# 将复杂逻辑移到宏或视图函数中 #}

{# 使用注释说明复杂的模板逻辑 #}
{# 
    这个循环生成一个分页导航
    参数:
    - pagination: 分页对象
    - endpoint: 视图端点名称
#}
{{render_pagination(pagination, endpoint) }}

{# 一致的缩进和格式 #}
{% if condition %}
    <div>
        <p>Content</p>
    </div>
{% endif %}

正文完
 0
一诺
版权声明:本站原创文章,由 一诺 于2025-10-21发表,共计14828字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)
验证码