【Python】Django模板语言 (DTL)

第四章:Django模板语言 (DTL) —— 框架驱动的设计典范

Django Template Language (DTL) 是 Django Web 框架内置的一套强大的模板系统。与 Jinja2 或 Mako 等通用模板引擎相比,DTL 的一个显著特点是它与 Django 框架的深度集成,并且其设计哲学中带有一种“有意限制模板逻辑”的倾向。这种设计旨在鼓励开发者将大部分业务逻辑保留在 Python视图 (Views) 和模型 (Models) 中,让模板专注于表现层。

4.1 DTL 概览:为表现而生,与框架共舞

Django 的创造者们认为,模板系统应该主要负责数据如何呈现,而不是如何产生处理数据。因此,DTL 被设计为一种功能强大但又相对简单的语言,它刻意避免了在模板中执行任意 Python 代码的能力。

DTL 的核心设计原则与特性:

已关注表现 (Focus on Presentation)

DTL 的主要目标是格式化和展示从视图传递过来的数据。它提供了丰富的工具来迭代数据、有条件地显示内容以及格式化输出,但避免了复杂的计算或数据操作。

与 Django 深度集成 (Tight Integration with Django)

自动上下文处理器 (Context Processors):Django 允许定义上下文处理器,这些处理器会自动向所有模板的上下文中添加特定的变量(例如,用户信息 request.user、站点设置等),无需在每个视图中手动添加。
视图与模板的清晰分离: Django 的 MTV (Model-Template-View) 架构鼓励将数据处理逻辑放在视图中,然后视图选择一个模板并传递上下文给它进行渲染。
表单处理: Django 的表单系统与 DTL 配合良好,可以轻松在模板中渲染表单字段、错误信息等。
国际化与本地化: DTL 内置了对国际化 (i18n) 和本地化 (l10n) 的强大支持,包括翻译标签和本地化过滤器。
静态文件管理: DTL 提供了 {% static %} 标签来方便地引用静态文件(CSS, JavaScript, 图片),并能与 Django 的静态文件处理器(如 ManifestStaticFilesStorage)协同工作,实现缓存清除等高级功能。
URL 反向解析: {% url %} 标签允许在模板中通过 URL 名称动态生成 URL,避免了硬编码 URL,使得 URL 结构变更时更易于维护。

有意限制的逻辑能力 (Intentionally Limited Logic)

禁止执行任意 Python 代码: 与 Mako 不同,你不能在 DTL 中直接嵌入任意的 Python 代码块。所有逻辑都必须通过预定义的标签和过滤器来实现。
有限的表达式: DTL 中的变量访问和表达式能力比纯 Python 或 Jinja2 更受限制。例如,不能直接在模板中进行复杂的算术运算或调用带任意参数的方法(除非通过自定义标签或过滤器)。
鼓励自定义扩展: 如果需要更复杂的逻辑,Django 鼓励开发者创建自定义模板标签 (template tags) 和过滤器 (template filters) 来封装这些逻辑,而不是在模板中直接编写。

安全性 (Security)

默认自动 HTML 转义: DTL 默认会对所有通过 {
{ ... }}
输出的变量进行 HTML 转义,以防止 XSS 攻击。可以通过 |safe 过滤器或 {% autoescape off %} 块来覆盖此行为,但需要非常谨慎。
由于不能执行任意 Python,也从源头上减少了模板层面引入安全漏洞的风险。

可读性与易用性 (Readability and Ease of Use)

DTL 的语法设计得相对简单直观,即使对于非 Python 背景的前端开发者或设计师也比较容易上手。
标签和过滤器的名称通常具有描述性。

可扩展性 (Extensibility)

开发者可以创建自定义的简单标签、赋值标签、包含标签以及过滤器,以满足特定项目的需求。

4.2 DTL 的基本配置与使用流程 (在 Django 项目中)

在标准的 Django 项目中,DTL 的配置和使用是框架自动处理的。开发者主要关心的是如何组织模板文件、如何在视图中加载和渲染模板,以及如何编写模板本身。

4.2.1 模板加载器 (Template Loaders) 与 TEMPLATES 设置

Django 通过 settings.py 文件中的 TEMPLATES 配置项来管理模板引擎和加载。一个典型的 TEMPLATES 设置如下:

# settings.py

TEMPLATES = [
    {
            
        'BACKEND': 'django.template.backends.django.DjangoTemplates', # 指定使用DTL后端
        'DIRS': [
            os.path.join(BASE_DIR, 'templates'), # 项目级模板目录 (项目根目录下的 'templates' 文件夹)
            # '/path/to/another/templates/directory', # 可以添加其他项目外的模板目录
        ],
        'APP_DIRS': True, # 允许Django在每个已安装应用的 'templates' 子目录中查找模板
        'OPTIONS': {
            
            'context_processors': [ # 上下文处理器列表
                'django.template.context_processors.debug', # 提供 DEBUG 和 sql_queries 变量
                'django.template.context_processors.request', # 提供 request 对象 (HttpRequest)
                'django.contrib.auth.context_processors.auth', # 提供 user 对象 (当前登录用户) 和 perms 对象 (权限)
                'django.contrib.messages.context_processors.messages', # 提供 messages (来自 Django messages 框架)
                # 自定义上下文处理器可以加在这里
                # 'myapp.context_processors.my_custom_processor',
            ],
            'builtins': [ # 可以添加自定义的内建标签和过滤器模块
                # 'myapp.templatetags.custom_tags_and_filters',
            ],
            'debug': True, # 开发时设为True,提供更详细的错误信息。生产环境应为False。
            # 'loaders': [ ... ], # 可以自定义模板加载器的顺序或类型,默认顺序通常够用
            # 'string_if_invalid': 'INVALID {
            { %s }}', # 当变量无效时显示的字符串 (生产环境用)
        },
    },
    # 如果需要,可以配置其他模板引擎,例如 Jinja2
    # {
            
    #     'BACKEND': 'django.template.backends.jinja2.Jinja2',
    #     'DIRS': [os.path.join(BASE_DIR, 'jinja2_templates')],
    #     'APP_DIRS': True,
    #     'OPTIONS': {
            
    #         'environment': 'myproject.jinja2.environment', # 指向一个返回Jinja2 Environment实例的函数
    #     }
    # },
]

代码解释 (TEMPLATES 设置):

'BACKEND': 'django.template.backends.django.DjangoTemplates': 明确指定使用 Django 内置的模板引擎 (DTL)。
'DIRS': [os.path.join(BASE_DIR, 'templates')]:

这是一个列表,定义了 Django 在查找模板文件时会搜索的项目级目录。
BASE_DIR 通常是 Django 项目的根目录。
这意味着 Django 会在你的项目根目录下的 templates 文件夹中寻找模板。例如,如果你尝试加载 index.html,Django 会查找 your_project_root/templates/index.html
你可以添加多个路径到这个列表中。

'APP_DIRS': True:

如果设置为 True,Django 还会自动在每个已安装的应用程序(在 INSTALLED_APPS 中列出的应用)的目录内查找名为 templates 的子目录。
例如,如果你的项目有一个名为 blog 的应用,并且 APP_DIRSTrue,那么 Django 会在 your_project_root/blog/templates/ 目录中查找模板。
命名空间化模板: 为了避免不同应用间的模板文件名冲突,最佳实践是在应用的 templates 目录下再创建一个以应用名命名的子目录,例如 blog/templates/blog/post_list.html。在模板中引用时,使用 blog/post_list.html

'OPTIONS': 一个包含特定于该模板后端选项的字典。

'context_processors':

一个非常重要的列表,定义了上下文处理器。
上下文处理器是一些 Python 函数,它们接收当前的 HttpRequest 对象作为参数,并返回一个字典。这个字典中的键值对会被自动添加到传递给所有模板的渲染上下文中。
django.template.context_processors.debug: 如果 settings.DEBUGTrue,它会添加 debug=Truesql_queries (一个包含执行过的SQL查询列表,用于调试) 到上下文中。
django.template.context_processors.request: 将当前的 HttpRequest 对象作为 request 变量添加到上下文中。这使得你可以在模板中访问请求的属性,例如 {
{ request.user }}
{
{ request.path }}
{
{ request.GET.query }}
等。
django.contrib.auth.context_processors.auth: 如果 django.contrib.auth 应用已安装,它会添加当前登录的用户对象(通常是 User 模型的实例)作为 user 变量,以及一个 PermWrapper 对象作为 perms 变量(用于权限检查 {% if perms.app_label.can_do_something %})。
django.contrib.messages.context_processors.messages: 如果 django.contrib.messages 应用已安装,它会添加一个包含待显示消息的列表(通过 django.contrib.messages 框架添加的消息)作为 messages 变量。

'builtins':

一个 Python 模块路径的列表,这些模块中定义的自定义模板标签和过滤器会自动注册为内建的,无需在每个模板中使用 {% load ... %} 标签来加载它们。
通常用于加载项目级别或广泛使用的自定义标签/过滤器。

'debug':

布尔值。如果为 True (通常与 settings.DEBUG 同步),当模板渲染发生错误时,Django 会显示一个详细的调试页面,包含错误信息、模板源代码片段和上下文数据。
生产环境中必须设置为 False,以避免泄露敏感信息。

'loaders': (可选)

允许你显式定义模板加载器的列表和顺序。默认情况下,Django 使用一组标准的加载器,包括文件系统加载器 (django.template.loaders.filesystem.Loader) 和应用目录加载器 (django.template.loaders.app_directories.Loader)。
只有在需要非常特殊的加载行为时(例如,从数据库加载模板,或者有自定义的加载器优先级)才需要修改此项。
默认顺序是先查找 filesystem.Loader (DIRS 中指定的路径),然后查找 app_directories.Loader (APP_DIRS=True 时)。

'string_if_invalid': (可选)

一个字符串,用于在模板中遇到无效(未定义或访问出错)的变量时显示。例如,'INVALID {
{ %s }}'
,其中 %s 会被替换为无效的变量名。
这主要用于生产环境,以避免因小的数据问题导致页面部分内容不显示,而是显示一个明确的标记。在开发环境中,通常希望看到详细的错误或让 debug=True 来处理。
默认情况下,无效变量在模板中通常不会输出任何内容 (类似于空字符串)。

4.2.2 在 Django 视图中渲染模板

Django 视图函数 (或基于类的视图的方法) 负责处理用户请求、准备数据,并选择一个模板来渲染这些数据,最终返回一个 HttpResponse

有几种常见的方式来渲染模板:

使用 django.shortcuts.render() (最常用)

这是一个便捷函数,它封装了加载模板、创建 Context、渲染模板和返回 HttpResponse 的整个过程。
语法: render(request, template_name, context=None, content_type=None, status=None, using=None)

request: 当前的 HttpRequest 对象。
template_name: 字符串,要加载的模板文件的路径(相对于模板目录)。
context: (可选) 一个字典,包含要传递给模板的上下文数据。
content_type, status, using: 其他 HttpResponse 的可选参数。

示例 (在 views.py 中):

# myapp/views.py
from django.shortcuts import render # 导入render快捷函数
from django.http import HttpRequest, HttpResponse # 导入HttpRequest和HttpResponse
import datetime # 导入datetime模块

# 假设有一个简单的模型
# class Article(models.Model):
#     title = models.CharField(max_length=200)
#     content = models.TextField()
#     pub_date = models.DateTimeField(auto_now_add=True)

def article_list_view(request: HttpRequest) -> HttpResponse: # 定义文章列表视图函数
    """视图函数,用于显示文章列表。"""
    # 模拟从数据库获取文章数据
    articles_data = [ # 文章数据列表
        {
                'id': 1, 'title': 'Django for Beginners', 'author': 'Alice', 'pub_date': datetime.date(2023, 1, 15)}, # 文章1
        {
                'id': 2, 'title': 'Advanced Django Patterns', 'author': 'Bob', 'pub_date': datetime.date(2023, 3, 22)}, # 文章2
        {
                'id': 3, 'title': 'Understanding DTL', 'author': 'Charlie', 'pub_date': datetime.date(2023, 5, 10)} # 文章3
    ] # 文章数据列表定义结束

    current_time = datetime.datetime.now() # 获取当前时间

    context = {
                 # 定义传递给模板的上下文
        'page_heading': 'Our Latest Articles', # 页面标题
        'articles': articles_data, # 文章列表数据
        'current_server_time': current_time, # 当前服务器时间
        'display_ad': True # 是否显示广告的标志
    } # 上下文定义结束

    # 渲染 'myapp/article_list.html' 模板,并传入上下文
    # Django 会根据 TEMPLATES 设置自动查找这个模板文件
    # (例如,在 myapp/templates/myapp/article_list.html 或 project/templates/myapp/article_list.html)
    return render(request, 'myapp/article_list.html', context) # 使用render函数渲染模板并返回HttpResponse

def simple_greeting_view(request: HttpRequest) -> HttpResponse: # 定义简单的问候视图函数
    """一个非常简单的视图,只传递少量数据。"""
    username = request.user.username if request.user.is_authenticated else "Guest" # 获取用户名,如果未登录则为Guest
    context = {
                 # 定义模板上下文
        'greet_name': username, # 问候名称
        'time_of_day': "afternoon" if 12 <= datetime.datetime.now().hour < 18 else "morning/evening" # 根据时间判断时段
    } # 上下文定义结束
    return render(request, 'myapp/greeting.html', context) # 渲染greeting.html模板

对应的模板文件示例 (myapp/templates/myapp/article_list.html):

{# myapp/templates/myapp/article_list.html #}
{% extends "base_generic.html" %} {# 假设有一个基础模板 #}

{% load static %} {# 加载static标签库,用于引用静态文件 #}

{% block title %}Article List{% endblock %} {# 覆盖基础模板的title块 #}

{% block head_extra %} {# 假设基础模板有这个块用于添加额外的head内容 #}
    <link rel="stylesheet" href="{% static 'myapp/css/article_styles.css' %}"> {# 引用应用的CSS文件 #}
{% endblock %}

{% block content %} {# 覆盖基础模板的content块 #}
    <h1>{
               { page_heading }}</h1>

    {% if articles %} {# 检查文章列表是否存在且不为空 #}
        <ul>
            {% for article in articles %} {# 遍历文章列表 #}
                <li>
                    <h2>
                        <a href="{% url 'myapp:article_detail' article.id %}"> {# 使用url标签反向解析文章详情页URL #}
                            {
               { article.title|capfirst }} {# 显示文章标题,首字母大写 #}
                        </a>
                    </h2>
                    <p>By: {
               { article.author }} on {
               { article.pub_date|date:"F j, Y" }}</p> {# 显示作者和格式化的发布日期 #}
                </li>
            {% endfor %}
        </ul>
    {% else %}
        <p>No articles are available at the moment. Please check back later.</p> {# 如果没有文章则显示此消息 #}
    {% endif %}

    <p><em>Report generated at: {
               { current_server_time|time:"H:i:s" }}</em></p> {# 显示格式化的当前时间 #}

    {% if display_ad %} {# 条件判断,是否显示广告 #}
        <div class="advertisement">
            <p>Special offer! Visit our sponsors!</p> {# 广告内容 #}
        </div>
    {% endif %}
{% endblock %}

代码解释 (article_list.html):

{% extends "base_generic.html" %}: 表明此模板继承自 base_generic.html
{% load static %}: 加载 static 标签库,使得 {% static ... %} 标签可用。
{% block title %}...{% endblock %}{% block content %}...{% endblock %}: 定义或覆盖父模板中的同名块。
{
{ page_heading }}
: 输出从上下文中传递的 page_heading 变量。
{% if articles %} ... {% else %} ... {% endif %}: 条件判断。
{% for article in articles %} ... {% endfor %}: 循环遍历 articles 列表。
{% url 'myapp:article_detail' article.id %}: DTL 的 url 标签,用于反向解析 URL。它会查找名为 article_detail 的 URL模式 (在 myapp 命名空间下),并传入 article.id 作为参数来构建 URL。
{
{ article.title|capfirst }}
: capfirst 是一个过滤器,将变量的第一个字母大写。
{
{ article.pub_date|date:"F j, Y" }}
: date 过滤器,用于格式化日期对象。"F j, Y" 是一个日期格式化字符串 (例如 “January 15, 2023”)。
{
{ current_server_time|time:"H:i:s" }}
: time 过滤器,用于格式化时间对象。

手动加载和渲染 (更底层,较少直接使用)

你可以手动加载模板、创建上下文并调用模板的 render() 方法。
这在你需要对渲染过程进行更精细控制,或者在 Django 视图之外使用 DTL 时可能会用到。
步骤:

django.template 导入 loaderContext
使用 loader.get_template(template_name) 获取 Template 对象。
创建一个 Context 对象 (或简单的字典,Django 会自动转换)。
调用 template.render(context)
将渲染后的字符串包装在 HttpResponse 中。

示例:

# myapp/views.py (alternative way)
from django.template import loader, Context # 导入loader和Context
from django.http import HttpResponse, HttpRequest # 导入HttpResponse和HttpRequest
import datetime # 导入datetime模块

def manual_render_view(request: HttpRequest) -> HttpResponse: # 定义手动渲染视图函数
    """演示手动加载和渲染模板。"""
    try:
        template = loader.get_template('myapp/manual_example.html') # 手动加载模板文件
    except loader.TemplateDoesNotExist: # 捕获模板未找到异常
        return HttpResponse("Error: Template 'myapp/manual_example.html' not found.", status=404) # 返回404错误响应

    context_data = {
                 # 定义模板上下文数据
        'message': 'This template was rendered manually!', # 消息内容
        'current_time': datetime.datetime.now(), # 当前时间
        'items': ['One', 'Two', 'Three'] # 项目列表
    } # 上下文数据定义结束

    # Django 的 render() 快捷函数会自动处理 request 上下文处理器。
    # 手动创建 Context 时,如果需要 request 对象或其上下文处理器添加的变量,
    # 需要将 request 对象传递给 Context 构造函数。
    # context_obj = Context(context_data, request=request) # 创建Context对象,并传入request
    # 或者,更常见的是,Django 1.8+ 允许直接传递字典,它会自动创建Context
    # 并且 loader.render_to_string 或 Template.render() 会处理request
    
    # 如果仅使用 Template.render(dict_context), 它不会自动运行上下文处理器。
    # Template.render(context_obj) 如果 context_obj 是 Context 实例且创建时传入了 request, 则会处理。
    # 最简单且确保上下文处理器运行的方式是使用 render_to_string:
    
    from django.template.loader import render_to_string # 导入render_to_string

    # render_to_string 会加载模板、应用上下文处理器 (如果request提供了),并渲染为字符串
    rendered_string = render_to_string('myapp/manual_example.html', context_data, request=request) # 使用render_to_string渲染为字符串

    # 或者,如果坚持完全手动:
    # 1. 获取模板
    # template = loader.get_template('myapp/manual_example.html')
    # 2. 创建一个完整的Context,包含处理器添加的内容 (较复杂)
    #    full_context_dict = {}
    #    for processor in loader.get_engine().template_context_processors:
    #        full_context_dict.update(processor(request))
    #    full_context_dict.update(context_data)
    #    context_obj_full = Context(full_context_dict)
    #    rendered_string = template.render(context_obj_full)

    return HttpResponse(rendered_string) # 将渲染后的字符串包装在HttpResponse中返回

对应的模板文件 (myapp/templates/myapp/manual_example.html):

{# myapp/templates/myapp/manual_example.html #}
<h1>Manual Render Example</h1>
<p>{
               { message }}</p>
<p>Rendered at: {
               { current_time|date:"Y-m-d H:i:s" }}</p>
<p>Request path (if available): {
               { request.path|default:"N/A" }}</p> {# 依赖于 request 上下文处理器 #}
<p>User (if available): {
               { user.username|default:"Anonymous" }}</p> {# 依赖于 auth 上下文处理器 #}
<ul>
    {% for item in items %}
        <li>{
               { item }}</li>
    {% endfor %}
</ul>

代码解释 (manual_render_view):

loader.get_template(...): 获取 Template 对象。
render_to_string('myapp/manual_example.html', context_data, request=request): 这是一个更推荐的手动渲染方式(如果只是想得到渲染后的字符串)。它会负责加载模板、创建包含上下文处理器结果的完整上下文(因为传入了 request),并渲染模板。
直接使用 template.render(context_dictionary) 不会自动运行上下文处理器。如果你需要它们(例如为了 {
{ request }}
{
{ user }}
),你需要手动构建一个包含这些处理器结果的 Context 对象,或者确保你的 context_dictionary 已经包含了所有需要的值,或者使用像 render_to_string 这样更高级的辅助函数。
render() 快捷函数之所以方便,就是因为它在内部为你处理了所有这些细节。

使用 django.template.loader.render_to_string():

这个函数与 render() 类似,但不返回 HttpResponse。它只加载模板、应用上下文(包括上下文处理器,如果提供了 request 参数)并返回渲染后的字符串。
这在需要将模板渲染结果用于其他目的(例如,生成邮件内容、AJAX响应的HTML片段)时非常有用。
语法: render_to_string(template_name, context=None, request=None, using=None)
示例:

# myapp/views.py
from django.template.loader import render_to_string # 导入render_to_string
from django.http import JsonResponse, HttpRequest # 导入JsonResponse和HttpRequest
# from django.core.mail import send_mail # 假设用于发送邮件

def get_item_details_ajax(request: HttpRequest) -> JsonResponse: # 定义获取项目详情的AJAX视图
    item_id = request.GET.get('item_id') # 从GET请求中获取item_id
    # 模拟获取项目数据
    item_data = None # 初始化项目数据为None
    if item_id == '1': # 如果item_id为'1'
        item_data = {
                'name': 'Super Gadget', 'price': 49.99, 'description': 'An amazing gadget for all your needs.'} # 项目数据1
    elif item_id == '2': # 如果item_id为'2'
        item_data = {
                'name': ' सहायक उपकरण', 'price': 19.50, 'description': 'एक उपयोगी सहायक उपकरण।'} # 项目数据2 (包含Unicode字符)
    
    if item_data: # 如果找到了项目数据
        # 渲染一个HTML片段模板
        html_fragment = render_to_string( # 使用render_to_string渲染HTML片段
            'myapp/ajax_item_detail_fragment.html', # 片段模板路径
            {
                'item': item_data}, # 传递给片段模板的上下文
            request=request # 传递request以确保上下文处理器运行 (如果片段需要的话)
        ) # HTML片段渲染结束
        return JsonResponse({
                'success': True, 'html': html_fragment}) # 返回JSON响应,包含成功状态和HTML片段
    else: # 如果未找到项目数据
        return JsonResponse({
                'success': False, 'error': 'Item not found.'}, status=404) # 返回JSON响应,包含失败状态和错误信息

# def send_welcome_email(user_email, username):
#     email_subject = "Welcome to Our Platform!"
#     email_context = {'username': username, 'login_url': '/login/'}
#     email_body_html = render_to_string('emails/welcome_email.html', email_context)
#     email_body_text = render_to_string('emails/welcome_email.txt', email_context)
#
#     send_mail(
#         email_subject,
#         email_body_text, # 纯文本内容
#         'noreply@example.com',
#         [user_email],
#         html_message=email_body_html # HTML内容
#     )
#     print(f"Welcome email sent to {user_email}")

对应的HTML片段模板 (myapp/templates/myapp/ajax_item_detail_fragment.html):

{# myapp/templates/myapp/ajax_item_detail_fragment.html #}
{% if item %} {# 检查item是否存在 #}
    <h3>{
               { item.name|escape }}</h3> {# 显示转义后的项目名称 #}
    <p><strong>Price:</strong> ${
               { item.price|floatformat:2 }}</p> {# 显示格式化为两位小数的价格 #}
    <p><strong>Description:</strong> {
               { item.description|linebreaksbr }}</p> {# 显示描述,并将换行符转为<br> #}
{% else %}
    <p class="error">Item details could not be loaded.</p> {# 如果item不存在则显示错误 #}
{% endif %}
4.2.3 模板组织与命名空间

项目级模板: 存放在 TEMPLATES 设置中 DIRS 指定的目录(例如 project_root/templates/)。通常用于存放整个项目的通用模板,如基础布局 (base.html)、错误页面 (404.html, 500.html) 等。
应用级模板: 存放在各个 Django 应用内部的 templates 子目录中(例如 myapp/templates/)。当 APP_DIRSTrue 时,Django 会自动查找这些目录。

命名空间化: 为了避免不同应用之间模板文件的名称冲突,强烈建议在应用的 templates 目录下再创建一个与应用同名的子目录,并将该应用的模板放在这个子目录中。

例如,对于名为 blog 的应用,其文章列表模板应存放在 blog/templates/blog/post_list.html
在视图中加载时,使用 'blog/post_list.html'
Django 的模板加载器在查找 blog/post_list.html 时,会检查 DIRS 中的每个目录以及每个启用了 APP_DIRS 的应用的 templates 目录,直到找到匹配的路径。
加载顺序: Django 会按照 TEMPLATES 设置中加载器的顺序(以及 DIRS 中路径的顺序)来查找模板。第一个找到的匹配模板将被使用。这意味着项目级的 templates 目录中的模板(如果与应用级模板同名且路径也相同)通常会优先于应用级的模板。这可以用来覆盖特定应用的默认模板。

示例目录结构:

my_django_project/
├── manage.py
├── my_django_project/  # 项目配置目录 (settings.py, urls.py, ...)
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── templates/          # 项目级模板目录 (在 settings.py 的 DIRS 中指定)
│   ├── base.html
│   ├── 404.html
│   └── 500.html
├── blog/               # 'blog' 应用
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations/
│   ├── models.py
│   ├── templates/      # 'blog' 应用的模板根目录
│   │   └── blog/       # 命名空间子目录,与应用名相同
│   │       ├── post_list.html
│   │       ├── post_detail.html
│   │       └── includes/
│   │           └── _post_summary_card.html
│   ├── tests.py
│   ├── urls.py         # blog 应用的 URL 配置
│   └── views.py
├── users/              # 'users' 应用
│   ├── __init__.py
│   ├── ...
│   └── templates/
│       └── users/      # 命名空间子目录
│           ├── login.html
│           └── profile.html
└── static/             # 项目级静态文件目录 (通常用于存放共用静态资源)
    └── css/
        └── global_styles.css

这种结构清晰地分离了项目级模板和应用级模板,并通过命名空间避免了冲突,是 Django 项目开发的标准实践。

4.3 DTL 核心语法元素详解

Django 模板语言 (DTL) 的语法主要由三部分构成:变量 (Variables)标签 (Tags)过滤器 (Filters)。这些元素共同协作,使得开发者能够将动态数据有效地呈现在模板中。

4.3.1 变量 (Variables) 在 DTL 中的深度解析

在 DTL 中,变量被双花括号包围:{
{ variable_name }}
。当模板引擎遇到一个变量时,它会从当前的上下文 (context) 中查找该变量,并将其值替换到模板的相应位置。

4.3.1.1 变量的查找与解析 (Dot Lookup)

DTL 使用“点号查找 (dot lookup)”机制来访问变量的属性或字典的键。当模板引擎遇到类似 {
{ person.name }}
的变量时,它会按以下顺序尝试解析:

字典键查找 (Dictionary Key Lookup):

首先,引擎会检查 person 是否是一个字典,并且是否有键 'name'
例如,如果上下文中 person = {'name': 'Alice', 'age': 30},则 {
{ person.name }}
会输出 “Alice”。

属性查找 (Attribute Lookup):

如果字典键查找失败(或者 person 不是字典),引擎会检查 person 是否有一个名为 name 的属性。
例如,如果 person 是一个对象实例 class Person: def __init__(self, name): self.name = name,且 person_obj = Person("Bob"),则 {
{ person_obj.name }}
会输出 “Bob”。
这包括通过 @property 装饰器定义的属性。

方法调用 (Method Call – 无参数):

如果属性查找也失败,引擎会检查 person 是否有一个名为 name无参数可调用方法。如果存在,引擎会调用该方法并使用其返回值。
重要限制: DTL 不允许在模板中直接向方法传递参数,例如 {
{ person.get_name('formal') }}
是无效的。如果你需要调用带参数的方法,或者方法有副作用,你应该在视图 (Python 代码) 中调用它,并将结果传递给模板上下文。或者,可以创建自定义模板标签或过滤器来实现此功能。
例如,如果 class Person: def name(self): return "Charlie",则 {
{ person_obj.name }}
会调用 name() 方法并输出 “Charlie”。

列表索引查找 (List-Index Lookup):

如果前面的查找都失败,并且变量名 name 是一个有效的整数,引擎会尝试将 person 视为一个列表,并使用 name 作为索引进行访问。
例如,如果 items = ['apple', 'banana', 'cherry'],则 {
{ items.0 }}
会输出 “apple”,{
{ items.1 }}
会输出 “banana”。
注意:是 items.0 而不是 items[0]。方括号在 DTL 变量访问中通常不直接使用,而是通过点号加数字索引。

查找顺序的意义: 这个查找顺序是固定的。例如,如果一个对象同时有一个名为 foo 的属性和一个名为 foo 的无参数方法,属性会优先被访问。

示例:DTL 变量解析顺序

Python 代码 (例如 myapp/views.py):

# myapp/views.py
from django.shortcuts import render # 导入render快捷函数
from django.http import HttpRequest # 导入HttpRequest

class DemoClass: # 定义一个演示类
    def __init__(self): # 初始化方法
        self.attr_name = "Attribute Value" # 类属性
        self.dict_attr = {
            'key_in_dict_attr': "Value from dict_attr's key"} # 类属性,其值为一个字典

    def method_name(self): # 类方法 (无参数)
        return "Method Call Result" # 返回方法调用的结果

    def method_with_collision(self): # 与属性同名的方法
        return "Result from method_with_collision" # 返回方法调用的结果

# 模拟一个与属性和方法都冲突的键名
demo_object_with_key_collision = DemoClass() # 创建DemoClass实例
# 添加一个与方法同名的属性,这个属性会覆盖方法(在DTL点号查找的属性阶段)
# 但如果属性不存在,方法会被调用。
# demo_object_with_key_collision.method_name = "This is now an attribute, not the method's result"

# 模拟一个与属性同名的方法,但属性优先
demo_object_attr_method_collision = DemoClass() # 创建DemoClass实例
demo_object_attr_method_collision.method_with_collision_attr = "Attribute value for method_with_collision_attr" # 定义一个属性
# 假设DemoClass也有一个名为 method_with_collision_attr 的方法,但属性查找优先

def dtl_variable_lookup_view(request: HttpRequest): # 定义DTL变量查找视图函数
    """演示DTL变量查找顺序的视图。"""
    demo_obj = DemoClass() # 创建DemoClass实例

    context = {
             # 定义模板上下文
        'my_string': "Hello DTL!", # 简单字符串
        'my_number': 123, # 数字
        'my_list': ['first_item', 'second_item', 'third_item'], # 列表
        'my_dict': {
             # 字典
            'name': 'Dictionary Name', # 字典键'name'
            'value': 'Dictionary Value', # 字典键'value'
            'nested_dict': {
            'inner_key': 'Inner Value'}, # 嵌套字典
            'clash_key': 'Value from dict.clash_key' # 与对象属性/方法可能冲突的键
        },
        'demo_instance': demo_obj, # DemoClass的实例
        'another_instance_for_method_test': DemoClass(), # 另一个DemoClass实例用于测试方法调用
        'data_for_method_attr_clash': {
             # 用于测试字典键与对象属性/方法冲突的数据
            'method_name': "Value from data_for_method_attr_clash.method_name (dict key)", # 字典键与方法名冲突
            'attr_name': "Value from data_for_method_attr_clash.attr_name (dict key)" # 字典键与属性名冲突
        },
        # 演示属性优先于方法
        'obj_attr_over_method': type('AttrMethodClash', (object,), {
             # 动态创建一个类
            'my_prop': 'This is an attribute', # 类属性
            'my_prop': lambda self: 'This is a method result' # 同名方法 (但属性会优先)
            # DTL 属性查找优先,所以上面的 lambda 不会被作为方法调用
            # 为了清晰,我们让属性和方法名不同,或者用更明确的方式
        })(),
        'obj_method_if_no_attr': type('MethodNoAttrClash', (object,), {
             # 动态创建另一个类
            # no 'my_callable_prop' attribute
            'my_callable_prop': lambda self: 'Method result (no attr clash)' # 只有方法
        })(),
        'complex_object': {
             # 复杂对象,包含多种类型
            'name': 'Complex Object Top Level', # 顶层名称
            'details': demo_obj, # 嵌套DemoClass实例
            'tags': ['tag1', 'tag2'], # 标签列表
            'config': {
             # 配置字典
                'enabled': True, # 布尔值
                'retry_count': 3 # 数字
            }
        },
        'none_value': None, # None值
        'empty_string': "", # 空字符串
        'zero_value': 0, # 零值
        'false_value': False # False值
    } # 上下文定义结束
    return render(request, 'myapp/variable_lookup_example.html', context) # 渲染模板

对应的模板文件 (myapp/templates/myapp/variable_lookup_example.html):

{# myapp/templates/myapp/variable_lookup_example.html #}
{% extends "base_generic.html" %} {# 继承基础模板 #}

{% block title %}DTL Variable Lookup Test{% endblock %} {# 定义标题块 #}

{% block content %} {# 定义内容块 #}
    <h1>DTL Variable Lookup Mechanisms</h1>

    <h2>Basic Types:</h2>
    <p>String: <strong>{
           { my_string }}</strong></p> {# 输出字符串变量 #}
    <p>Number: <strong>{
           { my_number }}</strong></p> {# 输出数字变量 #}
    <p>None Value (outputs nothing by default): [{
           { none_value }}]</p> {# 输出None值,默认为空 #}
    <p>Empty String: [{
           { empty_string }}]</p> {# 输出空字符串 #}
    <p>Zero Value: [{
           { zero_value }}]</p> {# 输出零值 #}
    <p>False Value: [{
           { false_value }}]</p> {# 输出False值 #}


    <h2>List Access:</h2>
    <p>First item in list: <strong>{
           { my_list.0 }}</strong></p> {# 访问列表的第一个元素 #}
    <p>Second item in list: <strong>{
           { my_list.1 }}</strong></p> {# 访问列表的第二个元素 #}
    <p>Accessing non-existent high index (e.g., my_list.5) - (outputs nothing or configured invalid string): [{
           { my_list.5 }}]</p> {# 访问不存在的列表索引 #}
    <p>Accessing non-integer index (e.g., my_list.foo) - (outputs nothing or configured invalid string): [{
           { my_list.foo }}]</p> {# 使用非整数访问列表索引 #}

    <h2>Dictionary Access:</h2>
    <p>Dict 'name' (key lookup): <strong>{
           { my_dict.name }}</strong></p> {# 通过键名访问字典值 #}
    <p>Dict 'value' (key lookup): <strong>{
           { my_dict.value }}</strong></p> {# 通过键名访问字典值 #}
    <p>Dict 'nested_dict.inner_key': <strong>{
           { my_dict.nested_dict.inner_key }}</strong></p> {# 访问嵌套字典的值 #}
    <p>Dict missing key 'city' (outputs nothing or configured invalid string): [{
           { my_dict.city }}]</p> {# 访问字典中不存在的键 #}

    <h2>Object Attribute and Method Access (demo_instance):</h2>
    <p>Object 'attr_name' (attribute lookup): <strong>{
           { demo_instance.attr_name }}</strong></p> {# 访问对象属性 #}
    <p>Object 'method_name' (no-arg method call): <strong>{
           { demo_instance.method_name }}</strong></p> {# 调用对象的无参数方法 #}
    <p>Object 'dict_attr.key_in_dict_attr' (attr is dict, then key lookup): <strong>{
           { demo_instance.dict_attr.key_in_dict_attr }}</strong></p> {# 先访问对象属性(该属性是字典),再访问字典的键 #}
    <p>Object non-existent attribute 'foo' (outputs nothing or configured invalid string): [{
           { demo_instance.foo }}]</p> {# 访问对象不存在的属性 #}

    <h2>Demonstrating Lookup Order (Dictionary Key vs. Object Attr/Method):</h2>
    <p>Context has <code>data_for_method_attr_clash.method_name</code> AND <code>another_instance_for_method_test.method_name()</code>.</p>
    <p>Accessing <code>another_instance_for_method_test.method_name</code>: <strong>{
           { another_instance_for_method_test.method_name }}</strong> (Should be method result from the object)</p> {# 调用另一个实例的方法 #}
    <p>Accessing <code>data_for_method_attr_clash.method_name</code>: <strong>{
           { data_for_method_attr_clash.method_name }}</strong> (Should be value from the dictionary key)</p> {# 访问字典的键 #}
    <p>Accessing <code>data_for_method_attr_clash.attr_name</code>: <strong>{
           { data_for_method_attr_clash.attr_name }}</strong> (Should be value from the dictionary key)</p> {# 访问字典的键 #}

    <hr>
    <p>Scenario: <code>my_dict</code> (a dict) vs <code>demo_instance</code> (an object). Both might have a 'clash_key' or 'clash_key()'.</p>
    <p>If <code>my_dict</code> is primary: <code>my_dict.clash_key</code> gives <strong>{
           { my_dict.clash_key }}</strong> (dict key lookup)</p> {# 访问字典的clash_key #}
    <p>If <code>demo_instance</code> is primary: <code>demo_instance.clash_key</code> (assuming it has such attr/method): [{
           { demo_instance.clash_key }}] (Will try attr, then method on demo_instance)</p> {# 尝试访问demo_instance的clash_key #}

    <h2>Attribute vs. Method (Same Name on Object):</h2>
    {# For this to work as intended, obj_attr_over_method needs 'my_prop' as attr and 'my_prop' as method #}
    {# Python attributes shadow methods of the same name. DTL's attr lookup comes before method lookup. #}
    {# Let's assume obj_attr_over_method.my_prop is an attribute #}
    <p>Object with attr 'my_prop' and method 'my_prop()': <code>obj_attr_over_method.my_prop</code> gives <strong>{
           { obj_attr_over_method.my_prop }}</strong> (Attribute should win)</p> {# 属性优先于同名方法 #}
    <p>Object with only method 'my_callable_prop()': <code>obj_method_if_no_attr.my_callable_prop</code> gives <strong>{
           { obj_method_if_no_attr.my_callable_prop }}</strong> (Method should be called)</p> {# 调用只有方法没有同名属性的对象的方法 #}

    <h2>Complex Object Traversal:</h2>
    <p>Complex Object Name: <strong>{
           { complex_object.name }}</strong></p> {# 访问复杂对象的顶层名称 #}
    <p>Complex Object's Details (DemoInstance) attr_name: <strong>{
           { complex_object.details.attr_name }}</strong></p> {# 访问嵌套对象的属性 #}
    <p>Complex Object's Details (DemoInstance) method_name: <strong>{
           { complex_object.details.method_name }}</strong></p> {# 调用嵌套对象的方法 #}
    <p>Complex Object's First Tag: <strong>{
           { complex_object.tags.0 }}</strong></p> {# 访问嵌套列表的第一个元素 #}
    <p>Complex Object's Config Enabled: <strong>{
           { complex_object.config.enabled }}</strong></p> {# 访问嵌套字典的值 #}
    <p>Complex Object's Config Retry Count: <strong>{
           { complex_object.config.retry_count }}</strong></p> {# 访问嵌套字典的值 #}

    <h2>Handling of Invalid Variables:</h2>
    {% if settings.DEBUG %} {# 检查settings.DEBUG,通常由debug上下文处理器提供 #}
        <p>In DEBUG mode, invalid variables might show more info or raise errors if not handled.</p>
    {% else %}
        <p>In non-DEBUG mode, invalid variables usually result in empty output or <code>string_if_invalid</code>.</p>
        <p><code>string_if_invalid</code> is '{
           { settings.TEMPLATES.0.OPTIONS.string_if_invalid|default:"(Not Set in settings for this demo)" }}'</p> {# 显示settings中string_if_invalid的配置 #}
    {% endif %}
    <p>Trying to access <code>non_existent_top_level_var</code>: [{
           { non_existent_top_level_var }}]</p> {# 访问顶层不存在的变量 #}
    <p>Trying to access <code>my_dict.non_existent_key</code>: [{
           { my_dict.non_existent_key }}]</p> {# 访问字典中不存在的键 #}
    <p>Trying to access <code>demo_instance.non_existent_attribute</code>: [{
           { demo_instance.non_existent_attribute }}]</p> {# 访问对象不存在的属性 #}

{% endblock %}

代码解释 (DTL 模板部分):

{
{ my_list.0 }}
: 演示了列表索引访问。
{
{ my_dict.name }}
: 演示了字典键访问。
{
{ demo_instance.attr_name }}
: 演示了对象属性访问。
{
{ demo_instance.method_name }}
: 演示了对象无参数方法的调用。
冲突和顺序:

当一个变量名(例如 method_name)同时存在于一个字典的键中(如 data_for_method_attr_clash.method_name)和另一个对象的无参数方法中(如 another_instance_for_method_test.method_name()),DTL 会根据你访问的“根”对象来决定使用哪个。如果你写 {
{ data_for_method_attr_clash.method_name }}
,它会执行字典查找。如果你写 {
{ another_instance_for_method_test.method_name }}
,它会先尝试属性查找,然后尝试方法调用。
如果一个对象 obj 同时有一个属性 x 和一个方法 x(),那么 {
{ obj.x }}
总是优先解析为属性 x 的值。只有当属性 x 不存在时,DTL 才会尝试调用方法 x()

{
{ complex_object.details.attr_name }}
: 演示了链式点号查找,可以深入嵌套的对象和字典。
处理无效/未定义变量:

当 DTL 尝试访问一个不存在的键、属性、索引,或者方法调用失败时,默认情况下,它不会抛出模板渲染错误(除非在非常特殊的情况下,如调用一个不存在的方法且该名称也不是属性)。相反,它通常会输出一个空字符串。
这种行为可以通过 settings.pyTEMPLATES 配置的 OPTIONS.string_if_invalid 来修改。例如,设置 string_if_invalid = 'DEBUG: undefined variable %s',那么当变量 %s 未定义时,模板会输出这个字符串。
在开发模式下 (settings.DEBUG = True),如果 string_if_invalid 未设置或设置为空字符串,对于某些类型的查找失败(尤其是属性/方法查找),Django 可能会在调试信息中提示,但页面通常仍会渲染。如果 string_if_invalid 被设置为非空字符串,则会显示该字符串。
StrictUndefined (像 Jinja2 中的) 在 DTL 中没有直接的等价物作为标准配置。DTL 的哲学更倾向于“静默失败”或通过 string_if_invalid 提供占位符,以避免因小数据问题导致整个页面崩溃。

4.3.1.2 变量的显示与自动转义

默认情况下,DTL 对所有通过 {
{ ... }}
输出的变量进行 HTML 转义。这意味着以下字符会被替换:

< 替换为 &lt;
> 替换为 &gt;
' (单引号) 替换为 '
" (双引号) 替换为 &quot;
& 替换为 &amp;

这是防止跨站脚本 (XSS) 攻击的一项重要安全措施。

示例:自动转义

# myapp/views.py
from django.shortcuts import render # 导入render快捷函数
from django.http import HttpRequest # 导入HttpRequest

def autoescape_demo_view(request: HttpRequest): # 定义自动转义演示视图函数
    context = {
             # 定义模板上下文
        'html_string': "<script>alert('This is an XSS attempt!');</script>", # 包含HTML脚本的字符串
        'normal_string': "This string has < and > symbols.", # 包含HTML特殊字符的普通字符串
        'user_input_title': "User's Title: "Special Chars & More"" # 用户输入的标题,包含引号和&符号
    } # 上下文定义结束
    return render(request, 'myapp/autoescape_example.html', context) # 渲染模板

对应的模板文件 (myapp/templates/myapp/autoescape_example.html):

{# myapp/templates/myapp/autoescape_example.html #}
<h1>DTL Autoescape Demonstration</h1>

<h2>Case 1: Potentially Harmful HTML</h2>
<p>Raw variable value (conceptual, not directly outputtable without turning off autoescape): <code>{
           { html_string_raw_placeholder }}</code></p> {# 这是一个占位符,实际不应这样输出 #}
<p>Rendered with default autoescape:</p>
<div>{
           { html_string }}</div> {# html_string 将被自动转义 #}

<h2>Case 2: String with HTML Special Characters</h2>
<p>Rendered with default autoescape:</p>
<div>{
           { normal_string }}</div> {# normal_string 将被自动转义 #}

<h2>Case 3: User Input with Quotes and Ampersand</h2>
<p>Rendered as title attribute (autoescape helps here too):</p>
<h3 title="{
           { user_input_title }}">Hover over this heading</h3> {# user_input_title 在HTML属性中,也应被转义 #}
<p>Rendered as content:</p>
<div>{
           { user_input_title }}</div> {# user_input_title 作为内容输出,也会被转义 #}

预期输出 (浏览器查看源代码会更清晰):

对于 {
{ html_string }}
:
浏览器显示: &lt;script&gt;alert('This is an XSS attempt!');&lt;/script&gt;
(脚本不会执行)
对于 {
{ normal_string }}
:
浏览器显示: This string has &lt; and &gt; symbols.
对于 h3 标签的 title 属性和 {
{ user_input_title }}
内容:
title 属性会是 User's Title: &quot;Special Chars &amp; More&quot;
内容会是 User's Title: &quot;Special Chars &amp; More&quot;

关闭自动转义:
在某些情况下,你可能确实需要输出一个已知是安全的 HTML 字符串,而不希望 DTL 对其进行转义。有几种方法:

|safe 过滤器:

|safe 过滤器应用于变量,可以将其标记为“安全”,从而阻止自动转义。
务必确保你传递给 |safe 的内容来源可靠且确实是安全的,否则会引入 XSS 漏洞。
{
{ trusted_html_variable|safe }}

{% autoescape off %}{% endautoescape %}:

可以将一段模板代码包裹在 {% autoescape off %}{% endautoescape %} 标签之间,这会暂时关闭该代码块内的自动转义。
同样需要极度谨慎使用。

{% autoescape off %}
    {
               { variable_containing_raw_html }} {# 在这个块内,此变量不会被转义 #}
{% endautoescape %}

{
               { variable_containing_raw_html }} {# 在块外,此变量仍会被默认转义 #}

使用 mark_safe() 在 Python 端:

在 Python 代码中,你可以使用 django.utils.safestring.mark_safe() 函数将一个字符串标记为安全。当这个被标记的字符串传递到模板中并被渲染时,DTL 不会对其进行自动转义。
这通常用于自定义模板标签或视图逻辑中,当你能从程序上保证某段 HTML 是安全的。

# myapp/views.py
from django.utils.safestring import mark_safe # 导入mark_safe
from django.shortcuts import render # 导入render
from django.http import HttpRequest # 导入HttpRequest

def safe_content_view(request: HttpRequest): # 定义安全内容视图函数
    # 假设这是从一个受信任的Markdown转换器或富文本编辑器得到的HTML
    generated_safe_html = "<p>This is <strong>bold</strong> and <em>italic</em> text, generated programmatically and marked safe.</p>" # 已知的安全HTML
    
    # 标记为安全
    safe_html_object = mark_safe(generated_safe_html) # 使用mark_safe标记字符串为安全
    
    unsafe_html_for_comparison = "<button onclick='alert("unsafe!")'>Click Me (Unsafe)</button>" # 不安全的HTML,用于对比

    context = {
                 # 定义模板上下文
        'trusted_html': safe_html_object, # 已标记为安全的HTML对象
        'untrusted_html': unsafe_html_for_comparison # 未标记的HTML字符串
    } # 上下文定义结束
    return render(request, 'myapp/safe_example.html', context) # 渲染模板

对应的模板 (myapp/templates/myapp/safe_example.html):

{# myapp/templates/myapp/safe_example.html #}
<h2>Safe Content Demonstration</h2>

<h3>Using <code>mark_safe()</code> in Python:</h3>
<div>{
               { trusted_html }}</div> {# trusted_html 是 SafeString 对象,不会被转义 #}

<h3>Using <code>|safe</code> filter:</h3>
<div>{
               { untrusted_html|safe }}</div> {# untrusted_html 被显式标记为安全 (!!! 仅当你知道它是安全的才这样做 !!!) #}

<h3>Default autoescape for comparison:</h3>
<div>{
               { untrusted_html }}</div> {# untrusted_html 会被默认转义 #}

<h3>Using <code>{% autoescape off %}</code> block:</h3>
{% autoescape off %} {# 关闭自动转义块开始 #}
    <div>{
               { untrusted_html }}</div> {# 在此块内,untrusted_html 不会被转义 #}
{% endautoescape %} {# 关闭自动转义块结束 #}

何时使用 mark_safe vs |safe:

如果 HTML 是在 Python 代码中生成并能确保其安全性(例如,来自一个可靠的HTML生成库的输出,或者你仔细构造了它),则在 Python 端使用 mark_safe() 更好,因为它明确了数据在进入模板之前就是安全的。
如果一个变量通常应该被转义,但你知道在特定模板的特定位置,它的当前值是安全的,可以在模板中使用 |safe。这通常用于处理来自数据库的、已知是经过清理和审查的富文本内容。

安全是首要考虑: 在处理用户生成的内容或任何可能包含不可信数据的变量时,应始终依赖 DTL 的默认自动转义。只有在你绝对确定一段 HTML 是安全的,并且需要将其作为原始 HTML 输出时,才应考虑使用 |safe{% autoescape off %}。错误地关闭转义是导致 XSS 漏洞的常见原因。

4.3.1.3 变量中无效字符的处理

如果变量名包含无效字符(例如空格、特殊符号,或者以数字开头且不是有效的列表索引),DTL 通常无法正确解析它。

{
{ my variable }}
(带空格) – 无效
{
{ data-value }}
(带连字符) – 无效 (DTL 会尝试查找 data 对象,然后查找名为 value 的属性/键,但不会将 data-value 视为一个整体变量名)。
{
{ 1st_item }}
(以数字开头) – 无效

在这种情况下,你需要确保传递给模板的上下文中的键/属性名是有效的 Python 标识符(或有效的字典键字符串,对于字典访问)。如果你的数据源的键名不符合要求,应在视图中进行预处理,将其转换为 DTL 可接受的格式。

4.3.1.4 变量作用域

DTL 中的变量作用域相对简单:

顶层上下文: 视图传递给 render() 函数的 context 字典中的所有键都成为模板的顶层可用变量。
{% for %} 循环: 循环变量(例如 {% for item in my_list %} 中的 item)只在 for 循环内部可用。
{% with %} 标签: {% with new_var=old_var.some_attr %} 会创建一个名为 new_var 的局部变量,它只在 {% with %}{% endwith %} 之间可用。这对于给复杂的表达式起一个简短的别名,或者避免重复访问深层嵌套的属性很有用。

{% with author_name=article.author.profile.get_full_name author_email=article.author.email %} {# 使用with标签定义局部变量 #}
    <p>Article by: {
             { author_name }}</p> {# 使用局部变量author_name #}
    <p>Contact: <a href="mailto:{
             { author_email }}">{
             { author_email }}</a></p> {# 使用局部变量author_email #}
{% endwith %} {# with标签结束 #}
{# author_name 和 author_email 在这里不再可用 #}

{% include %} 标签:

默认情况下,被包含的模板 ({% include "snippet.html" %}) 会继承调用它的模板的完整上下文。
你可以使用 with 子句来传递特定的、隔离的上下文给被包含的模板:
{% include "snippet.html" with person=user1 only %}

with person=user1: snippet.html 内部只能访问到 person 变量(其值为 user1),以及通过上下文处理器添加的全局变量。
only: 关键字 only 进一步限制了上下文,使得 snippet.html 能访问 with 子句中明确传递的变量,而不会继承父模板的其他上下文变量(但上下文处理器添加的变量通常仍然可用,除非模板引擎配置非常特殊)。

模板继承 ({% extends %}): 子模板可以访问父模板在渲染时所拥有的所有上下文变量。块 ({% block %}) 内部可以像模板的其他部分一样访问这些变量。

DTL 的变量系统设计得既能满足常见的表现层需求,又通过点号查找提供了对复杂数据结构的合理访问。其默认的自动转义机制是保障Web安全的重要一环。理解其查找规则、作用域以及如何安全地处理 HTML 是有效使用 DTL 的基础。

4.3.2 标签 (Tags) 在 DTL 中的深度探索

Django 模板标签是被 {%%} 包围的指令,它们控制模板的渲染逻辑、执行特定操作或引入外部内容。与主要用于输出值的变量不同,标签可以执行更复杂的操作,如创建循环、执行条件判断、加载其他模板、设置变量、加载外部资源等。

DTL 拥有一个丰富的内置标签集,并且允许开发者创建自定义标签来扩展其功能。

4.3.2.1 常用内置标签详解与高级应用

我们将逐一深入探讨 DTL 中最常用和最重要的内置标签,分析其参数、行为、内部机制以及在真实企业级场景中的应用和最佳实践。

A. {% for ... in ... %}{% empty %}

核心功能: 迭代一个序列(列表、元组、查询集等),并在每次迭代中渲染其间的代码块。

语法:

{% for variable in iterable [reversed] %} {# for标签开始,可以迭代iterable,reversed是可选的,用于反向迭代 #}
    ... (循环体,可以使用 'variable') ...
{% empty %} {# 可选的empty标签,当iterable为空或不存在时执行 #}
    ... (当 'iterable' 为空或不存在时显示的内容) ...
{% endfor %} {# for标签结束 #}

reversed: 可选关键字,如果提供,则会反向迭代 iterable

{% empty %}: 可选的子标签。如果 iterable 为空、不存在,或者解析为一个布尔值为 False 的对象 (例如 None 或一个空的自定义集合对象),则 {% empty %}{% endfor %} 之间的内容会被渲染。

forloop 特殊变量 (DTL 的 loop 等价物):
{% for %} 循环内部,DTL 自动提供一个名为 forloop 的特殊模板变量,它包含了当前循环状态的信息:

forloop.counter: 当前迭代的次数(从 1 开始)。
forloop.counter0: 当前迭代的次数(从 0 开始)。
forloop.revcounter: 从循环末尾开始的剩余迭代次数(从 1 开始)。当是最后一次迭代时为 1。
forloop.revcounter0: 从循环末尾开始的剩余迭代次数(从 0 开始)。当是最后一次迭代时为 0。
forloop.first: 布尔值,如果当前是第一次迭代,则为 True
forloop.last: 布尔值,如果当前是最后一次迭代,则为 True
forloop.parentloop: 在嵌套循环中,这个变量指向外层循环的 forloop 对象。这允许你访问外层循环的状态。

内部机制:

当 DTL 引擎遇到 {% for %} 标签时,它会尝试迭代传递给它的 iterable 对象。
对于 Django 的查询集 (QuerySets),DTL 的 for 标签非常高效。它不会立即将整个查询集加载到内存中,而是按需从数据库中获取数据。这意味着即使处理包含数千条记录的查询集,内存开销也相对较低。然而,如果在循环内部对每个对象执行了额外的数据库查询(例如,访问未通过 select_relatedprefetch_related 优化的关联对象),则可能导致 “N+1 查询问题”,严重影响性能。
reversed 关键字在应用于查询集时,会尝试使用数据库的反向排序(如果可能),或者在获取结果后在 Python 层面反转。

企业级应用场景与最佳实践:

渲染列表数据 (如文章、产品、用户):

# views.py
from django.shortcuts import render # 导入render快捷函数
from django.http import HttpRequest # 导入HttpRequest
# from .models import Product # 假设有一个Product模型

def product_list_view(request: HttpRequest): # 定义产品列表视图函数
    # products = Product.objects.filter(is_active=True).order_by('-popularity')[:20] # 获取激活且按流行度排序的前20个产品
    # 模拟产品数据
    products = [ # 产品数据列表
        {
                'name': 'Awesome Gadget', 'price': 99.99, 'stock': 15, 'category': 'Electronics'}, # 产品1
        {
                'name': 'Comfortable Chair', 'price': 149.00, 'stock': 5, 'category': 'Furniture'}, # 产品2
        {
                'name': 'Python Programming Book', 'price': 29.95, 'stock': 0, 'category': 'Books'}, # 产品3
        {
                'name': 'Coffee Maker', 'price': 75.50, 'stock': 30, 'category': 'Appliances'} # 产品4
    ] # 产品数据列表定义结束
    context = {
                'product_list': products} # 定义模板上下文
    return render(request, 'store/product_catalog.html', context) # 渲染模板
{# store/templates/store/product_catalog.html #}
{% if product_list %} {# 检查产品列表是否存在 #}
    <div class="product-grid">
        {% for product in product_list %} {# 遍历产品列表 #}
            <div class="product-card {% if forloop.counter0|divisibleby:2 %}even-row{% else %}odd-row{% endif %} {% if product.stock == 0 %}out-of-stock{% endif %}"> {# 根据循环索引和库存状态添加CSS类 #}
                <h3>{
               { product.name|truncatewords:5 }} ({
               { product.category }})</h3> {# 显示产品名称(截断到5个词)和类别 #}
                <p>Price: ${
               { product.price|floatformat:2 }}</p> {# 显示格式化后的价格 #}
                
                {% if product.stock > 0 and product.stock < 10 %} {# 如果库存在0和10之间 #}
                    <p class="low-stock-warning">Low stock! Only {
               { product.stock }} left.</p> {# 显示低库存警告 #}
                {% elif product.stock == 0 %} {# 如果库存为0 #}
                    <p class="unavailable">Currently unavailable.</p> {# 显示不可用信息 #}
                {% else %} {# 其他库存情况 #}
                    <p>In stock: {
               { product.stock }} units.</p> {# 显示库存数量 #}
                {% endif %}

                {% if forloop.first %} {# 如果是列表中的第一个产品 #}
                    <span class="badge new-arrival-badge">New Arrival!</span> {# 显示新到货标记 #}
                {% endif %}
                <a href="{% url 'store:product_detail' product.id %}">View Details</a> {# 产品详情链接 #}
            </div>
            {% if not forloop.last %} {# 如果不是最后一个产品 #}
                {# <hr class="product-separator"> #} {# 可以添加分隔线,但通常用CSS控制间距 #}
            {% endif %}
        {% endfor %}
    </div>
{% else %} {# 如果产品列表为空 #}
    <p>No products found matching your criteria. Please try a different search or category.</p> {# 显示无产品提示 #}
{% endif %}

代码解释与实践:

{% if forloop.counter0|divisibleby:2 %}even-row{% else %}odd-row{% endif %}: 使用 forloop.counter0 (0-based index) 和 divisibleby 过滤器来为表格行或列表项交替应用 CSS 类,实现斑马条纹效果。
{% if product.stock == 0 %}out-of-stock{% endif %}: 根据产品库存动态添加 CSS 类,用于视觉提示。
{
{ product.name|truncatewords:5 }}
: 使用 truncatewords 过滤器确保产品名称不会过长而破坏布局。
{% if forloop.first %}: 为第一个项目添加特殊标记。
性能: 如果 product_list 是一个 QuerySet,确保在视图中通过 select_related (对于一对一或外键) 和 prefetch_related (对于多对多或反向外键) 预先加载相关的对象数据,以避免在循环中为每个 product 触发额外的数据库查询。例如,如果 Product 有一个 category 外键,并且模板中显示 product.category.name,那么在视图中应该用 Product.objects.select_related('category')...

处理嵌套数据结构 (使用 forloop.parentloop):
假设我们需要显示一个按类别分组的产品列表。

# views.py
from django.shortcuts import render # 导入render快捷函数
from django.http import HttpRequest # 导入HttpRequest

def categorized_products_view(request: HttpRequest): # 定义按类别分组的产品视图函数
    # 模拟数据,实际中这可能来自数据库查询和Python端的预处理
    categorized_data = [ # 按类别分组的数据列表
        {
                
            'category_name': 'Electronics', # 类别1名称
            'products': [ # 类别1下的产品列表
                {
                'id': 101, 'name': 'Laptop Pro X', 'price': 1200.00}, # 产品1
                {
                'id': 102, 'name': 'Wireless Mouse', 'price': 25.00}, # 产品2
            ],
            'category_promo': '10% off all electronics this week!' # 类别1的促销信息
        },
        {
                
            'category_name': 'Books', # 类别2名称
            'products': [ # 类别2下的产品列表
                {
                'id': 201, 'name': 'The Art of DTL', 'price': 35.50}, # 产品1
                {
                'id': 202, 'name': 'Python Cookbook', 'price': 42.75}, # 产品2
            ],
            'category_promo': None # 类别2无促销信息
        },
        {
                
            'category_name': 'Apparel', # 类别3名称
            'products': [], # 类别3下无产品 (用于测试 empty)
            'category_promo': 'Free shipping on apparel orders over $50' # 类别3的促销信息
        }
    ] # 分组数据定义结束
    context = {
                'data_by_category': categorized_data} # 定义模板上下文
    return render(request, 'store/products_by_category.html', context) # 渲染模板
{# store/templates/store/products_by_category.html #}
<h1>Products by Category</h1>
{% for category_group in data_by_category %} {# 外层循环:遍历每个类别分组 #}
    <section class="category-section">
        <h2>
            Category {
               { forloop.counter }}: {
               { category_group.category_name }}
            (Outer loop index: {
               { forloop.counter0 }})
        </h2>
        {% if category_group.category_promo %} {# 如果类别有促销信息 #}
            <p class="promo">{
               { category_group.category_promo }}</p> {# 显示促销信息 #}
        {% endif %}

        {% if category_group.products %} {# 检查当前类别是否有产品 #}
            <ul>
                {% for product in category_group.products %} {# 内层循环:遍历当前类别下的产品 #}
                    <li>
                        {
               { product.name }} - ${
               { product.price|floatformat:2 }}
                        (Overall item #{
               { forloop.parentloop.counter0 }}.{
               { forloop.counter0 }}) {# 使用parentloop访问外层循环的索引 #}
                        {% if forloop.first %} (First in this category){% endif %} {# 标记类别中的第一个产品 #}
                    </li>
                {% empty %} {# 内层循环的empty块,如果category_group.products为空 #}
                    <li>No products currently in the {
               { category_group.category_name }} category.</li> {# 显示无产品提示 #}
                {% endfor %} {# 内层循环结束 #}
            </ul>
        {% else %} {# 如果category_group.products本身就是空的或不存在 (对应外层循环的当前项) #}
            <p>No products listed for the {
               { category_group.category_name }} category.</p> {# 另一种无产品提示方式 #}
        {% endif %}
    </section>
    {% if not forloop.last %}<hr>{% endif %} {# 在类别之间添加分隔线,除了最后一个 #}
{% empty %} {# 外层循环的empty块 #}
    <p>No categories to display.</p> {# 如果没有任何类别数据显示 #}
{% endfor %} {# 外层循环结束 #}

代码解释与实践:

{% for category_group in data_by_category %}: 外层循环遍历类别。
{% for product in category_group.products %}: 内层循环遍历该类别下的产品。
{
{ forloop.parentloop.counter0 }}
: 在内层循环中,forloop 指向内层循环的状态。通过 forloop.parentloop 可以访问到外层循环的 forloop 对象,从而获取外层循环的计数器等信息。这对于生成嵌套列表的编号或理解层次结构非常有用。
内层循环也使用了 {% empty %} 来处理某个类别下可能没有产品的情况。
外层循环的 {% empty %} 处理整个 data_by_category 为空的情况。

结合 {% cycle %} 标签 (稍后详述 cycle):
forloop.cycle 也可以实现类似 {% cycle %} 标签的功能,但 {% cycle %} 标签更通用,可以在循环外使用。

{# 假设 item_list 存在 #}
<ul>
{% for item in item_list %}
    <li class="{
               { forloop.cycle 'list-item-odd' 'list-item-even' 'list-item-highlight' }}"> {# 使用forloop.cycle交替CSS类 #}
        {
               { item }}
    </li>
{% endfor %}
</ul>

reversed 关键字的应用:
当需要按相反顺序显示项目时,例如最新的评论在最前面,而数据源是按时间升序排列的。

{# 假设 comments 是一个按时间升序排列的列表或查询集 #}
<div class="comments-section">
    <h3>Recent Comments (Newest First):</h3>
    {% for comment in comments reversed %} {# 反向迭代评论列表 #}
        <div class="comment">
            <p><strong>{
               { comment.user.username }}</strong> ({
               { comment.timestamp|timesince }} ago):</p> {# 显示用户名和评论时间 #}
            <p>{
               { comment.text|linebreaksbr }}</p> {# 显示评论内容,自动转换换行为<br> #}
        </div>
    {% empty %}
        <p>No comments yet. Be the first to comment!</p> {# 无评论提示 #}
    {% endfor %}
</div>

注意: 对于大型查询集,在数据库层面使用 order_by('-timestamp') 通常比在模板中使用 reversed 更高效。reversed 在模板层面执行,可能意味着先获取所有数据到内存再反转(取决于对象类型)。

避免在循环中执行复杂计算或数据库查询:
这是 DTL (以及所有模板引擎) 的通用性能准则。如果循环体内部需要复杂的数据处理或多次访问数据库,应将这些逻辑移到视图中,预先准备好所有需要展示的数据,然后将扁平化或结构化的数据传递给模板。模板中的 for 循环应尽可能只做简单的迭代和数据显示。

B. {% if ... %}, {% elif ... %}, {% else %}

核心功能: 执行条件判断,根据表达式的真假来渲染不同的代码块。

语法:

{% if condition1 %} {# if标签开始,判断condition1 #}
    ... (当 condition1 为真时渲染) ...
{% elif condition2 %} {# 可选的elif标签,当前面的if/elif为假时,判断condition2 #}
    ... (当 condition1 为假且 condition2 为真时渲染) ...
{% else %} {# 可选的else标签,当前面所有if/elif都为假时执行 #}
    ... (当所有条件都为假时渲染) ...
{% endif %} {# if标签结束 #}

条件表达式 (condition):

可以是单个变量 (DTL 会判断其“真实性”,类似于 Python 的 bool(variable))。

False 的值包括:None、空字符串 ""、空列表 []、空字典 {}、空元组 ()、数字零 0
所有其他值通常被认为是 True

可以使用比较操作符:== (等于), != (不等于), < (小于), > (大于), <= (小于等于), >= (大于等于)。
可以使用逻辑操作符:and, or, not (注意是小写)。
可以使用 innot in 操作符来检查成员关系。
重要: DTL 的 if 标签中的表达式求值能力远不如 Python 的 if 语句。

不能直接进行算术运算: {% if user.age + 5 > 20 %}无效的。算术运算应在视图中完成。
不能直接调用带参数的方法: {% if user.has_permission('edit_post') %}无效的。应在视图中调用并将布尔结果传递给模板,或者创建一个自定义模板标签。
变量属性/字典键访问: {% if user.profile.is_active %}{% if settings_dict.FEATURE_ENABLED %} 是允许的。
过滤器可以用在 if 条件中的变量上吗? 通常不行。DTL 的 if 标签直接对变量或其属性/键的“真实性”或比较结果进行判断。如果你需要基于过滤后的值做判断,通常也应在视图中处理或通过自定义标签。

例如,{% if items|length > 0 %}无效的。你应该用 {% if items %} (判断列表是否为空) 或 {% if items|length_is:"0"|not %} (使用 length_is 过滤器和 not,但这比较迂回)。更好的方式是直接判断 items 是否为空: {% if items %} (列表非空为真) 或 {% if not items %} (列表为空为真)。
对于长度,DTL 提供了一个 length 过滤器,但它通常用于输出,而不是直接在 if 逻辑中使用。{% if forloop.length > 5 %} 是可以的,因为 forloop.length 是一个可以直接比较的数字。

操作符优先级: DTL 遵循一定的操作符优先级 (not > and > or)。可以使用括号 () 来明确分组和改变运算顺序,但这在 DTL 的 if 中并不常见,因为表达式本身就应该保持简单。

内部机制:

DTL 引擎在遇到 if 标签时,会解析其后的条件表达式。
它会从上下文中获取变量的值,并按照 DTL 的“真实性”规则或比较操作符进行求值。
如果条件为真,则渲染该 if (或 elif) 块内部的内容,并跳过后续的 elifelse 块。
如果所有 ifelif 条件都为假,则渲染 else 块的内容(如果存在)。

企业级应用场景与最佳实践:

根据用户权限显示/隐藏内容:

# views.py
# request.user 对象由 django.contrib.auth.context_processors.auth 添加到上下文
# 假设 User 模型有一个 is_moderator 布尔字段或一个 has_perm 方法
# def some_view(request):
#     user_can_delete = request.user.has_perm('myapp.delete_comment')
#     return render(request, 'myapp/comment_display.html', {'user_can_delete_comment': user_can_delete, ...})
{# myapp/templates/myapp/comment_display.html #}
<div class="comment">
    <p>{
               { comment.text }}</p>
    <span class="author">By: {
               { comment.author.username }}</span>
    {% if request.user.is_authenticated %} {# 检查用户是否已认证 #}
        {% if request.user == comment.author or request.user.is_staff or user_can_delete_comment %} {# 用户是评论作者,或者是员工,或者有删除权限 #}
            <button class="delete-comment-btn" data-comment-id="{
               { comment.id }}">Delete</button> {# 显示删除按钮 #}
        {% endif %}
        {% if request.user.is_staff %} {# 如果用户是员工 #}
            <button class="edit-comment-btn" data-comment-id="{
               { comment.id }}">Edit (Staff)</button> {# 显示编辑按钮 #}
        {% endif %}
    {% endif %}
</div>

实践:

request.user (由上下文处理器提供) 在模板中非常常用。
复杂的权限逻辑 (如 user_can_delete_comment) 应该在视图中计算出来,并将一个简单的布尔标志传递给模板。模板中的 if 条件应保持简洁。

处理可选数据显示:

{# 假设 'product' 对象在上下文中 #}
<div class="product-details">
    <h2>{
               { product.name }}</h2>
    {% if product.description %} {# 如果产品描述存在且不为空 #}
        <div class="description">
            <h3>Product Description:</h3>
            <p>{
               { product.description|linebreaks }}</p> {# 显示描述,并将文本换行转为HTML换行 #}
        </div>
    {% endif %}

    {% if product.sku %} {# 如果SKU存在 #}
        <p>SKU: {
               { product.sku }}</p> {# 显示SKU #}
    {% else %}
        <p>SKU: Not Available</p> {# 如果SKU不存在,显示提示 #}
    {% endif %}

    {% if product.image_url %} {# 如果图片URL存在 #}
        <img src="{
               { product.image_url }}">{ product.name }}"> {# 显示产品图片 #}
    {% else %}
        <img src="{% static 'images/placeholder.png' %}">

实践: DTL 的 if 非常适合处理这种“如果数据存在则显示,否则显示替代内容或什么都不显示”的场景。

if 条件中使用 and, or, not:

{# 假设 'user' 和 'subscription' 对象在上下文中 #}
{% if user.is_authenticated and user.subscription.is_active and not user.is_banned %} {# 组合多个条件 #}
    <p>Welcome, valued subscriber!</p> {# 用户已认证、订阅激活且未被封禁 #}
    <a href="{% url 'premium_content' %}">Access Premium Content</a> {# 提供高级内容链接 #}
{% elif user.is_authenticated and not user.subscription.is_active %} {# 用户已认证但订阅未激活 #}
    <p>Your subscription is not active. Please renew to access premium features.</p> {# 提示续订 #}
    <a href="{% url 'renew_subscription' %}">Renew Now</a> {# 续订链接 #}
{% else %} {# 其他情况 (未登录或被封禁等) #}
    <p>Please log in or sign up to see more.</p> {# 提示登录或注册 #}
{% endif %}

实践: 可以组合多个条件,但如果逻辑变得非常复杂,应考虑在视图中预计算一个更简单的标志变量。

if 条件中使用 in 操作符:

# views.py
# def my_view(request):
#     user_groups = [group.name for group in request.user.groups.all()] # 获取用户所属组的名称列表
#     special_item_ids = [10, 25, 42]
#     context = {
                
#         'user_groups': user_groups,
#         'item': {'id': 25, 'name': 'Special Item'},
#         'special_item_ids': special_item_ids
#     }
#     return render(request, 'myapp/in_operator_example.html', context)
{# myapp/templates/myapp/in_operator_example.html #}
{% if "editors" in user_groups and "beta_testers" in user_groups %} {# 检查用户是否同时属于 'editors' 和 'beta_testers' 组 #}
    <p>You are an editor and a beta tester! Special access granted.</p> {# 显示特殊权限信息 #}
{% endif %}

{% if item.id in special_item_ids %} {# 检查 item.id 是否在 special_item_ids 列表中 #}
    <p>"{
               { item.name }}" is a specially featured item!</p> {# 如果是特殊项目则显示提示 #}
{% endif %}

{% if "admin" not in user_groups %} {# 检查用户是否不属于 'admin' 组 #}
    <p>You do not have administrator privileges.</p> {# 显示无管理员权限提示 #}
{% endif %}

实践: innot in 对于检查成员关系非常方便。user_groups 应该是一个列表或元组。special_item_ids 也是如此。

比较操作:

{# 假设 'score' 和 'max_score' 在上下文中 #}
{% if score == max_score %} {# 检查分数是否等于最高分 #}
    <p>Perfect score! Congratulations!</p> {# 满分祝贺 #}
{% elif score >= max_score * 0.8 %} {# 检查分数是否大于等于最高分的80% (注意:这种乘法应在视图中完成) #}
    {# DTL修正: 假设 high_threshold = max_score * 0.8 在视图中计算好并传入 #}
    {# {% if score >= high_threshold %} #}
    <p>Excellent score! You did very well.</p> {# 高分评价 #}
{% elif score < low_threshold %} {# 假设 low_threshold 在视图中计算好并传入 #}
    <p>Keep practicing! You can improve.</p> {# 低分鼓励 #}
{% endif %}

{% if product.quantity_available > 0 %} {# 检查产品可用数量是否大于0 #}
    <button>Add to Cart</button> {# 显示添加到购物车按钮 #}
{% else %}
    <button disabled>Out of Stock</button> {# 禁用按钮并显示缺货 #}
{% endif %}

重要修正与实践:

如注释中指出的,{% if score >= max_score * 0.8 %} 这样的表达式在 DTL 中是无效的,因为 DTL 的 if 标签不支持直接进行算术运算 (*)。
正确做法: 这种阈值比较 (max_score * 0.8) 应该在视图 (Python 代码) 中计算完成,然后将计算结果 (例如 high_threshold) 传递到模板的上下文中。模板中只进行简单的比较:{% if score >= high_threshold %}
这是 DTL “限制模板逻辑”设计哲学的一个典型体现。模板应该只做简单的比较和布尔判断,复杂的计算和数据转换属于业务逻辑,应在视图层处理。

ifchanged 标签 (与 if 不同,但相关): {% ifchanged %} 标签用于在循环中检测一个或多个变量的值是否与上一次迭代时不同。如果不同,则渲染其内容。这对于在排序列表中生成分组标题等场景非常有用。我们将在后面单独详细讨论 ifchanged

避免过度嵌套 if 语句:
如果模板中出现了三层或更多层的 if 嵌套,通常表明模板承担了过多的逻辑。这会降低模板的可读性和可维护性。应考虑:

在视图中重构逻辑: 将复杂的条件判断移到视图中,计算出一个或多个简单的布尔标志变量传递给模板。
使用自定义模板标签: 将复杂的条件显示逻辑封装到自定义标签中。
将部分逻辑拆分到包含的模板中: 如果适用,可以将内部的 if 块提取到 {% include %} 的子模板中,并有条件地包含它。

DTL 的 if 标签是控制模板渲染流程的核心工具之一。掌握其正确的条件表达式语法、理解其能力边界(特别是与 Python if 的区别),并遵循将复杂逻辑保留在视图中的原则,是编写清晰、高效 DTL 模板的关键。

4.3.2.1 {% for ... %} 循环标签的进阶应用

a. for...empty 子句:处理空序列

在实际应用中,我们经常会遇到需要迭代的序列可能为空的情况。如果直接使用 {% for %} 循环一个空序列,那么循环体内的所有内容都不会被渲染。{% empty %} 子句为我们提供了一种优雅的方式来处理这种情况,即当迭代的序列为空时,渲染 {% empty %} 块内的内容。

{# 场景:显示用户订单列表,如果用户没有订单,则显示提示信息 #}
{% if user_orders %} {# 首先检查 user_orders 是否存在且不为空,这是一种防御性编程 #}
    <h2>您的订单历史:</h2>
    <ul>
        {% for order in user_orders %} {# 遍历用户的订单列表 #}
            <li>
                订单号: {
           { order.id }} - 金额: ¥{
           { order.amount }} - 状态: {
           { order.get_status_display }} {# 显示订单的ID、金额和状态描述 #}
                <ul>
                    {% for item in order.items.all %} {# 嵌套循环,遍历订单中的商品项 #}
                        <li>{
           { item.product.name }} ({
           { item.quantity }}件) - ¥{
           { item.subtotal }}</li> {# 显示商品名称、数量和小计 #}
                    {% empty %} {# 如果订单中没有任何商品项(理论上不常见,但作为示例) #}
                        <li>此订单中没有商品。</li> {# 显示订单为空的提示 #}
                    {% endfor %} {# 结束商品项的循环 #}
                </ul>
            </li>
        {% empty %} {# 如果 user_orders 列表为空 #}
            <p>您还没有任何订单。快去挑选您喜欢的商品吧!</p> {# 显示用户没有订单的提示信息 #}
        {% endfor %} {# 结束订单列表的循环 #}
    </ul>
{% else %} {# 如果 user_orders 不存在或为空 #}
    <p>您还没有任何订单。快去挑选您喜欢的商品吧!</p> {# 同样显示用户没有订单的提示信息 #}
{% endif %}

代码解释:

{% if user_orders %}: 首先检查 user_orders 变量是否存在并且不为空。这是一种良好的实践,可以避免在 user_ordersNone 或其他非预期值时 {% for %} 标签可能引发的问题。
{% for order in user_orders %}: 开始遍历 user_orders 列表。
{
{ order.id }}
, {
{ order.amount }}
, {
{ order.get_status_display }}
: 展示每个订单的关键信息。get_status_display 是 Django Model Field Choices 提供的一个便捷方法,用于获取可读的选项描述。
{% for item in order.items.all %}: 这是一个嵌套循环,用于显示每个订单中的商品项。假设 order 对象有一个名为 items 的关联对象管理器(如 ForeignKey 或 ManyToManyField),.all 用于获取所有关联的商品项。
{% empty %} (内层循环): 如果一个订单 order.items.all 为空,则会显示 “此订单中没有商品。”。
{% endfor %} (内层循环): 结束对订单商品项的迭代。
{% empty %} (外层循环): 如果 user_orders 列表本身为空(即用户没有任何订单),则会执行这个 empty 块中的内容,显示 “您还没有任何订单。快去挑选您喜欢的商品吧!”。
{% else %}: 如果最初的 {% if user_orders %} 条件不满足,也会显示相同的提示信息。

企业级应用场景思考:

动态内容展示:在电商平台的订单列表、用户的消息通知、管理后台的数据表格等场景,如果数据列表为空,使用 {% empty %} 可以提供更友好的用户体验,而不是简单地留白。
占位符内容:在内容管理系统 (CMS) 中,如果某个分类下没有文章,可以使用 {% empty %} 显示引导用户创建内容的提示。
仪表盘组件:在数据分析仪表盘中,如果某个图表的数据源为空,可以使用 {% empty %} 显示“暂无数据”或“正在收集中”等状态。

b. for 循环的内置变量:forloop 对象

{% for %} 循环内部,DTL 提供了一个名为 forloop 的特殊模板变量。这个对象包含了当前循环迭代状态的有用信息,可以帮助我们实现更复杂的渲染逻辑。

forloop 对象包含以下属性:

forloop.counter: 当前迭代的次数(从1开始计数)。
forloop.counter0: 当前迭代的次数(从0开始计数)。
forloop.revcounter: 从循环末尾开始的迭代次数(从1开始计数)。当循环到最后一项时,revcounter 为1。
forloop.revcounter0: 从循环末尾开始的迭代次数(从0开始计数)。当循环到最后一项时,revcounter0 为0。
forloop.first: 一个布尔值,如果当前是第一次迭代,则为 True
forloop.last: 一个布尔值,如果当前是最后一次迭代,则为 True
forloop.parentloop: 在嵌套循环中,这个属性指向外层循环的 forloop 对象。

示例:使用 forloop 实现更丰富的列表展示

{# 场景:展示一个新闻列表,并对第一条和最后一条新闻进行特殊标记,同时显示序号 #}
{# 假设 news_list 是从视图传递过来的新闻对象列表 #}
{% if news_list %}
    <div class="news-container">
        {% for news_item in news_list %} {# 开始遍历新闻列表 #}
            <div class="news-article {% if forloop.first %}first-news{% endif %} {% if forloop.last %}last-news{% endif %}"> {# 根据是否为首尾项添加CSS类 #}
                <h4>
                    <span class="news-index">{
           { forloop.counter }}.</span> {# 显示新闻的序号,从1开始 #}
                    {
           { news_item.title }} {# 显示新闻标题 #}
                </h4>
                <p class="publish-date">发布于: {
           { news_item.publish_date|date:"Y-m-d H:i" }}</p> {# 显示发布日期,并使用date过滤器格式化 #}
                <p class="short-content">{
           { news_item.short_description|truncatewords:30 }}</p> {# 显示新闻摘要,并使用truncatewords过滤器截断 #}

                {% if forloop.first %} {# 如果是第一条新闻 #}
                    <p class="highlight"><strong>最新!</strong> 这是我们列表中的第一条新闻。</p> {# 对第一条新闻添加高亮提示 #}
                {% endif %}

                {% if forloop.last and not forloop.first %} {# 如果是最后一条新闻,且列表不只有一项 #}
                    <hr> {# 在最后一条新闻后添加分割线 #}
                    <p class="note">已是列表末尾。</p> {# 提示已到列表末尾 #}
                {% endif %}

                {# 示例:嵌套循环中的 parentloop (假设 news_item 有 related_tags 属性) #}
                {% if news_item.related_tags.all %} {# 检查是否有相关标签 #}
                    <div class="tags">
                        <strong>相关标签 (外层循环次数 {
           { forloop.counter0 }}):</strong>
                        {% for tag in news_item.related_tags.all %} {# 遍历相关标签 #}
                            <span class="tag-item">{
           { tag.name }}</span> {# 显示标签名称 #}
                            {# 访问父循环的计数器 #}
                            (父循环计数: {
           { forloop.parentloop.counter }}, 当前子循环计数: {
           { forloop.counter }})
                        {% endfor %} {# 结束标签循环 #}
                    </div>
                {% endif %}
            </div>
        {% empty %} {# 如果 news_list 为空 #}
            <p>暂时没有新闻发布。</p> {# 显示没有新闻的提示 #}
        {% endfor %} {# 结束新闻列表循环 #}
    </div>
{% else %}
    <p>无法加载新闻列表。</p> {# news_list 本身不存在或为 None 时的提示 #}
{% endif %}

代码解释:

{% for news_item in news_list %}: 遍历新闻列表。
class="news-article {% if forloop.first %}first-news{% endif %} {% if forloop.last %}last-news{% endif %}":

forloop.first: 判断当前是否为第一次迭代。如果是,则添加 first-news CSS 类。
forloop.last: 判断当前是否为最后一次迭代。如果是,则添加 last-news CSS 类。这常用于给列表的首尾项设置不同的样式。

<span class="news-index">{
{ forloop.counter }}.</span>
: 使用 forloop.counter 显示从1开始的序号。
{
{ news_item.publish_date|date:"Y-m-d H:i" }}
: 使用 date 过滤器格式化日期。
{
{ news_item.short_description|truncatewords:30 }}
: 使用 truncatewords 过滤器将摘要截断为30个词。
{% if forloop.first %} ... {% endif %}: 专门为第一条新闻添加额外的提示信息。
{% if forloop.last and not forloop.first %} ... {% endif %}:

为最后一条新闻添加分割线和提示。
and not forloop.first 条件是为了防止当列表只有一项时,同时触发 forloop.firstforloop.last 的逻辑,避免重复或冲突的显示。

嵌套循环与 forloop.parentloop:

{% for tag in news_item.related_tags.all %}: 这是一个嵌套循环,用于显示新闻的相关标签。
{
{ forloop.parentloop.counter }}
: 在内层循环中,可以通过 forloop.parentloop 访问外层(新闻列表)循环的 forloop 对象,从而获取外层循环的计数器等信息。这在需要根据外层循环状态调整内层循环行为时非常有用。

企业级应用场景思考与 forloop 的高级运用:

表格隔行变色 (Zebra Striping):

{% for item in items %}
    <tr class="{% if forloop.counter0|divisibleby:2 %}even-row{% else %}odd-row{% endif %}"> {# 使用 forloop.counter0 和 divisibleby 过滤器实现隔行变色 #}
        <td>{
             { item.name }}</td>
        <td>{
             { item.value }}</td>
    </tr>
{% endfor %}

这里 forloop.counter0 (从0开始) 配合 divisibleby:2 过滤器可以轻松判断当前行是奇数行还是偶数行,从而应用不同的CSS类。
每N项换行/分组: 在展示图片墙或产品列表时,可能需要每隔N个元素就换行或者开始一个新的分组。

<div class="product-grid">
    {% for product in product_list %}
        {% if forloop.counter0|divisibleby:4 and not forloop.first %} {# 每4个产品(从第0个开始计数)且不是第一个时,结束上一行 #}
            </div><div class="product-row"> {# 结束旧的 product-row 并开始新的 #}
        {% elif forloop.first %} {# 如果是第一个产品 #}
            <div class="product-row"> {# 开始第一行 #}
        {% endif %}
        <div class="product-item">{
             { product.name }}</div> {# 显示产品项 #}
        {% if forloop.last %} {# 如果是最后一个产品 #}
            </div> {# 结束最后一行 #}
        {% endif %}
    {% endfor %}
</div>

这个例子相对复杂,需要仔细处理边界条件。更简洁的方式可能是结合CSS的flexbox或grid布局。但在某些特定布局需求下,forloop 依然有用。
进度指示器: 在处理一个较长的列表时,可以根据 forloop.counter 和总数(如果知道)显示一个简单的进度,例如 “正在处理第 X / Y 项”。
嵌套数据结构的复杂渲染: 当处理树状结构或多层嵌套的评论时,forloop.parentloop 变得非常关键,它可以帮助你了解当前在嵌套结构中的深度或访问父级迭代的状态。

4.3.2.2 {% if %}, {% elif %}, {% else %} 标签的深入理解

条件判断是任何编程语言(包括模板语言)的核心功能。DTL 的 {% if %} 标签系列允许我们根据变量的值或表达式的结果来选择性地渲染模板内容。

基本语法回顾:

{% if condition1 %}
    {# 当 condition1 为真时渲染此部分 #}
{% elif condition2 %}
    {# 当 condition1 为假,且 condition2 为真时渲染此部分 #}
{% else %}
    {# 当所有前面的条件都为假时渲染此部分 #}
{% endif %}

{% elif %}{% else %} 都是可选的。

DTL 中“真值”与“假值”的判断标准:

DTL 在 {% if %} 标签中判断条件真假的标准与 Python 类似:

假值 (False):

False (布尔值)
None
空序列 (空列表 [], 空元组 (), 空字典 {}, 空字符串 "", 空集合 set())
数值零 (整数 0, 浮点数 0.0)
实现了 __bool__() 方法且返回 False 的对象。
实现了 __len__() 方法且返回 0 的对象(如果未实现 __bool__())。

真值 (True): 其他所有情况。

复杂的条件表达式与操作符:

{% if %} 标签支持使用以下操作符来构建更复杂的条件:

比较操作符:

== (等于)
!= (不等于)
< (小于)
> (大于)
<= (小于等于)
>= (大于等于)
in (成员测试,例如 value in some_list)
not in (非成员测试)
is (对象同一性测试,例如 variable is None)
is not (非对象同一性测试)

逻辑操作符:

and (逻辑与)
or (逻辑或)
not (逻辑非)

重要限制: DTL 中的 {% if %} 标签不允许直接在条件中使用括号 () 来控制运算优先级。条件的求值严格按照从左到右的顺序,and 的优先级高于 ornot 的优先级最高。如果需要复杂的逻辑组合,建议在视图 (View) 中预处理这些逻辑,并将结果(通常是一个布尔值)传递给模板。这是 DTL “保持模板逻辑简单”设计哲学的一部分。

示例:复杂的条件判断与操作符使用

{# 场景:根据用户角色、文章状态和权限来决定是否显示编辑按钮 #}
{# 假设从视图传递了以下上下文:
   user_is_authenticated (布尔值)
   user_is_staff (布尔值)
   user_is_superuser (布尔值)
   article (文章对象)
   user_can_edit_article (视图中预计算的布尔值,表示当前用户是否有编辑此文章的权限)
#}

{% if user_is_authenticated %} {# 首先检查用户是否已登录 #}
    <p>欢迎回来, {
           { request.user.username }}!</p> {# 显示欢迎信息,request.user 在已登录时可用 #}

    {% if article %} {# 确保文章对象存在 #}
        <h3>{
           { article.title }}</h3> {# 显示文章标题 #}
        <p>{
           { article.content|safe }}</p> {# 显示文章内容,使用 safe 过滤器允许 HTML(假设内容是安全的) #}

        {# 条件1: 超级用户总是可以编辑 #}
        {# 条件2: 或者,员工用户且文章是草稿状态 (假设 article.status == 'draft') #}
        {# 条件3: 或者,视图明确告知用户可以编辑此特定文章 (user_can_edit_article) #}
        {% if user_is_superuser or (user_is_staff and article.status == 'draft') or user_can_edit_article %} {# 组合多个条件判断 #}
            <a href="{% url 'edit_article' article_id=article.id %}">编辑文章</a> {# 显示编辑链接,使用 url 标签生成URL #}
        {% elif article.author == request.user and article.status != 'published' %} {# 条件4: 或者,当前用户是作者且文章未发布 #}
            <p>您可以编辑您的未发布文章。</p>
            <a href="{% url 'edit_article' article_id=article.id %}">编辑文章</a>
        {% else %} {# 如果以上条件都不满足 #}
            <p>您没有编辑此文章的权限。</p> {# 显示没有编辑权限的提示 #}
        {% endif %}

        {# 另一个例子:使用 'in' 和 'not' #}
        {% if 'feature_flag_x' in active_feature_flags and not user_is_beta_tester %} {# 检查特性标志是否激活,且用户不是beta测试者 #}
            <p>特性X对您不可用 (仅限 Beta 测试者)。</p>
        {% elif 'feature_flag_x' in active_feature_flags and user_is_beta_tester %}
             <p>您可以使用特性X!</p>
        {% endif %}

    {% else %} {# 如果 article 对象不存在 #}
        <p>请求的文章未找到。</p> {# 显示文章未找到的提示 #}
    {% endif %}

{% else %} {# 如果用户未登录 #}
    <p>请 <a href="{% url 'login' %}?next={
           { request.path }}">登录</a> 查看内容并进行操作。</p> {# 提示用户登录,并提供 next 参数以便登录后重定向 #}
{% endif %}

代码解释:

{% if user_is_authenticated %}: DTL 模板通常可以访问 request 对象,因此 request.user.is_authenticated 是一个常见的检查。这里假设 user_is_authenticated 是视图直接传递的布尔值,简化演示。
{
{ request.user.username }}
: 如果用户已登录,可以通过 request.user 访问用户信息。
{
{ article.content|safe }}
: 如果文章内容包含 HTML,并且你信任其来源(例如,富文本编辑器生成的,或者经过了充分的清理),可以使用 safe 过滤器来防止 Django 自动转义 HTML 标签。滥用 safe 会导致XSS漏洞,务必谨慎!
{% if user_is_superuser or (user_is_staff and article.status == 'draft') or user_can_edit_article %}:

这是一个多条件组合。注意,虽然 DTL 不支持显式括号,但 and 的优先级高于 or。所以这个表达式等价于 user_is_superuser or (user_is_staff and article.status == 'draft') or user_can_edit_article
article.status == 'draft': 比较文章状态是否为草稿。
user_can_edit_article: 这是推荐的做法,即将复杂的权限逻辑放在视图中处理,模板只接收一个布尔结果。

{% url 'edit_article' article_id=article.id %}: 使用 url 标签根据 URL 名称(在 urls.py 中定义)和参数动态生成 URL。这是保持 URL 结构灵活的最佳实践。
{% elif article.author == request.user and article.status != 'published' %}:

article.author == request.user: 检查文章作者是否为当前登录用户。
article.status != 'published': 检查文章状态是否不为“已发布”。

{% if 'feature_flag_x' in active_feature_flags and not user_is_beta_tester %}:

'feature_flag_x' in active_feature_flags: 检查字符串是否在一个列表(或任何可迭代对象)active_feature_flags 中。
not user_is_beta_tester: 对布尔值 user_is_beta_tester 取反。

{% url 'login' %}?next={
{ request.path }}
:

生成登录页面的 URL。
?next={
{ request.path }}
: 将当前页面的路径作为 next 查询参数传递给登录页面。Django 的登录视图通常会处理这个参数,在用户成功登录后将其重定向回原始页面。

企业级应用中的条件渲染策略:

视图层预处理复杂逻辑:

核心原则: 尽可能将复杂的业务逻辑、权限检查、数据转换等操作放在 Python 视图代码中完成。视图应该向模板传递已经处理好的、易于直接使用的数据和状态标志(通常是布尔值)。
优点:

模板简洁性: 保持模板的可读性和易维护性,使其专注于展示逻辑。
可测试性: Python 代码比模板逻辑更容易进行单元测试。
性能: Python 执行逻辑通常比在模板解释器中执行复杂逻辑更高效。
安全性: 避免在模板中暴露过多的业务细节或进行不安全的操作。

示例 (views.py):

from django.shortcuts import render
from .models import Article

def article_detail(request, article_id):
    article = Article.objects.get(pk=article_id)
    can_edit = False
    can_publish = False

    if request.user.is_authenticated:
        if request.user.is_superuser:
            can_edit = True
            can_publish = True
        elif request.user.is_staff and article.status == 'draft':
            can_edit = True
            can_publish = True # 假设员工也可以发布草稿
        elif article.author == request.user:
            can_edit = True
            if article.status != 'published': # 作者只能发布未发布的
                can_publish = True
        # 更复杂的权限检查,例如基于组或特定权限模型
        # if request.user.has_perm('myapp.change_article'):
        #     can_edit = True

    context = {
                
        'article': article,
        'user_can_edit_article': can_edit,       # 视图中计算好的布尔值
        'user_can_publish_article': can_publish, # 视图中计算好的布尔值
    }
    return render(request, 'myapp/article_detail.html', context)

模板 (myapp/article_detail.html):

{% if user_can_edit_article %}
    <a href="...">编辑</a>
{% endif %}
{% if user_can_publish_article %}
    <form method="post" action="{% url 'publish_article' article.id %}">
        {% csrf_token %}
        <button type="submit">发布文章</button>
    </form>
{% endif %}

特性标志 (Feature Flags/Toggles):

在大型企业应用中,经常需要逐步推出新功能或对不同用户群体展示不同的特性。特性标志是一种强大的技术。
可以将激活的特性标志列表传递到模板上下文中,然后在模板中使用 {% if 'my_new_feature' in active_flags %} 来控制相关 UI 元素的显示。
管理特性标志的系统(如 Django Waffle, Gargoyle, 或自定义实现)通常与用户分群、百分比发布等高级功能结合。

A/B 测试:

与特性标志类似,可以根据用户被分配到的 A/B 测试组来渲染不同的模板片段或应用不同的样式。
{% if user_group == 'A' %}{% elif user_group == 'B' %}{% endif %}

设备检测与响应式设计:

虽然主要的响应式设计通过 CSS媒体查询实现,但在某些情况下,服务器端可能需要根据用户代理(User-Agent)或其他信息判断设备类型(桌面、平板、手机),并传递一个标志给模板,以便渲染针对特定设备优化的布局或组件。
例如: {% if device_type == 'mobile' %}{% else %}{% endif %}
不过,过度依赖服务器端设备检测可能不如纯客户端 CSS 响应式灵活。

国际化 (i18n) 和本地化 (l10n):

{% if LANGUAGE_CODE == 'en-us' %}{% elif LANGUAGE_CODE == 'zh-hans' %}{% endif %}
虽然 Django 的 i18n 机制主要通过 {% trans %}{% blocktrans %} 标签以及翻译文件来实现,但在极少数情况下,可能需要根据当前语言代码来渲染完全不同的结构或包含特定于语言的资源。

避免在 {% if %} 中进行的方法调用(除非该方法是属性或无副作用的简单查询):

DTL 的设计哲学不鼓励在模板中执行复杂的计算或有副作用的操作。当你在 {% if %} 中使用点号查找属性时,例如 article.is_published,Django 会尝试几种查找方式:字典键查找、属性查找、方法调用(如果它是一个无参数的方法)。

安全: {% if article.is_published %} (如果 is_published 是一个属性或无参方法,通常是安全的)
不推荐: {% if article.calculate_complex_statistic() %} (如果这个方法计算量大或有副作用)
错误: {% if article.update_views_count() %} (方法调用通常不能带参数,且不应在模板中执行有副作用的操作)

如果方法需要参数,或者它执行的是修改数据的操作,或者计算成本很高,那么它绝对不应该在模板中被调用。这些都应该在视图中完成。

4.3.2.3 {% with %} 标签:简化复杂变量的访问与提高可读性

{% with %} 标签允许你为一个复杂的变量表达式赋予一个更简单的别名,并在 {% with %}{% endwith %} 标签块的范围内使用这个别名。这对于提高模板的可读性、避免重复计算(尽管 DTL 缓存属性查找结果)以及简化对深层嵌套数据结构的访问非常有用。

基本语法:

{% with alias=complex_variable_expression %}
    {# 在这个块内部,可以使用 'alias' 来代替 'complex_variable_expression' #}
    <p>值为: {
           { alias }}</p>
    {% if alias.some_property %}
        <p>属性存在: {
           { alias.some_property }}</p>
    {% endif %}
{% endwith %}

你可以同时定义多个别名:

{% with alpha=object.very_long_attribute_name beta=another_object.get_value.sub_value %}
    <p>Alpha: {
           { alpha }}</p>
    <p>Beta: {
           { beta }}</p>
{% endwith %}

{% with %} 的主要优点:

提高可读性: 用简短、有意义的别名替换冗长或复杂的变量路径。
减少重复: 如果一个复杂的表达式在模板的某个区域内被多次使用,{% with %} 可以避免重复书写。
性能(微优化): DTL 会对属性查找进行缓存。虽然 {% with %} 本身不一定带来巨大的性能提升(因为查找通常很快),但它可以使模板更清晰,并且确保一个复杂的查找只执行一次(其结果被赋给别名)。在极少数情况下,如果点号查找涉及到的是一个计算成本较高的 @property 或无参方法,{% with %} 可以显式地“缓存”其结果供块内使用。

示例:使用 {% with %} 简化模板

场景1:访问深层嵌套的数据

假设视图传递了一个复杂的配置对象:

# views.py (示例)
context = {
            
    'site_config': {
            
        'theme': {
            
            'colors': {
            
                'primary': '#007bff',
                'secondary': '#6c757d',
                'background': '#f8f9fa'
            },
            'font': {
            
                'family': 'Arial, sans-serif',
                'size': '16px'
            }
        },
        'seo': {
            
            'default_title': 'My Awesome Site',
            'default_description': 'A great site built with Django.'
        }
    }
}

不使用 {% with %} 的模板片段:

<body>
    <header>
        <h1>{
           { site_config.seo.default_title }}</h1>
    </header>
    <p>Welcome! The secondary color is {
           { site_config.theme.colors.secondary }}.</p>
</body>

这种方式非常冗长且容易出错。

使用 {% with %} 优化后的模板片段:

{% with theme_colors=site_config.theme.colors theme_font=site_config.theme.font site_seo=site_config.seo %}
    <body>
        <header>
            <h1>{
           { site_seo.default_title }}</h1>
        </header>
        <p>Welcome! The secondary color is {
           { theme_colors.secondary }}.</p>
        <p>Font size: {
           { theme_font.size }}</p>
        <meta name="description" content="{
           { site_seo.default_description }}">
    </body>
{% endwith %}

代码解释:

{% with theme_colors=site_config.theme.colors theme_font=site_config.theme.font site_seo=site_config.seo %}:

theme_colors 成为了 site_config.theme.colors 的别名。
theme_font 成为了 site_config.theme.font 的别名。
site_seo 成为了 site_config.seo 的别名。

{% with %}{% endwith %} 之间,可以直接使用这些简短的别名,使得模板代码更加简洁和易于理解。

场景2:缓存一个可能计算成本较高的方法的返回值(谨慎使用)

假设一个模型有一个 @property,它执行了一些计算:

# models.py
class Product(models.Model):
    name = models.CharField(max_length=100)
    base_price = models.DecimalField(max_digits=10, decimal_places=2)
    # ... other fields ...

    @property
    def discounted_price(self):
        # 模拟一个可能涉及数据库查询或复杂计算的属性
        # In a real scenario, this might involve checking active promotions, user group, etc.
        from decimal import Decimal
        # print(f"Calculating discounted_price for {self.name}") # 用于调试,观察调用次数
        discount_percentage = self.get_current_discount_for_user() # 这是一个假设的方法
        if discount_percentage > 0:
            return self.base_price * (Decimal('1') - discount_percentage / Decimal('100'))
        return self.base_price

    def get_current_discount_for_user(self):
        # 假设这个方法会查询数据库或执行一些逻辑
        # For this example, let's return a fixed discount
        return Decimal('10') # 10% discount

模板中多次使用该属性:

{# 假设 product 是单个 Product 实例 #}
<p>产品名称: {
           { product.name }}</p>
<p>原价: {
           { product.base_price }}</p>
{% if product.discounted_price < product.base_price %}
    <p>折扣价: <strong>{
           { product.discounted_price }}</strong> (省了 {
           { product.base_price|subtract:product.discounted_price }})</p>
    <p>您享受了折扣!最终价格为: {
           { product.discounted_price }}</p>
{% else %}
    <p>价格: {
           { product.discounted_price }} (当前无折扣)</p>
{% endif %}

在这个模板中,product.discounted_price 被引用了多次。如果 discounted_price 的计算成本很高,这可能会导致不必要的重复计算(尽管 Django 的模板系统在同一渲染周期内对同一对象的同一属性/无参方法的查找结果有缓存机制,但 {% with %} 可以更明确地控制这一点)。

使用 {% with %} 优化:

{# 假设 product 是单个 Product 实例 #}
<p>产品名称: {
           { product.name }}</p>
<p>原价: {
           { product.base_price }}</p>
{% with final_price=product.discounted_price original_price=product.base_price %} {# 将计算结果赋给别名 #}
    {% if final_price < original_price %} {# 使用别名进行比较和显示 #}
        <p>折扣价: <strong>{
           { final_price }}</strong> (省了 {
           { original_price|subtract:final_price }})</p> {# subtract 是一个需要自定义的过滤器或在视图中计算差额 #}
        <p>您享受了折扣!最终价格为: {
           { final_price }}</p>
    {% else %}
        <p>价格: {
           { final_price }} (当前无折扣)</p>
    {% endif %}
{% endwith %}

代码解释:

{% with final_price=product.discounted_price original_price=product.base_price %}:

product.discounted_price 的结果被计算一次并赋值给 final_price
product.base_price 的值被赋值给 original_price(这里主要是为了演示同时定义多个别名,对于简单属性访问,其性能影响可以忽略)。

在块内,所有对 final_price 的引用都将使用这个已经计算好的值。
{
{ original_price|subtract:final_price }}
: subtract 不是 Django 内置过滤器。你需要自定义它,或者更好的做法是在视图中计算差额并传递给模板。

企业级场景中的 {% with %} 运用思考:

处理来自API的复杂JSON响应: 当模板需要渲染从外部API获取的、结构嵌套较深的JSON数据时,{% with %} 可以极大地简化对特定数据片段的访问。

{# api_response 是一个复杂的字典 #}
{% with user_profile=api_response.data.user.profile shipping_address=api_response.data.user.addresses.shipping %}
    <p>用户名: {
             { user_profile.username }}</p>
    <p>邮箱: {
             { user_profile.email }}</p>
    <p>收货城市: {
             { shipping_address.city }}</p>
{% endwith %}

组件化模板中的参数传递: 在使用 {% include %} 标签引入子模板(组件)时,如果子模板需要多个基于父模板上下文计算或组合的变量,可以使用 {% with %}include 之前准备好这些变量,使 include 标签本身更简洁。

{# parent_template.html #}
{% with card_title=object.name|truncatechars:20 card_image=object.get_primary_image_url card_link=object.get_absolute_url %}
    {% include "myapp/components/info_card.html" with title=card_title image_url=card_image link_url=card_link only %}
{% endwith %}

{# myapp/components/info_card.html #}
<div class="card">
    {% if image_url %}<img src="{
             { image_url }}">{ title }}">{% endif %}
    <h3><a href="{
             { link_url }}">{
             { title }}</a></h3>
</div>

注意 {% include ... with ... only %} 的用法,only 关键字确保只有显式通过 with 传递的变量才对子模板可见,这有助于创建更独立的组件。

在循环内部简化对当前项复杂属性的访问:

{% for item in complex_item_list %}
    {% with primary_data=item.get_primary_data secondary_info=item.related_object.fetch_secondary_details %}
        <div class="item-display">
            <h4>{
             { primary_data.name }}</h4>
            <p>Info: {
             { secondary_info.description }}</p>
            {% if primary_data.is_active %}
                <span class="status-active">Active</span>
            {% endif %}
        </div>
    {% endwith %}
{% endfor %}

这样做可以使得循环体内部的逻辑更加清晰,尤其是当 item 的属性或方法调用链较长时。

{% with %} 是 DTL 中一个简单但非常实用的工具,它体现了编写清晰、可维护模板代码的重要性。

4.3.2.4 模板继承:{% extends %}{% block %} 标签

模板继承是 DTL 中最强大、最核心的概念之一。它允许你构建一个基础的“骨架”模板 (base template),该骨架包含了网站所有页面共享的元素(如页头、页脚、导航栏等),然后让各个子模板 (child templates) 继承这个骨架并覆盖或填充其中预定义的特定区域。这种机制极大地减少了代码冗余,提高了模板的可维护性和一致性。

核心思想:

基础模板 (Base Template): 定义整个站点或应用某一部分的通用HTML结构。在这个模板中,使用 {% block block_name %} 标签来定义一些可被子模板覆盖的区域。
子模板 (Child Template): 使用 {% extends "base_template_name.html" %} 标签(必须是模板中的第一个标签)来声明它继承自哪个基础模板。然后,在子模板中,使用与基础模板中同名的 {% block block_name %} 标签来提供该特定块的内容。

{% extends "path/to/base_template.html" %} 标签

作用: 告知模板引擎当前模板继承自另一个模板。
参数: 一个字符串,表示基础模板的路径。这个路径的解析方式与 {% include %} 类似,通常是相对于模板加载器配置的某个模板目录。
位置: {% extends %} 标签必须是模板文件中的第一个模板标签。在其之前可以有 HTML 注释 ({# ... #}),但不能有任何其他 Django 模板标签或 HTML 内容。否则,模板引擎会抛出 TemplateSyntaxError
行为: 当模板引擎遇到 {% extends %} 标签时,它会首先加载并解析父模板。然后,它会找到子模板中定义的 {% block %} 标签,并用子模板中相应 block 的内容替换父模板中同名的 block。如果子模板没有定义某个父模板中的 block,则会使用父模板中该 block 的默认内容(如果在父模板中提供了)。

{% block block_name %}{% endblock [block_name] %} 标签

作用: 在基础模板中定义可被子模板覆盖的区域,并在子模板中提供这些区域的具体内容。
block_name: 一个唯一的名称,用于标识这个块。名称应由字母、数字和下划线组成。
在基础模板中:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-F-8">
    <title>{% block title %}默认标题{% endblock title %}</title> {# 定义一个名为 title 的块,并提供默认内容 #}
    {% block extra_head %}{% endblock extra_head %} {# 定义一个名为 extra_head 的空块,子模板可以填充额外的头部内容 #}
</head>
<body>
    <header>
        <h1>{% block page_header %}我的网站{% endblock page_header %}</h1>
    </header>
    <nav>
        {% block navigation %}
            <ul>
                <li><a href="/">首页</a></li>
                <li><a href="/about/">关于</a></li>
            </ul>
        {% endblock navigation %}
    </nav>
    <main>
        {% block content %}
            <p>这是默认的主要内容区域。</p>
        {% endblock content %}
    </main>
    <footer>
        <p>&copy; {% now "Y" %} 我的公司. {% block footer_extra %}{% endblock %}</p> {# now 标签获取当前年份,footer_extra 是一个可扩展的块 #}
    </footer>
    {% block scripts %}{% endblock scripts %} {# 用于子模板添加页面特定的 JavaScript 文件 #}
</body>
</html>

在子模板中:

{% extends "base.html" %} {# 声明继承自 base.html #}

{% block title %}产品详情页{% endblock title %} {# 覆盖 title 块的内容 #}

{% block extra_head %} {# 添加额外的头部内容 #}
    <link rel="stylesheet" href="/static/css/product_detail.css">
    <meta name="description" content="查看我们的最新产品详情。">
{% endblock extra_head %}

{% block page_header %}产品:{
             { product.name }}{% endblock page_header %} {# 覆盖页面主标题 #}

{% block content %} {# 覆盖主要内容区域 #}
    <h2>{
             { product.name }}</h2>
    <img src="{
             { product.image.url }}">{ product.name }}">
    <p>价格: ¥{
             { product.price }}</p>
    <p>描述: {
             { product.description|linebreaksbr }}</p> {# linebreaksbr 过滤器将换行符转为 <br> #}

    {# 如果想在子模板中追加内容到父块,而不是完全覆盖,可以使用 {
             { block.super }} #}
{% endblock content %}

{% block scripts %} {# 添加此页面特定的 JavaScript #}
    {
             { block.super }} {# 如果父模板的 scripts 块有内容,先渲染它 #}
    <script src="/static/js/product_interaction.js"></script>
{% endblock scripts %}

{% endblock %}{% endblock block_name %}: 用于结束一个 block 定义。在 {% endblock %} 后面指定块名称是可选的,但推荐这样做,尤其是在嵌套 block 或较长的 block 中,可以提高可读性。如果指定了名称,它必须与起始 {% block %} 的名称匹配。

{
{ block.super }}
变量

作用: 在子模板的 {% block %} 内部,{
{ block.super }}
变量允许你获取并渲染父模板中同名 block 的内容。这对于需要在父块内容的基础上追加或前置内容非常有用,而不是完全替换它。
使用场景:

向父模板的头部信息中添加额外的 CSS 或 JavaScript。
在父模板的导航链接列表后添加子模板特有的导航项。
在父模板的通用脚本块之后加载特定页面的脚本。

示例:使用 {
{ block.super }}

基础模板 (base_layout.html):

<!DOCTYPE html>
<html>
<head>
    <title>{% block page_title %}通用网站标题{% endblock %}</title>
    {% block head_css %}
        <link rel="stylesheet" href="/static/css/base_styles.css"> {# 通用基础样式 #}
    {% endblock head_css %}
</head>
<body>
    <div>
        {% block sidebar_content %}
            <h4>导航</h4>
            <ul>
                <li><a href="/">主页</a></li>
            </ul>
        {% endblock sidebar_content %}
    </div>
    <div>
        {% block main_content_area %}
            <p>欢迎来到我们的网站!</p>
        {% endblock main_content_area %}
    </div>
    {% block body_scripts %}
        <script src="/static/js/global_analytics.js"></script> {# 全局统计脚本 #}
    {% endblock body_scripts %}
</body>
</html>

子模板 (article_page.html):

{% extends "base_layout.html" %} {# 继承 base_layout.html #}

{% block page_title %}{
           { article.title }} - 文章详情{% endblock page_title %} {# 覆盖页面标题 #}

{% block head_css %} {# 扩展 head_css 块 #}
    {
           { block.super }} {# 首先引入父模板 base_styles.css #}
    <link rel="stylesheet" href="/static/css/article_specific.css"> {# 然后添加文章页特定的样式 #}
    <link rel="canonical" href="{
           { article.get_absolute_url }}"> {# 添加规范链接 #}
{% endblock head_css %}

{% block sidebar_content %} {# 完全覆盖侧边栏内容 #}
    <h4>相关文章</h4>
    <ul>
        {% for related_article in article.related_articles.all %} {# 假设 article 有 related_articles 关联 #}
            <li><a href="{
           { related_article.get_absolute_url }}">{
           { related_article.title }}</a></li>
        {% endfor %}
    </ul>
    <hr>
    <p><a href="{% url 'article_archive' %}">返回文章列表</a></p> {# 使用 url 标签生成归档页链接 #}
{% endblock sidebar_content %}

{% block main_content_area %} {# 覆盖主要内容区域 #}
    <h1>{
           { article.title }}</h1>
    <div class="meta">作者: {
           { article.author.get_full_name }} | 发布于: {
           { article.publish_date|date:"Y-m-d" }}</div>
    <div class="article-body">
        {
           { article.body|safe }} {# 假设文章内容是安全的 HTML #}
    </div>
{% endblock main_content_area %}

{% block body_scripts %} {# 扩展 body_scripts 块 #}
    {
           { block.super }} {# 首先加载父模板的 global_analytics.js #}
    <script src="/static/js/article_comments.js"></script> {# 然后加载文章评论相关的脚本 #}
    <script>
        // 页面特定的内联脚本
        console.log("正在查看文章: {
           { article.id }}");
    </script>
{% endblock body_scripts %}

代码解释:

base_layout.html:

定义了 page_title, head_css, sidebar_content, main_content_area, body_scripts 等多个 block
head_css 块包含了基础样式表。
body_scripts 块包含了全局分析脚本。

article_page.html:

{% extends "base_layout.html">: 声明继承。
{% block page_title %}: 完全覆盖了父模板的页面标题。
{% block head_css %}:

{
{ block.super }}
: 这一行至关重要,它会先将 base_layout.htmlhead_css 块的内容(即 <link rel="stylesheet" href="/static/css/base_styles.css">)渲染出来。
然后,子模板添加了它自己的 article_specific.csscanonical 链接。

{% block sidebar_content %}: 这个块完全覆盖了父模板的侧边栏,没有使用 {
{ block.super }}
,因此父模板的默认导航不会显示。
{% block main_content_area %}: 完全覆盖了主要内容区域,用于显示文章详情。
{% block body_scripts %}:

{
{ block.super }}
: 先渲染了父模板的 global_analytics.js
然后添加了 article_comments.js 和一些内联脚本。

多级继承:

模板继承可以进行多级。例如,你可能有一个非常通用的站点基础模板 site_base.html,然后有一个针对应用某个部分的 app_base.html 继承自 site_base.html,最后具体的页面模板再继承自 app_base.html

site_base.html (顶级父模板):

<!DOCTYPE html>
<html lang="{% get_current_language as LANGUAGE_CODE %}{
           { LANGUAGE_CODE }}"> {# 获取当前语言代码 #}
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block site_title %}我的全球网站{% endblock %}</title>
    {% block site_global_styles %}
        <link rel="stylesheet" href="/static/css/normalize.css">
        <link rel="stylesheet" href="/static/css/global_theme.css">
    {% endblock %}
    {% block site_extra_head %}{% endblock %}
</head>
<body>
    <div>
        <header>
            {% block site_branding %}
                <a href="/">公司Logo</a>
            {% endblock %}
            {% block site_global_nav %}
                {# 全站通用导航,可能包含语言切换等 #}
            {% endblock %}
        </header>
        <div>
            {% block site_content_wrapper %}
                <p>这是最外层的内容包装。</p>
            {% endblock %}
        </div>
        <footer>
            {% block site_footer_content %}
                <p>&copy; {% now "Y" %} Acme Corp. All rights reserved.</p>
            {% endblock %}
        </footer>
    </div>
    {% block site_global_scripts %}
        <script src="/static/js/third_party_library.js"></script>
    {% endblock %}
    {% block site_page_specific_scripts %}{% endblock %}
</body>
</html>

blog_base.html (继承自 site_base.html, 作为博客部分的父模板):

{% extends "site_base.html" %} {# 继承站点基础模板 #}

{% block site_title %}{% block blog_title %}博客{% endblock %} - {
           { block.super }}{% endblock %} {# 修改站点标题,并允许博客子页面进一步定义 blog_title #}

{% block site_global_styles %}
    {
           { block.super }} {# 引入 normalize.css 和 global_theme.css #}
    <link rel="stylesheet" href="/static/css/blog_theme.css"> {# 添加博客模块专属样式 #}
{% endblock %}

{% block site_global_nav %}
    {
           { block.super }} {# 保留可能的全局导航 #}
    <nav>
        <ul>
            <li><a href="{% url 'blog:post_list' %}">所有文章</a></li>
            <li><a href="{% url 'blog:category_list' %}">分类</a></li>
            {% if user.is_staff %}
                <li><a href="{% url 'admin:index' %}">管理后台</a></li> {# 假设 admin 应用的 URL #}
            {% endif %}
        </ul>
    </nav>
{% endblock %}

{% block site_content_wrapper %} {# 替换站点内容包装区域 #}
    <div class="blog-container">
        <aside>
            {% block blog_sidebar %}
                <h3>最新动态</h3>
                {# ... 最新文章、热门标签等 ... #}
            {% endblock blog_sidebar %}
        </aside>
        <main>
            {% block blog_content %}
                <p>欢迎来到博客区。</p>
            {% endblock blog_content %}
        </main>
    </div>
{% endblock %}

{% block site_page_specific_scripts %}
    {
           { block.super }} {# 保留父模板可能有的脚本 #}
    <script src="/static/js/blog_common.js"></script> {# 博客模块通用脚本 #}
{% endblock %}

blog_post_detail.html (博客文章详情页, 继承自 blog_base.html):

{% extends "blog_base.html" %} {# 继承博客基础模板 #}
{% load humanize %} {# 加载 humanize 过滤器,例如 intcomma #}

{% block blog_title %}{
           { post.title|truncatechars:50 }}{% endblock %} {# 定义博客标题部分 #}

{% block blog_sidebar %}
    {
           { block.super }} {# 显示 blog_base.html 中定义的最新动态等 #}
    <hr>
    <h4>关于作者</h4>
    <p>{
           { post.author.username }} (共发表 {
           { post.author.posts.count }} 篇文章)</p>
{% endblock %}

{% block blog_content %} {# 填充博客主内容区域 #}
    <article class="blog-post">
        <h1>{
           { post.title }}</h1>
        <p class="meta">
            发布于 {
           { post.publish_date|naturaltime }} {# naturaltime 过滤器显示 "3小时前", "2天前" 等 #}
            {% if post.category %} | 分类: <a href="{
           { post.category.get_absolute_url }}">{
           { post.category.name }}</a>{% endif %}
        </p>
        <div class="post-body">
            {
           { post.content|safe }}
        </div>
        <div class="post-tags">
            {% for tag in post.tags.all %} {# 假设 post 有 tags 多对多字段 #}
                <a href="{% url 'blog:posts_by_tag' tag.slug %}" class="tag">{
           { tag.name }}</a>
            {% endfor %}
        </div>
    </article>
    {% include "blog/includes/comments_section.html" with post=post %} {# 引入评论区子模板 #}
{% endblock blog_content %}

{% block site_page_specific_scripts %} {# 继承自 site_base,但 blog_base 也调用了 block.super #}
    {
           { block.super }} {# 这会先加载 blog_common.js,而 blog_common.js 会先加载 third_party_library.js #}
    <script src="/static/js/blog_post_interactions.js"></script>
    <script>
        // 文章详情页特定脚本
        console.log("当前文章ID: {
           { post.id }}");
    </script>
{% endblock %}

代码解释 (多级继承):

site_base.html: 定义了最顶层的结构和全局资源,例如 site_title, site_global_styles, site_global_scripts
blog_base.html:

{% extends "site_base.html">: 继承自站点基础。
{% block site_title %}{% block blog_title %}博客{% endblock %} - {
{ block.super }}{% endblock %}
:

它覆盖了 site_basesite_title 块。
内部又定义了一个新的 blog_title 块,这样更具体的博客页面(如文章详情页)可以只定义 blog_title,而不用关心 - {
{ block.super }}
(即 - 我的全球网站) 这部分。
{
{ block.super }}
在这里指的是 site_base.htmlsite_title 的默认内容 “我的全球网站”。

它通过 {
{ block.super }}
保留了 site_global_stylessite_global_nav 的内容,并添加了博客特定的样式和导航。
它完全重写了 site_content_wrapper,提供了博客特有的两栏布局(侧边栏 + 主内容区),并在这两栏内部定义了新的 block (blog_sidebar, blog_content)。

blog_post_detail.html:

{% extends "blog_base.html" %}: 继承自博客基础。
它填充了 blog_base.html 中定义的 blog_title, blog_sidebar (通过 block.super 追加内容), 和 blog_content
{
{ block.super }}
site_page_specific_scripts 中的行为:因为 blog_base.html 在它的 site_page_specific_scripts 块中也调用了 {
{ block.super }}
,所以最终脚本的加载顺序会是:

site_base.htmlsite_global_scripts 内容 (third_party_library.js)
blog_base.htmlsite_page_specific_scripts 中添加的内容 (blog_common.js)
blog_post_detail.htmlsite_page_specific_scripts 中添加的内容 (blog_post_interactions.js 和内联脚本)
这种链式调用 block.super 对于层层叠加资源非常有用。

模板继承的最佳实践与企业级考量:

合理的块 (Block) 划分:

在基础模板中定义足够多、粒度适中的 block。考虑哪些部分最有可能被子模板修改或扩展。
常见的 block 包括:title, meta_tags, styles (或 head_css), scripts (或 footer_scripts, body_scripts), header, navigation, sidebar, content, footer
为特定的可重用组件或区域创建 block,即使它们在父模板中是空的,也为子模板提供了插入点。

清晰的继承层级:

通常2-3层继承足够。过深的继承层级会使模板结构难以理解和维护。
例如:

base.html (全站最基础,HTML骨架, doctype, head, body, 全局CSS/JS)
app_base.html (应用级基础,继承 base.html,添加应用通用导航、布局结构)
page_specific.html (具体页面,继承 app_base.html,填充内容块)

{
{ block.super }}
的明智使用
:

当你希望在父块内容基础上添加,而不是完全替换时,务必使用 {
{ block.super }}

忘记使用 {
{ block.super }}
是一个常见错误,会导致父模板中重要的资源(如CSS、JS)丢失。

不要在 {% block %} 标签内部使用 {% extends %}: 这是不允许的,会导致 TemplateSyntaxError

块名称的唯一性: 在一个模板的继承链中,所有 block 的名称都共享同一个命名空间。子模板中的 block 会覆盖继承链上任何父模板中同名的 block

在视图中决定使用哪个父模板 (高级):

在极少数情况下,你可能需要根据某些条件在视图中动态选择要继承的父模板。
视图可以将父模板的名称作为上下文变量传递给子模板,然后在子模板的 {% extends %} 标签中使用这个变量:

# views.py
def my_view(request):
    if request.user.is_premium_member:
        base_template_name = 'premium_base.html'
    else:
        base_template_name = 'standard_base.html'
    return render(request, 'my_page_content.html', {
                'base_template_to_extend': base_template_name})
{# my_page_content.html #}
{% extends base_template_to_extend %} {# 动态继承父模板 #}

{% block content %}
    <p>这是特定页面的内容...</p>
{% endblock %}

注意: {% extends %} 标签的参数必须在编译时能够解析为字符串或变量。它不能是复杂的模板标签。

性能考量:

模板继承本身对性能的影响通常很小,因为模板在首次加载后会被编译并缓存(如果开启了模板缓存)。
主要的性能瓶颈通常来自模板中过多的数据库查询、复杂的计算(应移至视图)或大量的 I/O 操作。
使用 django-debug-toolbar 可以帮助分析模板渲染时间和查询次数。

与前端框架的集成:

当使用像 React, Vue, Angular 等现代前端框架时,Django 模板的继承可能主要用于渲染初始的 HTML 骨架,包括挂载前端应用的 <div> 元素和引入前端打包后的 JavaScript/CSS 文件。
block 仍然可以用于注入后端传递的初始数据(例如,JSON 序列化的配置或用户状态到 <script> 标签中)。

{# base.html for a SPA #}
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}My SPA{% endblock %}</title>
    {% block styles %}
        <link rel="stylesheet" href="/static/dist/main.css"> {# Webpack/Vite 等打包的CSS #}
    {% endblock %}
</head>
<body>
    <div>
        {% block app_ssr_content %}{% endblock %} {# 可选:用于服务器端渲染的初始内容 #}
    </div>

    {% block initial_data_script %}
        <script type="application/json">
            {
             { initial_data_json|safe }} {# 从视图传递 JSON 数据 #}
        </script>
    {% endblock %}

    {% block scripts %}
        <script src="/static/dist/main.js"></script> {# Webpack/Vite 等打包的JS #}
    {% endblock %}
</body>
</html>

模板继承是 Django 保持 “DRY” (Don’t Repeat Yourself) 原则的关键机制之一,它使得构建大型、复杂且易于维护的网站界面成为可能。

4.3.2.5 {% include "path/to/template.html"> 标签:代码片段的重用

{% include %} 标签允许你将一个模板的内容包含(嵌入)到另一个模板中。这对于重用常见的UI组件、代码片段或模板的一部分非常有用,可以避免在多个地方复制代码。

基本语法:

{% include "myapp/includes/header_navigation.html" %}

参数:

模板名称 (必需): 一个字符串,表示要包含的模板文件的路径。可以是硬编码的字符串,也可以是一个模板变量(该变量的值应该是模板路径)。
with (可选): 用于向被包含的模板传递额外的上下文变量。
only (可选): 与 with 结合使用时,only 关键字表示只有通过 with 显式传递的变量才对被包含的模板可见。否则,被包含的模板会继承当前模板的所有上下文。

模板名称解析:

{% extends %} 类似,{% include %} 的模板名称参数由 Django 的模板加载器解析。加载器会查找配置的模板目录。

上下文传递:

默认行为 (继承上下文):
如果不使用 only 关键字,被包含的模板会接收到当前模板(调用 {% include %} 的模板)的完整上下文。

{# main_template.html - 假设上下文中已有 user 和 site_name #}
{% with current_section="profile" %} {# 定义一个局部变量 #}
    <p>User: {
             { user.name }}, Site: {
             { site_name }}, Section: {
             { current_section }}</p>
    {% include "myapp/includes/user_greeting.html" %} {# user_greeting.html 可以访问 user, site_name, current_section #}
{% endwith %}

{# myapp/includes/user_greeting.html #}
<p>Hello, {
             { user.name }}! Welcome to {
             { site_name }}. You are in {
             { current_section }}.</p>

使用 with 传递额外或覆盖的上下文:
你可以使用 with 关键字向被包含的模板传递特定的变量。这些变量会添加到(或覆盖)被包含模板的上下文中。

{# main_template.html #}
{% include "myapp/includes/product_card.html" with product=featured_product card_theme="dark" show_rating=True %} {# 传递 product, card_theme, show_rating #}

{# myapp/includes/product_card.html #}
<div class="product-card theme-{
             { card_theme|default:'light' }}"> {# 使用传递的 card_theme,默认为 'light' #}
    <h3>{
             { product.name }}</h3>
    <p>Price: {
             { product.price }}</p>
    {% if show_rating and product.rating %} {# 使用传递的 show_rating #}
        <p>Rating: {
             { product.rating }} stars</p>
    {% endif %}
</div>

在这个例子中,myapp/includes/product_card.html 会收到 product (值为 featured_product)、card_theme (值为 "dark") 和 show_rating (值为 True)。它仍然可以访问 main_template.html 的其他上下文变量(除非使用了 only)。

使用 only 限制上下文:
当你在 with 子句后添加 only 关键字时,被包含的模板将只能访问通过 with 传递的变量,它不会继承父模板的任何其他上下文。这有助于创建更加独立和可预测的组件,防止意外的上下文泄漏。

{# main_template.html - 上下文包含 site_name, user #}
{% with product_to_display=special_offer_product %}
    {% include "myapp/includes/isolated_product_display.html" with item=product_to_display display_mode="compact" only %}
    {# isolated_product_display.html 只能访问 item 和 display_mode。它不能访问 site_name 或 user。 #}
{% endwith %}

{# myapp/includes/isolated_product_display.html #}
<div class="isolated-product mode-{
             { display_mode }}">
    <h4>{
             { item.name }}</h4>
    {# <p>Welcome, {
             { user.name }}</p>  <-- 这行会出错,因为 user 没有被传递且 only 生效了 #}
</div>

使用 only 是创建可重用、封装良好的模板组件的最佳实践。

模板变量作为模板名称:

{% include %} 的参数可以是模板变量,这允许动态选择要包含的模板。

{# views.py #}
# context['notification_template'] = 'notifications/email_notification.html'
# or context['notification_template'] = 'notifications/sms_notification.html'

{# main_template.html #}
{% include notification_template with notification=user_notification only %} {# notification_template 是一个变量 #}

这在需要根据条件渲染不同子模板的场景中非常有用,例如根据用户偏好显示不同类型的通知,或者根据内容类型加载不同的渲染器。

{% include %}{% extends %} 的区别:

{% extends %}: 用于建立父子模板关系,定义一个骨架并由子模板填充。一个模板中只能有一个 {% extends %},且必须是第一个标签。它是关于“整体页面结构”的。
{% include %}: 用于将一个模板片段嵌入到另一个模板中。一个模板中可以有多个 {% include %}。它是关于“重用UI片段/组件”的。
通常,一个页面模板会 {% extends %} 一个基础模板,然后在基础模板的某个 {% block %} 内部,可能会使用多个 {% include %} 来引入可重用的组件(如导航栏、侧边栏小部件、表单等)。

企业级应用场景与最佳实践:

创建可重用的UI组件:

表单 (Forms): _form.html (包含表单字段、错误消息、CSRF令牌、提交按钮)。

{# myapp/includes/_form_snippet.html #}
<form method="{
               { form_method|default:'post' }}" action="{
               { form_action|default:'' }}" {% if form.is_multipart %}enctype="multipart/form-data"{% endif %}>
    {% csrf_token %} {# 确保 CSRF 令牌被包含 #}
    {
               { form.management_form }} {# 用于 formsets #}
    {% for hidden_field in form.hidden_fields %} {# 渲染隐藏字段 #}
        {
               { hidden_field }}
    {% endfor %}

    {% for field in form.visible_fields %} {# 遍历可见字段 #}
        <div class="form-group {% if field.errors %}has-error{% endif %}">
            {
               { field.label_tag }} {# 渲染字段标签 #}
            {
               { field }} {# 渲染字段本身 (input, select, textarea) #}
            {% if field.help_text %}
                <small class="form-text text-muted">{
               { field.help_text|safe }}</small> {# 显示帮助文本 #}
            {% endif %}
            {% for error in field.errors %} {# 显示字段级错误 #}
                <div class="invalid-feedback">{
               { error }}</div>
            {% endfor %}
        </div>
    {% endfor %}

    {% if form.non_field_errors %} {# 显示表单级错误 #}
        <div class="alert alert-danger">
            {% for error in form.non_field_errors %}
                <p>{
               { error }}</p>
            {% endfor %}
        </div>
    {% endif %}

    <button type="submit" class="btn btn-primary">{
               { submit_button_text|default:'提交' }}</button>
</form>

{# 在其他模板中使用 #}
{# {% include "myapp/includes/_form_snippet.html" with form=my_user_profile_form submit_button_text="更新资料" only %} #}

导航栏 (Navigation Bars): _navbar.html
页脚 (Footers): _footer.html
卡片 (Cards): _product_card.html, _article_summary_card.html
分页控件 (Pagination Controls): _pagination.html
消息/警告框 (Alerts/Messages): _messages.html (常用于显示 django.contrib.messages)

管理后台的自定义组件: 在 Django Admin 中,你可以覆盖 admin 的模板,并使用 {% include %} 来插入自定义的小部件或信息面板。

邮件模板: 将邮件的页眉、页脚、签名等通用部分提取到单独的模板中,然后 {% include %} 到具体的邮件内容模板中。

避免过度使用 {% include %}:

如果一个片段非常小,并且只在一个地方使用,或者其上下文传递非常复杂,那么直接写在父模板中可能更清晰。
过多的、细碎的 {% include %} 可能会使模板的整体逻辑难以追踪。需要权衡可重用性和可读性。

{% block %} 结合:

你可以在被 {% include %} 的模板内部使用 {% block %}。然而,这些 block 只能被继承自该被包含模板的模板所覆盖,它们与调用 {% include %} 的模板的继承链是隔离的。
这种用法不常见,通常如果需要这种级别的可定制性,可能意味着组件设计得不够灵活,或者应该考虑使用自定义模板标签。

性能考量:

{% extends %} 类似,{% include %} 本身对性能影响不大,因为模板会被编译和缓存。
如果被包含的模板执行了耗时的操作(如自己的数据库查询,不推荐),那么每次包含都会执行。
确保传递给被包含模板的上下文是高效获取的。

示例:使用 {% include %} 显示 Django 的消息框架 (django.contrib.messages)

Django 的消息框架允许你在视图中存储一次性通知消息(如“操作成功”、“发生错误”),然后在下一个请求的模板中显示它们。

myapp/templates/myapp/includes/_messages.html (或放在全局 templates/includes/ 下):

{% if messages %} {# 检查是否有任何消息 #}
    <div class="messages-container" aria-live="polite" aria-atomic="true"> {# ARIA属性增强可访问性 #}
        {% for message in messages %} {# 遍历所有消息 #}
            {# message.tags 通常包含消息级别,如 "debug", "info", "success", "warning", "error" #}
            {# 这些可以用来映射到 Bootstrap 或其他 CSS 框架的警告框类名 #}
            <div class="alert alert-{
           { message.level_tag }} alert-dismissible fade show" role="alert"> {# 使用消息级别作为CSS类 #}
                {
           { message|safe }} {# 显示消息内容,通常消息内容是安全的 #}
                <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
        {% endfor %}
    </div>
    {# 清除已显示的消息,防止它们在后续的 include 中再次出现(通常 Django 自动处理,但显式操作无害) #}
    {# {% get_messages as messages %}  <-- This is not standard DTL, message consumption happens automatically. #}
{% endif %}

在基础模板 base.html 中包含它:

{# base.html #}
<body>
    <header>...</header>
    {% include "myapp/includes/_messages.html" %} {# 在页面顶部(或合适位置)包含消息显示模板 #}
    <main>
        {% block content %}{% endblock %}
    </main>
    <footer>...</footer>
</body>

在视图中添加消息 views.py:

from django.contrib import messages
from django.shortcuts import render, redirect

def my_form_view(request):
    if request.method == 'POST':
        form = MyForm(request.POST)
        if form.is_valid():
            form.save()
            messages.success(request, '您的信息已成功保存!') # 添加成功消息
            return redirect('some_success_url')
        else:
            messages.error(request, '表单提交失败,请检查错误。') # 添加错误消息
    else:
        form = MyForm()
    return render(request, 'my_form_template.html', {
            'form': form})

def another_view(request):
    # ...
    messages.info(request, '这是一条提示信息。') # 添加提示消息
    return render(request, 'another_template.html')

当渲染 my_form_template.htmlanother_template.html (假设它们都继承自包含 _messages.htmlbase.html) 时,_messages.html 会被包含,并渲染出视图中添加的消息。message.level_tag 可以方便地与CSS框架(如Bootstrap的 alert-success, alert-danger 等)集成。

{% include %} 是实现模板模块化和DRY原则的重要工具,它使得管理复杂界面变得更加容易。

4.3.2.6 {% load %} 标签:扩展模板功能

Django 模板语言的核心功能虽然强大,但有时我们需要执行更复杂的操作、格式化数据或引入在标准标签和过滤器中无法实现的功能。{% load %} 标签就是为此而生,它允许你加载自定义的模板标签 (template tags) 和过滤器 (filters),或者加载 Django 内置的一些标签库(如 static, humanize, i18n 等)。

基本语法:

{% load library_name1 library_name2 ... %}

或者,从特定库中加载单个标签或过滤器:

{% load specific_tag_or_filter from library_name %}

library_name: 包含自定义标签/过滤器的Python模块的名称。Django 会在每个已安装应用的 templatetags 子目录中查找这些模块。
specific_tag_or_filter: 要从库中加载的特定标签或过滤器的名称。

加载 Django 内置的标签/过滤器库:

Django 自带了一些非常有用的模板标签库,你需要先加载它们才能使用其中的标签或过滤器。

static:

用于处理静态文件(CSS, JavaScript, Images等)。
核心标签:{% static "path/to/file" %}
加载方式: {% load static %}
示例:

{% load static %} {# 加载 static 标签库 #}
<link rel="stylesheet" href="{% static 'myapp/css/style.css' %}"> {# 生成指向 style.css 的URL #}
<img src="{% static 'myapp/images/logo.png' %}">

{% static %} 标签会使用 STATIC_URL 设置以及可能的静态文件存储后端(如 ManifestStaticFilesStorage 用于文件名哈希)来构造正确的URL。

i18n (国际化与本地化):

用于翻译文本和格式化本地化数据(如日期、数字)。
核心标签:{% trans "Text to translate" %}, {% blocktrans %}, {% localize %}, {% unlocalize %}
核心过滤器:date (受本地化影响), number (受本地化影响)。
加载方式: {% load i18n %}
示例:

{% load i18n %} {# 加载 i18n 标签库 #}

<p>{% trans "Welcome to our website!" %}</p> {# 翻译字符串 #}

{% blocktrans count counter=item_count %} {# 处理单复数形式的翻译 #}
There is one item.
{% plural %}
There are {
               { counter }} items.
{% endblocktrans %}

<p>Date: {
               { some_date_variable|date:"SHORT_DATE_FORMAT" }}</p> {# 本地化日期格式 #}

{% localize on %} {# 开启区域感知格式化 #}
    <p>Value: {
               { some_number_variable }}</p> {# 数字会根据当前区域设置格式化,例如使用逗号或点作为千位分隔符 #}
{% endlocalize %}

humanize:

提供了一系列使数据更易读的过滤器。
常用过滤器:intcomma (整数加逗号), intword (大数字转为 “1.2 million”), naturalday (“today”, “yesterday”, “tomorrow”), naturaltime (“2 hours ago”), apnumber (数字1-9转为英文单词)。
加载方式: {% load humanize %}
示例:

{% load humanize %} {# 加载 humanize 过滤器库 #}

<p>Total users: {
               { user_count|intcomma }}</p> {# 例如:1,234,567 #}
<p>Large number: {
               { very_large_number|intword }}</p> {# 例如:1.2 million #}
<p>Article published: {
               { article.publish_date|naturaltime }}</p> {# 例如:2 hours ago #}
<p>Event date: {
               { event.date|naturalday }}</p> {# 例如:tomorrow #}

tz (时区处理):

用于在模板中进行显式的时区转换和感知。
核心标签:{% timezone "Europe/Paris" %}, {% localtime %}
加载方式: {% load tz %}
示例:

{% load tz %} {# 加载 tz 标签库 #}

<p>Current time (UTC): {
               { utc_now_variable }}</p> {# 假设 utc_now_variable 是一个UTC的datetime对象 #}

{% timezone "America/New_York" %} {# 切换到纽约时区 #}
    <p>Event starts at (New York Time): {
               { event_datetime_utc|date:"Y-m-d H:i:s T" }}</p> {# event_datetime_utc 会被转换为纽约时间显示 #}
{% endtimezone %} {# 恢复到之前的时区 #}

{% localtime on %} {# 开启本地时间转换(基于 TIME_ZONE 设置或当前激活的时区) #}
    <p>Server time in local: {
               { server_datetime_aware|date:"H:i" }}</p>
{% endlocaltime %}

<p>Back to default timezone: {
               { utc_now_variable|date:"Y-m-d H:i:s T" }}</p>

加载自定义模板标签和过滤器:

这是 {% load %} 标签最强大的用途,它允许你扩展 DTL 的能力。

创建自定义标签/过滤器的步骤:

在你的 Django 应用内创建一个 templatetags 目录。
例如,如果你的应用名为 myapp,则创建 myapp/templatetags/

在该目录下创建一个空的 __init__.py 文件,使其成为一个 Python 包。
myapp/templatetags/__init__.py

templatetags 目录下创建一个 Python 文件 来存放你的自定义标签和过滤器,例如 myapp_extras.py
myapp/templatetags/myapp_extras.py

在该 Python 文件中,导入 django.template.Library 并实例化它。

# myapp/templatetags/myapp_extras.py
from django import template
from django.utils.html import format_html
from django.utils.safestring import mark_safe
import datetime

register = template.Library() # 必须将实例命名为 register

使用 @register.filter 装饰器注册自定义过滤器。
过滤器通常接收一个或两个参数:第一个是输入值(被|管道符左边的值),第二个(可选)是过滤器的参数。

# myapp/templatetags/myapp_extras.py
# ... (register = template.Library()) ...

@register.filter(name='cut_string') # 注册名为 cut_string 的过滤器
def cut_string_filter(value, arg):
    """Removes all values of arg from the given string"""
    return value.replace(str(arg), '')

@register.filter
def to_lowercase(value): # 过滤器名将是 to_lowercase
    """Converts a string to all lowercase."""
    return str(value).lower()

@register.filter(is_safe=True) # is_safe=True 表示过滤器不会引入不安全的HTML
def highlight_text(value, query):
    """Highlights occurrences of query in value, marked as safe."""
    # 注意:这个实现非常基础,实际中可能需要更复杂的HTML转义和处理
    # 确保 query 本身是安全的,或者在使用前进行清理
    highlighted_value = str(value).replace(
        str(query),
        f'<strong class="highlight">{
                str(query)}</strong>'
    )
    return mark_safe(highlighted_value) # 使用 mark_safe 告诉 Django 这是安全的 HTML

使用 @register.simple_tag 装饰器注册简单标签。
简单标签可以接受任意数量的位置或关键字参数,并返回一个字符串(可以是HTML,如果返回HTML,需要确保其安全或使用 mark_safe / format_html)。

# myapp/templatetags/myapp_extras.py
# ... (register = template.Library()) ...

@register.simple_tag # 标签名将是 current_time_formatted
def current_time_formatted(format_string="%b %d %Y, %I:%M %p"):
    """Returns the current time formatted according to the given format string."""
    return datetime.datetime.now().strftime(format_string)

@register.simple_tag(takes_context=True) # takes_context=True 使标签可以访问当前模板上下文
def user_greeting_banner(context, greeting_message="Welcome"):
    """Displays a greeting banner with the username from context."""
    user = context.get('user') # 从上下文中获取 user 对象
    username = user.username if user and user.is_authenticated else "Guest"
    banner_html = f'<div class="banner">{
                greeting_message}, {
                username}!</div>'
    return mark_safe(banner_html) # 返回安全的 HTML

使用 @register.inclusion_tag 装饰器注册包含标签。
包含标签会渲染另一个模板,并将标签返回的字典作为该模板的上下文。这对于生成需要独立模板来渲染的复杂HTML片段(如小部件)非常有用。

# myapp/templatetags/myapp_extras.py
# ... (register = template.Library()) ...

@register.inclusion_tag('myapp/includes/_user_profile_card.html', takes_context=True) # 指定要渲染的模板
def show_user_profile_card(context, user_object=None):
    """Renders a user profile card using the provided user_object or user from context."""
    target_user = user_object if user_object else context.get('user')
    if not target_user or not target_user.is_authenticated:
        return {
              'display_card': False} # 如果没有有效用户,可以不显示卡片

    # 准备传递给 _user_profile_card.html 的上下文
    profile_data = {
              
        'display_card': True,
        'username': target_user.username,
        'email': target_user.email,
        'full_name': target_user.get_full_name(),
        'join_date': target_user.date_joined,
        'profile_picture_url': getattr(target_user, 'profile_picture_url', '/static/images/default_avatar.png')
    }
    return profile_data

对应的 myapp/templates/myapp/includes/_user_profile_card.html:

{% if display_card %}
    <div class="user-profile-card">
        <img src="{
             { profile_picture_url }}">{ username }}'s profile picture">
        <h4>{
             { full_name|default:username }}</h4>
        <p>Email: {
             { email }}</p>
        <p>Joined: {
             { join_date|date:"M d, Y" }}</p>
    </div>
{% else %}
    {# <p>User profile card cannot be displayed.</p> #}
{% endif %}

在模板中使用 {% load %} 加载你的自定义库,然后使用你的标签和过滤器。

{% load myapp_extras %} {# 加载 myapp_extras.py 中定义的所有标签和过滤器 #}
{% load static %} {# 也可以同时加载其他库 #}

<p>Original: Some sample text with removable parts.</p>
<p>Cut: {
             { "Some sample text with removable parts."|cut_string:"removable" }}</p> {# 使用 cut_string 过滤器 #}
<p>Lowercase: {
             { "This Should Be Lowercase"|to_lowercase }}</p> {# 使用 to_lowercase 过滤器 #}
<p>Highlighted: {
             { "Search for the word 'Python' in this Python text."|highlight_text:"Python" }}</p>

<p>Current server time: {% current_time_formatted "%Y-%m-%d %H:%M:%S" %}</p> {# 使用 simple_tag,并传递参数 #}
<p>Default time format: {% current_time_formatted %}</p> {# 使用 simple_tag 的默认参数 #}

{% user_greeting_banner "Hello there" %} {# 使用带上下文的 simple_tag #}

{# 使用 inclusion_tag 来显示当前登录用户的卡片 #}
{% show_user_profile_card %}

{# 假设有一个 specific_user 对象,为该用户显示卡片 #}
{% if specific_user_profile %}
    <h4>Profile for {
             { specific_user_profile.username }}:</h4>
    {% show_user_profile_card user_object=specific_user_profile %}
{% endif %}

加载自定义标签/过滤器的注意事项与企业级实践:

应用命名空间: 为了避免不同应用之间的自定义标签/过滤器名称冲突,最佳实践是在你的模板标签模块中只定义特定于该应用的标签。如果需要更通用的标签,可以创建一个共享的“utils”或“core”应用来存放它们。
{% load %} 的位置: 通常将 {% load %} 标签放在模板的顶部,{% extends %} 标签(如果存在)之后。一个 {% load %} 标签可以加载多个库。
自动转义 (Autoescaping):

自定义过滤器:默认情况下,过滤器的输出会经过 Django 的自动HTML转义。如果你的过滤器确定生成的是安全的HTML,并且不应被再次转义,可以在注册时设置 is_safe=True,并确保使用 mark_safe() 标记返回的字符串。
自定义简单标签:默认情况下,简单标签的输出也会被转义。如果返回HTML,使用 django.utils.html.format_html()mark_safe() 来处理。format_html() 更安全,因为它会转义其参数,除非参数本身已被标记为安全。
自定义包含标签: 包含标签渲染的是一个完整的模板,该模板自身的自动转义行为照常生效。

性能:

避免在自定义标签/过滤器中执行耗时的数据库查询或复杂计算,尤其是那些会在循环中被多次调用的。如果需要数据,尽可能在视图中获取并传递到模板上下文。
如果标签需要访问大量数据或进行复杂处理,考虑是否可以通过缓存结果来优化。

可测试性: 自定义模板标签和过滤器是Python代码,应该像其他Python代码一样进行单元测试。你可以直接调用它们(对于过滤器和简单标签),或者使用 Django 的模板渲染API来测试包含标签和更复杂的交互。
文档: 为你的自定义标签和过滤器编写清晰的文档字符串 (docstrings),解释它们的功能、参数和用法。这对于团队协作和长期维护至关重要。
优雅地处理错误: 在自定义标签/过滤器中,考虑可能出现的错误情况(如预期的上下文变量不存在、参数类型错误等),并进行适当的处理(例如,返回空字符串、记录错误日志,或者在开发模式下抛出明确的异常)。

{% load %} 标签是 Django 模板系统灵活性的关键所在,它使得开发者能够根据项目需求无限扩展模板的功能,同时保持模板本身的简洁性和声明性。

4.3.2.7 {% url %} 标签:动态、健壮的URL反向解析

在Web应用中,URL是连接不同页面的纽带。硬编码URL(即直接在模板中写入 /products/3/ 这样的路径)是一种非常脆弱的做法。如果将来URL结构发生变化(例如,/products/3/ 变成 /items/3/ 或者 /shop/category/product/3/),所有硬编码的地方都需要手动修改,这既耗时又容易出错。

Django 的URL反向解析机制,以及在模板中与之对应的 {% url %} 标签,完美地解决了这个问题。它允许你通过在 urls.py 中为URL模式命名的名称来动态生成URL。

核心思想:

urls.py 中为URL模式命名: 当使用 path()re_path() 定义URL模式时,通过 name 参数为其指定一个唯一的名称。

# myapp/urls.py
from django.urls import path
from . import views

app_name = 'myapp' # 应用命名空间 (推荐)

urlpatterns = [
    path('home/', views.home_view, name='home'), # 名为 'home'
    path('articles/<int:year>/<int:month>/', views.article_archive, name='article_archive_monthly'), # 名为 'article_archive_monthly'
    path('product/<slug:product_slug>/', views.product_detail, name='product_detail'), # 名为 'product_detail'
    path('profile/<str:username>/', views.user_profile, name='user_profile'),
]

在模板中使用 {% url %} 标签通过名称引用URL: {% url %} 标签会查找与给定名称匹配的URL模式,并根据需要填充参数,最终生成正确的URL字符串。

基本语法:

{% url 'url_name_string' [arg1 arg2 ...] [kwarg1=value1 kwarg2=value2 ...] %}

'url_name_string' (必需):

一个字符串,表示在 urls.py 中定义的URL模式的 name
如果使用了应用命名空间 (在应用的 urls.py 中定义了 app_name),则名称应该是 'app_name:url_name' 的形式,例如 'myapp:home'
如果使用了实例命名空间 (在项目 urls.pyinclude() 时指定了 namespace),则名称可能是 'instance_name:app_name:url_name''instance_name:url_name' (如果应用内部没有 app_name)。

[arg1 arg2 ...] (可选):

传递给URL模式的位置参数。这些参数会按照顺序填充URL模式中未命名的捕获组。
例如,对于 path('articles/<int:year>/<int:month>/', ...)yearmonth 就是位置参数。

[kwarg1=value1 ...] (可选):

传递给URL模式的关键字参数。这些参数会填充URL模式中命名的捕获组。
例如,对于 path('product/<slug:product_slug>/', ...)product_slug 就是一个命名的捕获组,可以通过关键字参数传递。

{% url %} 的行为:

Django 的模板引擎解析到 {% url %} 标签。
它会根据提供的 url_name_string 和参数,在已加载的URL配置中查找匹配的URL模式。
如果找到匹配的模式,并且提供的参数与模式期望的参数相符(类型和数量),Django 就会使用这些参数来构建完整的URL路径。
如果找不到匹配的模式,或者参数不匹配,Django 会抛出 NoReverseMatch 异常。

示例与详细解释:

假设我们有以下 myapp/urls.py:

# myapp/urls.py
from django.urls import path
from . import views

app_name = 'myapp' # 应用命名空间

urlpatterns = [
    path('', views.index_view, name='index'), # 首页,无参数
    path('contact-us/', views.contact_view, name='contact'), # 联系页面,无参数
    path('item/<int:item_id>/details/', views.item_detail_view, name='item_detail'), # 带一个整数参数 item_id
    path('category/<slug:category_slug>/product/<uuid:product_uuid>/', views.category_product_view, name='category_product'), # 带 slug 和 uuid 参数
    path('search/', views.search_view, name='search_results'), # 用于处理带查询参数的URL,但URL本身无路径参数
]

以及项目 project/urls.py:

# project/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('app/', include('myapp.urls', namespace='main_app')), # 包含 myapp.urls 并指定实例命名空间 'main_app'
    # 假设还有一个没有命名空间的 include
    # path('legacy/', include('legacy_app.urls')),
]

在模板中使用 {% url %}:

{# 假设在 myapp 应用的模板中 #}

{# 1. 无参数的URL #}
<a href="{% url 'myapp:index' %}">返回首页 (myapp)</a> {# 正确引用 myapp:index #}
<a href="{% url 'main_app:contact' %}">联系我们 (main_app:contact)</a> {# 通过实例和应用命名空间引用 #}

{# 2. 带位置参数的URL #}
{% with item_object_id=123 %} {# 假设 item_object_id 是一个变量 #}
    <a href="{% url 'myapp:item_detail' item_object_id %}">查看商品 {
           { item_object_id }} 详情 (位置参数)</a>
    {# Django 会生成类似 /app/item/123/details/ 的URL (取决于 project/urls.py 中的 'app/' 前缀) #}
{% endwith %}

{# 3. 带关键字参数的URL #}
{% with current_item_id=456 %}
    <a href="{% url 'myapp:item_detail' item_id=current_item_id %}">查看商品 {
           { current_item_id }} 详情 (关键字参数)</a>
    {# 效果同上,推荐使用关键字参数,更清晰 #}
{% endwith %}

{# 4. 带多个关键字参数的URL #}
{% with current_category="electronics" current_product_guid="a1b2c3d4-e5f6-7890-1234-567890abcdef" %}
    <a href="{% url 'myapp:category_product' category_slug=current_category product_uuid=current_product_guid %}">
        查看 {
           { current_category }} 类别的产品 {
           { current_product_guid }}
    </a>
    {# 会生成类似 /app/category/electronics/product/a1b2c3d4-e5f6-7890-1234-567890abcdef/ 的URL #}
{% endwith %}

{# 5. 混合使用位置参数和关键字参数 (不推荐,容易混淆,但技术上可行,只要不冲突) #}
{# 对于 path('archive/<int:year>/<slug:month_slug>/', views.archive, name='archive_by_month') #}
{# {% url 'myapp:archive_by_month' 2024 month_slug='jan' %}  (位置参数优先匹配未命名组或按顺序匹配命名组) #}
{# {% url 'myapp:archive_by_month' year=2024 'jan' %} (如果可以,不推荐) #}
{# 最佳实践是:如果URL模式中的所有捕获组都是命名的,则始终使用关键字参数传递给 {% url %}。如果都是未命名的,则使用位置参数。 #}

{# 6. URL 不直接接收路径参数,但可能用于构建带查询字符串的链接 #}
{# {% url %} 只负责生成 path 部分。查询字符串 (?key=value) 需要手动附加或通过其他方式。 #}
<form action="{% url 'myapp:search_results' %}" method="get"> {# 生成 /app/search/ #}
    <input type="text" name="q" placeholder="搜索...">
    <button type="submit">搜索</button>
</form>

{# 7. 处理实例命名空间 #}
{# 如果在 project/urls.py 中是这样 include 的: path('app/', include('myapp.urls', namespace='main_app')) #}
{# 那么在模板中引用 myapp 下的URL时,需要带上实例命名空间 #}
<a href="{% url 'main_app:index' %}">首页 (通过实例 'main_app' 和应用 'myapp' (隐式) )</a>
{# 如果 myapp/urls.py 中没有 app_name,则为: {% url 'main_app:index' %} #}
{# 如果 myapp/urls.py 中有 app_name='myapp',则为: {% url 'main_app:myapp:index' %} 这种用法较少见,通常app_name足够 #}
{# 最常见的是 {% url 'app_namespace:view_name' %} 或 {% url 'instance_namespace:view_name' %} (如果应用内部没有app_namespace) #}
{# 或者 {% url 'instance_namespace:app_namespace:view_name' %} (如果两者都有且需要明确) #}
{# Django 会尝试解析,通常 'instance_namespace:view_name' 或 'app_namespace:view_name' 能够工作,具体取决于你的URL配置的复杂程度。 #}
{# 推荐:应用内部使用 app_name,在项目级 include 时,如果同一个 app 被多次 include,则使用 instance namespace。 #}
{# 引用时,优先使用 'app_name:view_name'。如果存在实例命名空间,则 'instance_name:view_name'。 #}

{# 假设一个URL的name是全局唯一的 (不推荐,容易冲突) #}
{# 如果 urls.py 是 path('global-unique-url/', views.some_view, name='global_view_name') #}
{# {% url 'global_view_name' %} #}

代码解释:

{% url 'myapp:index' %}:

myapp: 指的是 myapp/urls.py 中定义的 app_name
index: 指的是该 app_name 空间下名为 index 的URL模式。

{% url 'myapp:item_detail' item_object_id %}:

item_object_id (变量): 作为位置参数传递给名为 item_detail 的URL。它会填充 item_detail URL模式中的第一个捕获组(即 <int:item_id>)。

{% url 'myapp:item_detail' item_id=current_item_id %}:

item_id=current_item_id: 使用关键字参数 item_id 来填充URL模式中名为 item_id 的捕获组。这种方式更具可读性和健壮性,因为参数顺序无关紧要。

{% url 'main_app:contact' %}:

main_app: 指的是在项目 urls.pyinclude('myapp.urls', namespace='main_app') 时指定的实例命名空间。
contact: 指的是 myapp 应用内部(其 app_name 可能是 myapp 或未定义,但这里通过实例命名空间访问)名为 contact 的URL。如果 myapp.urls 中有 app_name='myapp',那么更精确的写法可能是 {% url 'main_app:myapp:contact' %},但Django的解析器通常能智能处理。优先使用更简洁且能工作的形式。

as 关键字:将URL存储在变量中

有时,你可能需要多次使用同一个生成的URL,或者需要在 {% if %} 等逻辑标签中使用它,或者只是为了提高可读性。{% url %} 标签支持使用 as 将生成的URL存储在一个模板变量中,而不是直接输出。

{% url 'myapp:product_detail' product_slug=product.slug as detail_url %} {# 将生成的URL存储在 detail_url 变量中 #}

<a href="{
           { detail_url }}">查看 {
           { product.name }}</a>

{% if user.is_staff %}
    <a href="{
           { detail_url }}?edit=true">编辑 (员工)</a> {# 复用 detail_url 并添加查询参数 #}
{% endif %}

<meta property="og:url" content="{% get_current_site %}{
           { detail_url }}"> {# 在 meta 标签中使用 #}

{% comment %}
   假设有一个复杂的URL,多次在不同地方使用,
   或者需要传递给一个 include 的子模板。
{% endcomment %}
{% url 'myapp:category_product' category_slug="laptops" product_uuid=laptop_product.uuid as laptop_product_url %}

<div class="main-link">
    <a href="{
           { laptop_product_url }}">主要链接到笔记本电脑</a>
</div>

{% include "myapp/includes/share_buttons.html" with share_url=laptop_product_url only %}

代码解释:

{% url 'myapp:product_detail' product_slug=product.slug as detail_url %}:

as detail_url: 这一部分告诉模板引擎不要立即输出URL,而是将其值赋给名为 detail_url 的新模板变量。
之后,你就可以在模板的其他地方通过 {
{ detail_url }}
来使用这个已生成的URL。

{% url %} 的高级应用与企业级考量:

动态导航菜单:
在基础模板或可包含的导航模板中,使用 {% url %} 生成菜单项的链接,确保即使URL结构调整,导航也能正确工作。

{# _navigation.html #}
<nav>
    <ul>
        <li><a href="{% url 'main_app:index' %}" class="{% if request.resolver_match.view_name == 'main_app:index' %}active{% endif %}">首页</a></li> {# request.resolver_match.view_name 可以用于判断当前页面是否匹配某个URL名称 #}
        <li><a href="{% url 'main_app:contact' %}" class="{% if request.resolver_match.view_name == 'main_app:contact' %}active{% endif %}">联系</a></li>
        {% if user.is_authenticated %}
            <li><a href="{% url 'accounts:profile' username=user.username %}">我的账户</a></li> {# 假设有 'accounts' 应用和对应的URL #}
            <li><a href="{% url 'accounts:logout' %}">登出</a></li>
        {% else %}
            <li><a href="{% url 'accounts:login' %}?next={
             { request.path|urlencode }}">登录</a></li> {# 登录后跳转回当前页 #}
        {% endif %}
    </ul>
</nav>

request.resolver_match.view_name (或 request.resolver_match.url_name, request.resolver_match.app_name, request.resolver_match.namespace) 可以用来判断当前渲染的页面对应哪个URL名称/命名空间,从而高亮活动菜单项。

表单的 action 属性:
始终使用 {% url %} 来设置表单的 action URL。

<form method="post" action="{% url 'myapp:submit_feedback' %}">
    {% csrf_token %}
    {
             { form.as_p }}
    <button type="submit">提交反馈</button>
</form>

重定向URL:
虽然重定向主要在视图中通过 redirect('app_name:url_name', ...) 完成,但在某些情况下(例如,JavaScript中需要一个URL,或者在模板中构建一个包含 next 参数的URL),{% url %} 非常有用。

<a href="{% url 'accounts:login' %}?next={% url 'myapp:user_dashboard' as dashboard_url %}{
             { dashboard_url|urlencode }}">
    登录后访问仪表盘
</a>

这里,dashboard_url 被urlencode以确保它可以安全地作为查询参数的值。

处理 NoReverseMatch 异常:

常见原因:

URL名称拼写错误或命名空间错误。
传递的参数数量或类型与URL模式定义不符。
URL模式需要的某个参数没有被提供。
相关的 urls.py 没有被正确加载(例如,应用未在 INSTALLED_APPS 中,或者项目 urls.py 没有 include 它)。

调试: Django 的 NoReverseMatch 错误信息通常非常详细,会列出它尝试匹配的参数和所有已知的URL模式,仔细阅读错误信息是解决问题的关键。
预防:

使用一致的命名约定。
urls.py 中对所有重要的URL模式进行命名。
使用关键字参数向 {% url %} 传递参数,以减少顺序错误。
编写测试用例来验证URL反向解析是否按预期工作 (使用 Django 的 reverse() 函数在测试中)。

与绝对URL和 get_absolute_url() 方法:

Django模型通常会定义一个 get_absolute_url() 方法,该方法使用 reverse() (Python代码中的对应物) 来返回该模型实例的规范URL。

# models.py
from django.db import models
from django.urls import reverse

class Product(models.Model):
    name = models.CharField(max_length=100)
    slug = models.SlugField(unique=True)
    # ... other fields ...

    def get_absolute_url(self):
        return reverse('myapp:product_detail', kwargs={
                'product_slug': self.slug})

在模板中,你可以直接调用这个方法:

<a href="{
               { product_instance.get_absolute_url }}">查看 {
               { product_instance.name }}</a>

这通常比在模板中每次都使用 {% url %} 并传递参数更简洁,尤其是对于模型实例的详情页。get_absolute_url() 将URL构建逻辑封装在了模型内部。

国际化URL (i18n URL patterns):
如果你的项目使用了 Django 的国际化URL功能 (django.conf.urls.i18n.i18n_patterns),{% url %} 标签会自动处理当前激活的语言,并生成相应语言版本的URL。

{% url %} 标签是 Django 模板系统中一个不可或缺的工具,它通过解耦模板中的链接与项目的URL配置,极大地增强了项目的可维护性和灵活性。

4.3.2.8 {% csrf_token %} 标签:保护表单免受CSRF攻击

跨站请求伪造 (Cross-Site Request Forgery, CSRF) 是一种常见的网络攻击,攻击者诱骗已认证用户在他们当前已登录的Web应用上执行非本意的操作。例如,用户登录了银行网站,然后访问了一个恶意网站,该恶意网站可能包含一个自动提交到银行网站转账表单的隐藏表单。

Django 内置了强大的CSRF保护机制,而 {% csrf_token %} 标签是前端(模板)实现这一保护的关键部分。

工作机制:

中间件: Django 的 django.middleware.csrf.CsrfViewMiddleware 在处理响应时,会为每个用户会话生成一个唯一的、随机的CSRF令牌 (token)。
{% csrf_token %} 标签: 当你在模板的表单中包含此标签时,它会渲染成一个隐藏的HTML输入字段 (<input type="hidden">),其名称通常是 csrfmiddlewaretoken,值为当前会话的CSRF令牌。

<input type="hidden" name="csrfmiddlewaretoken" value="a_long_random_looking_string_here">

请求验证: 当用户通过POST方法提交这个表单时:

CsrfViewMiddleware 会拦截所有POST, PUT, DELETE(以及其他非安全的HTTP方法)请求。
它会查找请求数据中的 csrfmiddlewaretoken 参数(或者HTTP头部 X-CSRFToken,用于AJAX)。
它会将提交的令牌与用户会话中存储的令牌(或cookie中的令牌,取决于配置)进行比较。
如果令牌匹配,请求被允许继续处理。
如果令牌缺失或不匹配,中间件会拒绝该请求,通常返回一个HTTP 403 Forbidden(禁止访问)错误。

基本语法与用法:

<form method="post" action="{% url 'some_app:some_action' %}">
    {% csrf_token %} {# 必须放在 <form> 标签内部 #}

    {# ... 其他表单字段 ... #}
    {
           { form.as_p }}

    <button type="submit">提交</button>
</form>

位置: {% csrf_token %} 标签必须放置在任何使用POST方法向你的Django应用提交数据的 <form> 标签内部。
HTTP方法: CSRF保护主要针对会修改数据的HTTP方法,如POST, PUT, DELETE。对于GET, HEAD, OPTIONS, TRACE等安全方法,通常不需要CSRF令牌,因为它们不应有副作用。

何时使用 {% csrf_token %}

任何时候你创建一个HTML表单,其 method 属性为 post (或其他非安全方法),并且该表单的目标URL是你的Django应用内部的视图,都应该包含 {% csrf_token %}
即使表单是通过JavaScript(例如AJAX)以POST方式提交的,也需要CSRF令牌。对于AJAX,令牌通常不是通过隐藏字段发送,而是通过自定义的HTTP头部(如 X-CSRFToken)发送。Django提供了获取令牌并在AJAX请求中使用的机制。

示例:一个简单的POST表单

{# template_with_form.html #}
<h2>提交您的反馈</h2>
<form method="post" action="{% url 'myapp:submit_feedback' %}"> {# 表单提交到 myapp:submit_feedback URL #}
    {% csrf_token %} {# 插入CSRF令牌隐藏字段 #}

    <label for="id_name">您的名字:</label>
    <input type="text" name="name" required>
    <br>
    <label for="id_feedback">反馈内容:</label>
    <textarea name="feedback_text" rows="4" required></textarea>
    <br>
    <button type="submit">发送反馈</button>
</form>

{% if submitted_feedback %} {# 假设视图在成功后传递此变量 #}
    <p>感谢您的反馈:“{
           { submitted_feedback }}”</p>
{% endif %}
# myapp/views.py
from django.shortcuts import render
from django.http import HttpResponse

# 假设有一个简单的模型或存储反馈的地方
feedback_storage = []

def submit_feedback_view(request):
    submitted_text = None
    if request.method == 'POST':
        # CSRF验证已由 CsrfViewMiddleware 在此之前自动完成
        # 如果CSRF验证失败,视图代码根本不会执行,会直接返回403
        name = request.POST.get('name', '匿名')
        feedback = request.POST.get('feedback_text', '')
        if feedback:
            full_feedback = f"{
              name} 说: {
              feedback}"
            feedback_storage.append(full_feedback)
            submitted_text = feedback # 用于在模板中显示
            print(f"收到反馈: {
              full_feedback}") # 在服务器端打印
        # 通常这里会进行表单验证、数据保存等操作,然后重定向
        # return redirect('myapp:feedback_success')
    return render(request, 'template_with_form.html', {
            'submitted_feedback': submitted_text})

代码解释:

template_with_form.html 中,{% csrf_token %} 被放置在 <form> 标签内。当用户提交表单时,浏览器会包含名为 csrfmiddlewaretoken 的隐藏字段及其值。
submit_feedback_view 视图中,我们不需要显式检查CSRF令牌。CsrfViewMiddleware 在视图代码执行之前就已经完成了这个工作。如果令牌无效或缺失,用户会看到403错误页面。

AJAX请求中的CSRF保护:

对于使用JavaScript(如jQuery AJAX, Fetch API, Axios等)发起的POST请求,不能直接使用 {% csrf_token %} 标签(因为它生成的是HTML字段)。你需要从DOM中获取CSRF令牌的值,并将其包含在AJAX请求的HTTP头部中。

Django官方文档推荐的方法是:

在模板中确保CSRF cookie (csrftoken) 已设置。通常 CsrfViewMiddleware 会处理。

或者,在某个地方(如 base.html)输出CSRF令牌供JavaScript读取 (不常用,cookie方式更佳)。

{# <script>const csrfToken = "{
             { csrf_token }}";</script>  <-- 这样不行,csrf_token 是一个标签,不是直接的值 #}
{# 正确做法是从cookie或DOM中已有的 input[name=csrfmiddlewaretoken] 获取 #}

JavaScript获取CSRF令牌值:
通常是从名为 csrftoken 的cookie中获取。

function getCookie(name) {
              
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
              
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
              
            const cookie = cookies[i].trim();
            // Does this cookie string begin with the name we want?
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
              
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
}
const csrftoken = getCookie('csrftoken'); // 获取名为 'csrftoken' 的cookie值

在AJAX请求中设置 X-CSRFToken 头部:

// 使用 Fetch API 示例
fetch("{% url 'myapp:ajax_action' %}", {
               // 使用 {% url %} 生成AJAX请求的目标URL
    method: 'POST',
    headers: {
              
        'Content-Type': 'application/json',
        'X-CSRFToken': csrftoken, // 设置 X-CSRFToken 头部
        // 'X-Requested-With': 'XMLHttpRequest' // 有时也需要此头部,表明是AJAX请求
    },
    body: JSON.stringify({
               key: 'value' })
})
.then(response => {
              
    if (!response.ok) {
              
        if (response.status === 403) {
              
            console.error("CSRF verification failed or other permission issue.");
        }
        throw new Error('Network response was not ok ' + response.statusText);
    }
    return response.json();
})
.then(data => {
              
    console.log('Success:', data);
})
.catch((error) => {
              
    console.error('Error:', error);
});

// 使用 jQuery AJAX 示例
/*
$.ajax({
    url: "{% url 'myapp:ajax_action' %}",
    type: "POST",
    data: { my_data: "some value" },
    beforeSend: function(xhr) {
        xhr.setRequestHeader("X-CSRFToken", csrftoken);
    },
    success: function(response) {
        console.log("jQuery AJAX success:", response);
    },
    error: function(xhr, errmsg, err) {
        console.log("jQuery AJAX error:", xhr.status + ": " + xhr.responseText);
        if (xhr.status === 403) {
            alert("CSRF token error or permission denied.");
        }
    }
});
*/

Django的 CsrfViewMiddleware 会自动检查 X-CSRFToken HTTP头部。

企业级考量与安全注意事项:

始终启用 CsrfViewMiddleware: 确保 django.middleware.csrf.CsrfViewMiddleware 在你的 MIDDLEWARE 设置中,并且位于任何可能修改响应(如设置cookie)的中间件之后,但在任何假设CSRF攻击已被处理的视图中间件(如 @csrf_exempt 相关的)之前。

HTTPS: CSRF保护本身并不能防止中间人攻击(Man-in-the-Middle, MITM)。始终在生产环境中使用HTTPS来保护CSRF令牌和整个通信过程不被窃听或篡改。

@csrf_exempt 装饰器:

在极少数情况下,你可能需要对某个特定视图禁用CSRF保护(例如,该视图接收来自不受你控制的外部服务的POST请求,这些服务无法发送CSRF令牌)。这时可以使用 @csrf_exempt 装饰器。

from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse

@csrf_exempt # 对此视图禁用CSRF保护
def external_webhook_receiver(request):
    if request.method == 'POST':
        # 处理来自外部服务的POST数据
        # 注意:由于禁用了CSRF,需要其他方式验证请求的合法性,如签名、IP白名单等
        return HttpResponse("Webhook received.", status=200)
    return HttpResponse("Please POST data.", status=400)

警告: 滥用 @csrf_exempt 会使你的应用易受CSRF攻击。仅在绝对必要且有其他安全措施替代时使用。

@csrf_protect 装饰器:
如果 CsrfViewMiddleware 没有全局启用(不推荐),或者你想在默认豁免的视图(例如通过 @csrf_exempt 豁免了某个类的所有方法,但想保护其中一个)上强制执行CSRF检查,可以使用 @csrf_protect

@ensure_csrf_cookie 装饰器:
如果你的表单是通过JavaScript动态添加到页面的,并且页面最初没有包含 {% csrf_token %} 的表单,那么CSRF cookie 可能不会被发送给客户端。在这种情况下,可以在提供该页面的视图上使用 @ensure_csrf_cookie 装饰器,它会确保 CsrfViewMiddleware 发送CSRF cookie,即使模板中没有 {% csrf_token %} 标签。这对于大量使用AJAX和SPA(单页应用)的场景尤其重要。

from django.views.decorators.csrf import ensure_csrf_cookie
from django.shortcuts import render

@ensure_csrf_cookie # 确保CSRF cookie被发送
def spa_entry_point_view(request):
    # 这个视图渲染一个单页应用的骨架,其中表单和AJAX请求由JS处理
    return render(request, 'spa_base.html')

Cookie配置:

CSRF_COOKIE_SECURE: 在生产环境中应设置为 True,确保CSRF cookie只通过HTTPS发送。
CSRF_COOKIE_HTTPONLY: 默认False。如果设为True,JavaScript将无法通过 document.cookie 读取CSRF令牌cookie,这时需要通过其他方式(如在页面某处渲染令牌值供JS读取)来处理AJAX的CSRF。通常保持False以方便JS读取cookie是可行的,因为CSRF令牌的主要目的是防止伪造请求,而不是防止令牌本身被(同源的)JS读取。
CSRF_COOKIE_SAMESITE: 设置为 'Lax' (默认) 或 'Strict' 可以提供额外的保护,防止CSRF令牌在跨站请求中被发送。

子域名和CSRF: 如果你的应用涉及多个子域名,并且需要在它们之间共享CSRF令牌(例如,表单在一个子域名,提交到另一个子域名),你需要仔细配置 CSRF_COOKIE_DOMAIN

{% csrf_token %} 及其背后的机制是 Django 安全体系的重要组成部分。正确使用它可以有效地抵御一种常见的Web漏洞,保护用户数据和应用状态不被恶意篡改。

4.3.2.9 {# ... #}{% comment %} 标签:模板中的注释

在编写模板时,添加注释是一种良好的实践,它可以帮助解释复杂的逻辑、记录待办事项、或者临时禁用某段代码。DTL 提供了两种主要的注释方式。

1. 单行/行内注释:{# ... #}

这是最常用的注释方式,类似于Python中的 # 注释。

语法:

{# 这是一个单行注释,它不会被渲染到最终的HTML输出中 #}

<p>Hello, {
           { user.name }}! {# 显示用户名 #}</p>

{#
    这也可以看作是一个多行注释,
    只要所有内容都在 {# 和 #} 之间。
    每一行都会被忽略。
#}

{% if user.is_authenticated %} {# 检查用户是否登录 #}
    {# 用户已登录,显示欢迎信息 #}
    <p>Welcome back!</p>
{% else %}
    {# 用户未登录,显示登录链接 #}
    <a href="{% url 'login' %}">Login</a>
{% endif %}

特点:

{##} 包围的任何内容(包括换行符)都会被模板引擎忽略,不会出现在最终渲染的HTML中。
它们对模板的渲染逻辑没有影响。
可以用于注释单行代码、解释变量用途、或者在标签旁边添加简短说明。

2. 块级注释:{% comment %}{% endcomment %}

当需要注释掉一大段模板代码(可能包含多个标签、HTML元素和变量)时,{% comment %} 块标签非常有用。

语法:

{% comment %}
    这里是块级注释的开始。
    下面这段代码将被完全忽略,直到 {% endcomment %} 标签出现。

    <div class="old-feature">
        <h3>旧功能标题</h3>
        <p>旧功能的描述: {
           { old_data.description }}</p>
        {% for item in old_data.items %}
            <li>{
           { item }}</li>
        {% endfor %}
    </div>

    这个注释可以跨越多行,并且可以包含其他的Django模板标签、HTML等,
    它们都不会被解析或渲染。
{% endcomment %}

<p>这部分内容会正常渲染。</p>

{# 你也可以给 comment 标签一个可选的 "note" 参数,它会被忽略,但可以用于标记注释的目的 #}
{% comment "Temporary removal of experimental feature for v2.1 release" %}
    <section class="experimental-feature">
        {% if flags.enable_beta_feature_X %}
            <h4>实验性特性 X</h4>
            <!-- 此处是 HTML 注释,会被发送到浏览器 -->
            <p>此特性仍在测试中。</p>
        {% endif %}
    </section>
{% endcomment %}

特点:

{% comment %}{% endcomment %} 之间的所有内容都会被模板引擎忽略。
可选的 “note” 参数(一个字符串)可以放在 {% comment %} 标签后,例如 {% comment "Refactoring this section later" %}。这个 note 本身不会被渲染,仅作为开发者阅读模板时的提示。
非常适合临时禁用大块功能或旧代码,而无需逐行删除或用 {# ... #} 包裹每一行。

与HTML注释 <!-- ... --> 的区别:

DTL注释 ({# ... #}{% comment %}):

由Django模板引擎在服务器端处理并移除。
不会出现在发送给客户端(浏览器)的最终HTML源代码中。
用于开发者内部沟通、解释模板逻辑、临时移除代码等。

HTML注释 (<!-- ... -->):

包含在发送给客户端的HTML源代码中。
浏览器在渲染页面时会忽略它们,但用户可以通过“查看源代码”看到它们。
通常用于注释HTML结构,或者提供给可能直接查看HTML源(如其他开发者、SEO工具)的信息。
不应该用于包含敏感信息或服务器端逻辑的注释。

示例:比较不同类型的注释

{# DTL单行注释:解释下面的变量 #}
{% with company_name="ACME Corp" %}
    <h1>Welcome to {
           { company_name }}</h1> {# company_name在块内可用 #}

    <!-- HTML注释:这个标题对SEO很重要 -->
    <h2>Our Products</h2>

    {% comment "TODO: Implement dynamic product listing from database in v2" %}
        <div class="product-list-placeholder">
            <p>产品列表将显示在此处。</p>
            {# DTL注释:当前是静态占位符 #}
            <!-- HTML注释:产品项将会是<li>元素 -->
        </div>
    {% endcomment %}

    <p>Current year: {% now "Y" %}</p> {# 使用 now 标签获取年份 #}
    {# {% include "old_footer.html" %} <-- 使用DTL注释临时禁用include #}
{% endwith %}

代码解释:

{# DTL单行注释:解释下面的变量 #}{# company_name在块内可用 #} 以及 {# DTL注释:当前是静态占位符 #}{# 使用DTL注释临时禁用include #} 这些都是DTL注释,它们不会出现在发送给浏览器的HTML中。
<!-- HTML注释:这个标题对SEO很重要 --><!-- HTML注释:产品项将会是<li>元素 --> 这些是HTML注释,它们会出现在浏览器接收到的HTML源代码中,但不会在页面上显示。
{% comment "TODO: Implement dynamic product listing from database in v2" %} ... {% endcomment %} 这整个块及其包含的所有内容(包括内部的DTL注释和HTML注释)都不会被Django处理或渲染。

企业级应用场景与最佳实践:

解释复杂逻辑:
如果模板中有复杂的 {% if %} 条件链、嵌套循环或使用了多个自定义标签和过滤器,使用 {# ... #} 注释来解释其目的和行为。

{# 根据用户角色和订阅状态决定显示哪个仪表盘版本 #}
{% if user.is_superuser %}
    {% include "dashboards/admin_dashboard.html" %}
{% elif user.is_authenticated and user.subscription.is_active and user.subscription.level == "premium" %}
    {# 高级用户显示完整功能的仪表盘 #}
    {% include "dashboards/premium_dashboard.html" with stats=user.get_premium_stats %}
{% elif user.is_authenticated %}
    {# 普通登录用户显示基础仪表盘 #}
    {% include "dashboards/basic_dashboard.html" %}
{% else %}
    {# 未登录用户提示 #}
    <p>请登录查看您的仪表盘。</p>
{% endif %}

标记TODO和FIXME:
在模板中留下待办事项或需要修复的问题的标记。

{# FIXME: 这里的价格显示没有考虑税费,需要在视图中处理或使用过滤器 (JIRA-123) #}
<p>Price: {
             { product.price }}</p>

{# TODO: 添加用户头像上传功能 (v2.1) #}
<img src="{
             { user.profile.avatar_url|default:'/static/images/default_avatar.png' }}">

结合团队的规范,这些标记可以帮助追踪开发进度。

临时禁用代码/特性:
{% comment %} 块是A/B测试、逐步推出特性或在调试时临时移除某部分UI的理想选择。

{% comment "A/B Test: Variant B for new checkout button - disabled for now" %}
    <button class="checkout-button-variant-b">Proceed to Checkout (New)</button>
{% endcomment %}
<button class="checkout-button-variant-a">Checkout</button>

版本控制与历史注释 (谨慎使用):
虽然版本控制系统(如Git)是追踪代码变更历史的最佳工具,但有时在模板中保留一些关于重大变更的简短注释可能对快速理解有帮助。

{% comment %}
    v1.0 (2022-01-15): 初始版本
    v1.5 (2023-03-10): 重构了导航栏,使用了新的 include 片段 _navbar_v2.html
                       旧导航 {% include "_navbar_v1.html" %} 已被移除。
{% endcomment %}

但过度依赖这种方式会使模板变得混乱,应优先使用Git的提交信息和标签。

不要在DTL注释中暴露敏感信息:
虽然DTL注释不会发送到客户端,但它们仍然存在于服务器上的模板文件中。团队成员、有服务器文件访问权限的人可以看到它们。因此,不应在注释中包含密码、私钥、或非常详细的内部系统架构等敏感信息。

保持注释的更新:
过时的注释比没有注释更糟糕,因为它会误导开发者。在修改模板逻辑时,务必同时更新相关的注释。

通过合理使用 {# ... #}{% comment %},可以使你的Django模板更易于理解、维护和团队协作。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容