Django模板系统架构、Context处理器与渲染机制深度解析

本文深入剖析Django模板系统的核心机制,涵盖模板架构、语法体系、内置标签与过滤器、Context处理器、自定义组件及渲染机制,帮助开发者掌握模板层的核心开发技能。


一、模板系统架构概述

1.1 模板系统核心概念

Django模板系统是一个强大的文本渲染引擎,负责将动态数据与静态模板结合生成最终的HTML输出。

组件关系

模板系统架构

TEMPLATES配置

模板引擎

模板加载器

模板文件

上下文数据

渲染器

HTML输出

DjangoTemplates

模板解析器

节点树

渲染器

1.2 模板配置详解

# settings.py
TEMPLATES = [
    {
        # 模板引擎后端
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        
        # 模板目录
        'DIRS': [
            BASE_DIR / 'templates',
        ],
        
        # 是否在应用目录中查找模板
        'APP_DIRS': True,
        
        # 模板选项
        'OPTIONS': {
            # Context处理器
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
            
            # 调试模式
            'debug': DEBUG,
            
            # 自动转义
            'autoescape': True,
            
            # 无效变量显示值
            'string_if_invalid': '',
            
            # 文件字符集
            'file_charset': 'utf-8',
        },
    },
]

1.3 核心类关系

creates

uses

contains

Engine

+dirs: list

+app_dirs: bool

+template_loaders: list

+render_to_string()

+get_template()

Template

+template: Origin

+backend: Engine

+render(context)

+source()

Context

+dicts: list

+autoescape: bool

+current_app: str

+use_l10n: bool

+getitem()

+setitem()

+update()

+flatten()

«abstract»

Node

+render(context)

+render_annotated(context)

TextNode

+s: str

VariableNode

+filter_expression: str

ForNode

+loopvars: list

+nodelist: list

IfNode

+conditions_nodelists: list


二、模板语法体系详解

2.1 变量语法

<!-- 基本变量 -->
<h1>{{ title }}</h1>

<!-- 属性访问 -->
<p>{{ user.username }}</p>
<p>{{ article.author.name }}</p>

<!-- 方法调用 -->
<p>{{ user.get_full_name }}</p>
<p>{{ user.get_age }}</p>

<!-- 字典访问 -->
<p>{{ data.key }}</p>
<p>{{ data.items.0 }}</p>

<!-- 列表索引 -->
<p>{{ items.0 }}</p>
<p>{{ items|first }}</p>

2.2 标签语法

<!-- 注释 -->
{# 这是注释,不会显示 #}

<!-- for循环 -->
{% for article in articles %}
    <div class="article">
        <h2>{{ article.title }}</h2>
        <p>{{ article.summary }}</p>
    </div>
{% empty %}
    <p>暂无文章</p>
{% endfor %}

<!-- if条件 -->
{% if user.is_authenticated %}
    <p>欢迎,{{ user.username }}</p>
{% elif user.is_guest %}
    <p>访客用户</p>
{% else %}
    <p>请登录</p>
{% endif %}

<!-- block块 -->
{% block content %}
    <p>默认内容</p>
{% endblock %}

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

<!-- include包含 -->
{% include "sidebar.html" %}

<!-- with上下文 -->
{% with title="页面标题" %}
    <h1>{{ title }}</h1>
{% endwith %}

<!-- url反向解析 -->
<a href="{% url 'article_detail' article.id %}">{{ article.title }}</a>

<!-- load加载标签库 -->
{% load static i18n %}

2.3 过滤器语法

<!-- 基本过滤器 -->
<p>{{ name|upper }}</p>
<p>{{ name|lower }}</p>
<p>{{ name|title }}</p>

<!-- 链式过滤器 -->
<p>{{ text|lower|truncate:50 }}</p>

<!-- 带参数的过滤器 -->
<p>{{ price|floatformat:2 }}</p>
<p>{{ date|date:"Y-m-d" }}</p>

<!-- 默认值 -->
<p>{{ name|default:"匿名" }}</p>

<!-- 安全转义 -->
<p>{{ html|safe }}</p>
<p>{{ html|escape }}</p>

三、内置标签详解

3.1 控制流标签

<!-- for标签 -->
{% for item in items %}
    <p>{{ forloop.counter }}: {{ item.name }}</p>
    
    <!-- forloop变量 -->
    <p>索引: {{ forloop.counter0 }}</p>      <!-- 从0开始 -->
    <p>序号: {{ forloop.counter }}</p>       <!-- 从1开始 -->
    <p>是否第一: {{ forloop.first }}</p>
    <p>是否最后: {{ forloop.last }}</p>
    <p>父循环计数: {{ forloop.parentloop.counter }}</p>
    <p>循环次数: {{ forloop.revcounter }}</p>
    <p>循环次数(0): {{ forloop.revcounter0 }}</p>
{% empty %}
    <p>列表为空</p>
{% endfor %}

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

<!-- 条件运算符 -->
{% if user.is_authenticated and user.is_staff %}
    <p>管理员</p>
{% endif %}

{% if user.is_superuser or user.is_staff %}
    <p>有权限</p>
{% endif %}

{% if not user.is_banned %}
    <p>正常用户</p>
{% endif %}

{% if user.age|add:1 > 18 %}
    <p>成年人</p>
{% endif %}

3.2 模板继承标签

<!-- base.html - 基础模板 -->
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}默认标题{% endblock %}</title>
    {% block meta %}
    <meta charset="UTF-8">
    {% endblock %}
    {% block stylesheets %}
    <link rel="stylesheet" href="{% static 'css/base.css' %}">
    {% endblock %}
</head>
<body>
    <header>
        {% block header %}
        <nav>导航栏</nav>
        {% endblock %}
    </header>
    
    <main>
        {% block content %}{% endblock %}
    </main>
    
    <footer>
        {% block footer %}
        <p>版权所有</p>
        {% endblock %}
    </footer>
    
    {% block scripts %}
    <script src="{% static 'js/base.js' %}"></script>
    {% endblock %}
</body>
</html>

<!-- article.html - 继承模板 -->
{% extends "base.html" %}
{% load static %}

{% block title %}{{ article.title }}{% endblock %}

{% block stylesheets %}
{{ block.super }}  <!-- 保留父模板内容 -->
<link rel="stylesheet" href="{% static 'css/article.css' %}">
{% endblock %}

{% block content %}
<article>
    <h1>{{ article.title }}</h1>
    <div class="content">{{ article.content|safe }}</div>
</article>
{% endblock %}

3.3 包含与导入标签

<!-- include标签 -->
{% include "header.html" %}

<!-- 带上下文的include -->
{% include "article_card.html" with article=featured_article show_image=True %}

<!-- only参数(不继承上下文) -->
{% include "widget.html" with title="组件标题" only %}

<!-- load标签 -->
{% load static %}           <!-- 加载static标签库 -->
{% load i18n %}             <!-- 加载国际化标签库 -->
{% load myapp_tags %}       <!-- 加载自定义标签库 -->

<!-- 多个标签库 -->
{% load static i18n cache %}

3.4 URL标签

<!-- 基本URL -->
<a href="{% url 'home' %}">首页</a>

<!-- 带参数的URL -->
<a href="{% url 'article_detail' article.id %}">{{ article.title }}</a>

<!-- 命名参数 -->
<a href="{% url 'archive' year=2024 month=3 %}">2024年3月</a>

<!-- 命名空间URL -->
<a href="{% url 'blog:article_detail' article.id %}">文章详情</a>

<!-- as语法(存储URL) -->
{% url 'article_list' as article_url %}
<a href="{{ article_url }}">文章列表</a>

<!-- 条件URL -->
{% url 'article_detail' article.id as article_url %}
{% if article_url %}
    <a href="{{ article_url }}">查看文章</a>
{% endif %}

3.5 其他常用标签

<!-- csrf_token -->
<form method="post">
    {% csrf_token %}
    <input type="text" name="title">
    <button type="submit">提交</button>
</form>

<!-- static -->
<link rel="stylesheet" href="{% static 'css/style.css' %}">
<img src="{% static 'images/logo.png' %}" alt="Logo">

<!-- 带变量的static -->
{% static 'images/' as image_base %}
<img src="{{ image_base }}logo.png">

<!-- cache缓存 -->
{% load cache %}
{% cache 500 sidebar %}
    <!-- 缓存500秒 -->
    <div class="sidebar">
        {% include "sidebar.html" %}
    </div>
{% endcache %}

<!-- 带键名的缓存 -->
{% cache 500 sidebar user.id %}
    <div class="user-sidebar">
        {{ user.username }}
    </div>
{% endcache %}

<!-- spaceless(去除空白) -->
{% spaceless %}
    <div>
        <p>内容</p>
    </div>
{% endspaceless %}
<!-- 输出: <div><p>内容</p></div> -->

<!-- verbatim(不解析) -->
{% verbatim %}
    {{ 这里的模板语法不会被解析 }}
{% endverbatim %}

<!-- autoescape -->
{% autoescape off %}
    {{ html_content }}  <!-- 不转义 -->
{% endautoescape %}

{% autoescape on %}
    {{ html_content }}  <!-- 转义 -->
{% endautoescape %}

四、内置过滤器详解

4.1 字符串过滤器

<!-- 大小写转换 -->
{{ name|upper }}           <!-- 大写 -->
{{ name|lower }}           <!-- 小写 -->
{{ name|title }}           <!-- 标题格式 -->
{{ name|capfirst }}        <!-- 首字母大写 -->

<!-- 字符串处理 -->
{{ text|truncatewords:10 }}      <!-- 截取10个单词 -->
{{ text|truncatechars:50 }}       <!-- 截取50个字符 -->
{{ text|truncatechars_html:50 }}  <!-- 截取HTML保留标签 -->

{{ text|wordcount }}              <!-- 单词数 -->
{{ text|linebreaks }}             <!-- 换行转<br> -->
{{ text|linebreaksbr }}           <!-- 换行转<br>(不转义) -->
{{ text|linenumbers }}            <!-- 添加行号 -->

{{ text|striptags }}              <!-- 移除HTML标签 -->
{{ text|removetags:"p div" }}     <!-- 移除指定标签 -->
{{ text|escapejs }}               <!-- JS转义 -->

{{ text|cut:" " }}                <!-- 移除空格 -->
{{ text|slugify }}                <!-- 转为slug格式 -->
{{ text|urlencode }}              <!-- URL编码 -->
{{ text|urlize }}                 <!-- URL转链接 -->
{{ text|urlizetrunc:30 }}         <!-- URL转链接并截断 -->

<!-- 字符串判断 -->
{{ text|startswith:"Hello" }}     <!-- 是否以Hello开头 -->
{{ text|endswith:"world" }}       <!-- 是否以world结尾 -->
{{ text|contains:"test" }}        <!-- 是否包含test -->

4.2 数值过滤器

<!-- 数值格式化 -->
{{ value|add:5 }}                 <!-- 加5 -->
{{ value|add:"-3" }}              <!-- 减3 -->

{{ price|floatformat }}           <!-- 默认1位小数 -->
{{ price|floatformat:2 }}         <!-- 2位小数 -->
{{ price|floatformat:"-2" }}      <!-- 2位小数(负数不显示小数点后0) -->

{{ value|divisibleby:3 }}         <!-- 是否能被3整除 -->

<!-- 获取绝对值 -->
{{ value|abs }}

<!-- 整数处理 -->
{{ items|length }}                <!-- 长度 -->
{{ items|length_is:5 }}           <!-- 长度是否为5 -->

4.3 日期时间过滤器

<!-- 日期格式化 -->
{{ date|date:"Y-m-d" }}           <!-- 2024-03-15 -->
{{ date|date:"Y年m月d日" }}       <!-- 2024年03月15日 -->
{{ date|date:"Y-m-d H:i:s" }}     <!-- 2024-03-15 14:30:00 -->

<!-- 时间格式化 -->
{{ time|time:"H:i" }}             <!-- 14:30 -->

<!-- 时间间隔 -->
{{ duration|timesince:start }}    <!-- 距start多久 -->
{{ duration|timeuntil:end }}      <!-- 距end还有多久 -->

<!-- 格式符号说明 -->
<!--
Y: 四位年份
y: 两位年份
m: 两位月份
n: 月份(不补零)
M: 月份缩写
F: 月份全称
d: 两位日期
j: 日期(不补零)
D: 星期缩写
l: 星期全称
H: 24小时制小时
h: 12小时制小时
i: 分钟
s: 秒
A/a: AM/PM
-->

4.4 列表过滤器

<!-- 列表操作 -->
{{ items|first }}                 <!-- 第一个元素 -->
{{ items|last }}                  <!-- 最后一个元素 -->
{{ items|random }}                <!-- 随机元素 -->
{{ items|length }}                <!-- 长度 -->

{{ items|join:", " }}             <!-- 用逗号连接 -->
{{ items|slice:":5" }}            <!-- 切片前5个 -->
{{ items|slice:"1:3" }}           <!-- 切片索引1-3 -->

{{ items|dictsort:"name" }}       <!-- 按name排序 -->
{{ items|dictsortreversed:"name" }} <!-- 按name降序 -->

{{ items|unordered_list }}        <!-- 无序列表HTML -->

<!-- 列表判断 -->
{{ items|length_is:0 }}           <!-- 长度是否为0 -->
{{ items|length_is:5 }}           <!-- 长度是否为5 -->

4.5 默认值与逻辑过滤器

<!-- 默认值 -->
{{ value|default:"默认值" }}      <!-- None/False时显示默认值 -->
{{ value|default_if_none:"值" }}  <!-- 仅None时显示默认值 -->

<!-- 布尔值 -->
{{ value|yesno:"是,否,未知" }}    <!-- True/False/None映射 -->

<!-- 逻辑判断 -->
{{ items|isempty }}               <!-- 是否为空 -->

4.6 安全过滤器

<!-- 转义 -->
{{ html|escape }}                 <!-- HTML转义 -->
{{ html|safe }}                   <!-- 不转义(信任HTML) -->
{{ html|force_escape }}           <!-- 强制转义 -->

<!-- 安全转义 -->
{{ js|escapejs }}                 <!-- JS转义 -->
{{ json|json_script:"data" }}     <!-- 输出为script标签 -->

五、Context处理器详解

5.1 Context处理器机制

内置处理器

Context处理器流程

HTTP请求

视图函数

render函数

模板引擎

Context处理器

模板上下文

模板渲染

debug

request

auth

messages

static

media

5.2 内置Context处理器

# 内置Context处理器说明

# django.template.context_processors.debug
"""
注入变量:
- DEBUG: 调试模式
- sql_queries: SQL查询列表(DEBUG=True时)
"""

# django.template.context_processors.request
"""
注入变量:
- request: 当前HttpRequest对象
"""

# django.contrib.auth.context_processors.auth
"""
注入变量:
- user: 当前用户对象
- perms: 用户权限对象
"""

# django.contrib.messages.context_processors.messages
"""
注入变量:
- messages: 消息列表
- DEFAULT_MESSAGE_LEVELS: 消息级别映射
"""

# django.template.context_processors.static
"""
注入变量:
- STATIC_URL: 静态文件URL
"""

# django.template.context_processors.media
"""
注入变量:
- MEDIA_URL: 媒体文件URL
"""

# django.template.context_processors.csrf
"""
注入变量:
- csrf_token: CSRF令牌
"""

# django.template.context_processors.tz
"""
注入变量:
- TIME_ZONE: 时区
"""

# django.template.context_processors.static
"""
注入变量:
- STATIC_URL: 静态文件URL
- STATIC_ROOT: 静态文件根目录
"""

5.3 自定义Context处理器

# myapp/context_processors.py

def site_settings(request):
    """
    站点设置Context处理器
    
    注入站点全局配置
    """
    from django.conf import settings
    
    return {
        'SITE_NAME': getattr(settings, 'SITE_NAME', 'My Site'),
        'SITE_VERSION': getattr(settings, 'SITE_VERSION', '1.0.0'),
        'SITE_DESCRIPTION': getattr(settings, 'SITE_DESCRIPTION', ''),
    }


def user_notifications(request):
    """
    用户通知Context处理器
    
    注入用户未读通知
    """
    if request.user.is_authenticated:
        from myapp.models import Notification
        unread_count = Notification.objects.filter(
            user=request.user,
            is_read=False
        ).count()
        return {
            'unread_notifications': unread_count,
        }
    return {
        'unread_notifications': 0,
    }


def categories(request):
    """
    分类列表Context处理器
    
    注入全站分类
    """
    from myapp.models import Category
    from django.core.cache import cache
    
    categories = cache.get('global_categories')
    if categories is None:
        categories = list(Category.objects.filter(is_active=True))
        cache.set('global_categories', categories, 3600)
    
    return {
        'global_categories': categories,
    }


def theme_settings(request):
    """
    主题设置Context处理器
    
    支持用户自定义主题
    """
    theme = 'default'
    
    if request.user.is_authenticated:
        theme = getattr(request.user, 'theme', 'default')
    elif 'theme' in request.COOKIES:
        theme = request.COOKIES['theme']
    
    return {
        'current_theme': theme,
        'theme_css': f'css/themes/{theme}.css',
    }


# settings.py 配置
TEMPLATES = [{
    # ...
    'OPTIONS': {
        'context_processors': [
            # 内置处理器
            'django.template.context_processors.debug',
            'django.template.context_processors.request',
            'django.contrib.auth.context_processors.auth',
            'django.contrib.messages.context_processors.messages',
            # 自定义处理器
            'myapp.context_processors.site_settings',
            'myapp.context_processors.user_notifications',
            'myapp.context_processors.categories',
            'myapp.context_processors.theme_settings',
        ],
    },
}]

六、自定义模板组件

6.1 自定义过滤器

# myapp/templatetags/myapp_filters.py
from django import template
from django.utils.safestring import mark_safe
import re

register = template.Library()


@register.filter(name='currency')
def currency(value, symbol='¥'):
    """
    货币格式化过滤器
    
    用法: {{ price|currency:"$" }}
    """
    try:
        value = float(value)
        return f"{symbol}{value:,.2f}"
    except (ValueError, TypeError):
        return value


@register.filter
def percentage(value, decimals=2):
    """
    百分比格式化
    
    用法: {{ rate|percentage }} 或 {{ rate|percentage:1 }}
    """
    try:
        value = float(value)
        return f"{value:.{decimals}f}%"
    except (ValueError, TypeError):
        return value


@register.filter
def truncate_middle(value, length=50):
    """
    中间截断
    
    用法: {{ long_text|truncate_middle:30 }}
    """
    if len(str(value)) <= length:
        return value
    half = length // 2
    return f"{value[:half]}...{value[-half:]}"


@register.filter
def highlight(text, query):
    """
    高亮搜索词
    
    用法: {{ content|highlight:query }}
    """
    if not query:
        return text
    
    pattern = re.compile(f'({re.escape(query)})', re.IGNORECASE)
    result = pattern.sub(r'<mark>\1</mark>', str(text))
    return mark_safe(result)


@register.filter
def timesince_simple(value):
    """
    简化的时间间隔
    
    用法: {{ created_at|timesince_simple }}
    """
    from django.utils import timezone
    from datetime import timedelta
    
    if not value:
        return ''
    
    now = timezone.now()
    diff = now - value
    
    if diff < timedelta(minutes=1):
        return '刚刚'
    elif diff < timedelta(hours=1):
        return f'{int(diff.seconds / 60)}分钟前'
    elif diff < timedelta(days=1):
        return f'{int(diff.seconds / 3600)}小时前'
    elif diff < timedelta(days=30):
        return f'{diff.days}天前'
    elif diff < timedelta(days=365):
        return f'{int(diff.days / 30)}个月前'
    else:
        return f'{int(diff.days / 365)}年前'


@register.filter
def model_name(obj):
    """
    获取模型名称
    
    用法: {{ obj|model_name }}
    """
    if hasattr(obj, '_meta'):
        return obj._meta.verbose_name
    return ''


@register.filter(is_safe=True)
def markdown(value):
    """
    Markdown渲染
    
    用法: {{ content|markdown }}
    """
    import markdown as md
    return mark_safe(md.markdown(value, extensions=['extra', 'codehilite']))


@register.filter
def get_item(dictionary, key):
    """
    字典取值
    
    用法: {{ mydict|get_item:key }}
    """
    if dictionary is None:
        return None
    return dictionary.get(key)


# 在模板中加载
# {% load myapp_filters %}

6.2 自定义简单标签

# myapp/templatetags/myapp_tags.py
from django import template
from django.utils.safestring import mark_safe
import datetime

register = template.Library()


@register.simple_tag
def current_time(format_string='%Y-%m-%d %H:%M:%S'):
    """
    当前时间标签
    
    用法: {% current_time "%Y-%m-%d" %}
    """
    return datetime.datetime.now().strftime(format_string)


@register.simple_tag
def query_transform(request, **kwargs):
    """
    URL参数转换
    
    用法: {% query_transform request page=2 %}
    """
    updated = request.GET.copy()
    for key, value in kwargs.items():
        if value:
            updated[key] = value
        else:
            updated.pop(key, None)
    return updated.urlencode()


@register.simple_tag
def url_replace(request, field, value):
    """
    URL参数替换
    
    用法: {% url_replace request 'page' 2 %}
    """
    query_dict = request.GET.copy()
    query_dict[field] = value
    return query_dict.urlencode()


@register.simple_tag
def active_class(request, pattern):
    """
    导航激活状态
    
    用法: {% active_class request '/blog/' %}
    """
    import re
    if re.search(pattern, request.path):
        return 'active'
    return ''


@register.simple_tag
def site_version():
    """
    站点版本
    
    用法: {% site_version %}
    """
    from django.conf import settings
    return getattr(settings, 'SITE_VERSION', '1.0.0')


@register.simple_tag(takes_context=True)
def current_user_attr(context, attr):
    """
    获取当前用户属性
    
    用法: {% current_user_attr 'username' %}
    """
    request = context['request']
    if request.user.is_authenticated:
        return getattr(request.user, attr, '')
    return ''


@register.simple_tag(takes_context=True)
def render_breadcrumb(context):
    """
    渲染面包屑
    
    用法: {% render_breadcrumb %}
    """
    request = context.get('request')
    if not request:
        return ''
    
    path_parts = request.path.strip('/').split('/')
    if not path_parts or path_parts == ['']:
        return mark_safe('<nav class="breadcrumb"><a href="/">首页</a></nav>')
    
    breadcrumb = ['<nav class="breadcrumb"><a href="/">首页</a>']
    current_path = ''
    
    for part in path_parts:
        current_path += f'/{part}'
        breadcrumb.append(f' &gt; <a href="{current_path}">{part.title()}</a>')
    
    breadcrumb.append('</nav>')
    return mark_safe(''.join(breadcrumb))

6.3 自定义包含标签

# myapp/templatetags/myapp_tags.py
from django import template

register = template.Library()


@register.inclusion_tag('tags/pagination.html')
def pagination(page_obj, request=None):
    """
    分页标签
    
    用法: {% pagination page_obj request %}
    """
    return {
        'page_obj': page_obj,
        'request': request,
    }


@register.inclusion_tag('tags/article_card.html')
def article_card(article, show_image=True, show_summary=True):
    """
    文章卡片标签
    
    用法: {% article_card article show_image=False %}
    """
    return {
        'article': article,
        'show_image': show_image,
        'show_summary': show_summary,
    }


@register.inclusion_tag('tags/user_avatar.html')
def user_avatar(user, size=40, show_name=False):
    """
    用户头像标签
    
    用法: {% user_avatar user size=80 show_name=True %}
    """
    return {
        'user': user,
        'size': size,
        'show_name': show_name,
    }


@register.inclusion_tag('tags/alert.html')
def alert(message, alert_type='info', dismissible=True):
    """
    警告框标签
    
    用法: {% alert "操作成功" "success" %}
    """
    return {
        'message': message,
        'alert_type': alert_type,
        'dismissible': dismissible,
    }


@register.inclusion_tag('tags/social_share.html', takes_context=True)
def social_share(context, url=None, title=None):
    """
    社交分享标签
    
    用法: {% social_share url=title %}
    """
    request = context['request']
    if url is None:
        url = request.build_absolute_uri()
    if title is None:
        title = context.get('title', '')
    
    return {
        'url': url,
        'title': title,
    }


# templates/tags/pagination.html
"""
<nav class="pagination">
    {% if page_obj.has_previous %}
    <a href="?page={{ page_obj.previous_page_number }}" class="prev">上一页</a>
    {% endif %}
    
    <span class="current">
        第 {{ page_obj.number }} 页,共 {{ page_obj.paginator.num_pages }} 页
    </span>
    
    {% if page_obj.has_next %}
    <a href="?page={{ page_obj.next_page_number }}" class="next">下一页</a>
    {% endif %}
</nav>
"""

6.4 自定义模板标签Node类

# myapp/templatetags/advanced_tags.py
from django import template
from django.utils.safestring import mark_safe

register = template.Library()


class EchoNode(template.Node):
    """
    Echo标签节点
    """
    
    def __init__(self, values):
        self.values = values
    
    def render(self, context):
        rendered = []
        for value in self.values:
            if isinstance(value, template.Variable):
                rendered.append(str(value.resolve(context)))
            else:
                rendered.append(str(value))
        return ' '.join(rendered)


@register.tag
def echo(parser, token):
    """
    Echo标签
    
    用法: {% echo "Hello" user.name "!" %}
    """
    parts = token.split_contents()[1:]
    values = []
    for part in parts:
        if part[0] in ('"', "'"):
            values.append(part[1:-1])
        else:
            values.append(template.Variable(part))
    return EchoNode(values)


class CaptureNode(template.Node):
    """
    Capture标签节点
    将内容存储到变量中
    """
    
    def __init__(self, nodelist, variable_name):
        self.nodelist = nodelist
        self.variable_name = variable_name
    
    def render(self, context):
        output = self.nodelist.render(context)
        context[self.variable_name] = output
        return ''


@register.tag
def capture(parser, token):
    """
    Capture标签
    
    用法:
    {% capture as captured_content %}
        <div>复杂内容</div>
    {% endcapture %}
    {{ captured_content }}
    """
    parts = token.split_contents()
    if len(parts) != 3 or parts[1] != 'as':
        raise template.TemplateSyntaxError(
            "capture标签语法: {% capture as variable_name %}...{% endcapture %}"
        )
    
    variable_name = parts[2]
    nodelist = parser.parse(('endcapture',))
    parser.delete_first_token()
    
    return CaptureNode(nodelist, variable_name)


class IfInNode(template.Node):
    """
    Ifin标签节点
    检查值是否在列表中
    """
    
    def __init__(self, value, list_var, nodelist_true, nodelist_false):
        self.value = template.Variable(value)
        self.list_var = template.Variable(list_var)
        self.nodelist_true = nodelist_true
        self.nodelist_false = nodelist_false
    
    def render(self, context):
        try:
            value = self.value.resolve(context)
            list_value = self.list_var.resolve(context)
            
            if value in list_value:
                return self.nodelist_true.render(context)
            return self.nodelist_false.render(context)
        except Exception:
            return self.nodelist_false.render(context)


@register.tag
def ifin(parser, token):
    """
    Ifin标签
    
    用法:
    {% ifin user.role allowed_roles %}
        有权限
    {% else %}
        无权限
    {% endifin %}
    """
    parts = token.split_contents()
    if len(parts) != 3:
        raise template.TemplateSyntaxError(
            "ifin标签语法: {% ifin value list %}...{% endifin %}"
        )
    
    value = parts[1]
    list_var = parts[2]
    
    nodelist_true = parser.parse(('else', 'endifin'))
    token = parser.next_token()
    
    if token.contents == 'else':
        nodelist_false = parser.parse(('endifin',))
        parser.delete_first_token()
    else:
        nodelist_false = template.NodeList()
    
    return IfInNode(value, list_var, nodelist_true, nodelist_false)

七、模板渲染机制

7.1 渲染流程

节点树 上下文 模板解析器 模板加载器 模板引擎 render() 视图函数 节点树 上下文 模板解析器 模板加载器 模板引擎 render() 视图函数 render(request, 'template.html', context) get_template('template.html') get_template_sources() 模板源 parse(template_source) 节点树 Template对象 创建上下文 执行Context处理器 render(context) HTML字符串 HttpResponse

7.2 模板加载器

# Django内置模板加载器

# 1. filesystem.Loader - 文件系统加载器
"""
从TEMPLATES['DIRS']指定的目录加载模板
"""

# 2. app_directories.Loader - 应用目录加载器
"""
从各应用的templates子目录加载模板
需要设置'APP_DIRS': True
"""

# 3. cached.Loader - 缓存加载器
"""
缓存已解析的模板,提高性能
配置:
    'loaders': [
        ('django.template.loaders.cached.Loader', [
            'django.template.loaders.filesystem.Loader',
            'django.template.loaders.app_directories.Loader',
        ]),
    ],
"""

# 自定义模板加载器
from django.template.loaders.base import Loader
from django.template import TemplateDoesNotExist


class DatabaseLoader(Loader):
    """
    从数据库加载模板
    """
    
    def get_template_sources(self, template_name):
        from myapp.models import DatabaseTemplate
        
        try:
            template = DatabaseTemplate.objects.get(name=template_name)
            yield template.content
        except DatabaseTemplate.DoesNotExist:
            raise TemplateDoesNotExist(template_name)
    
    def load_template_source(self, template_name, template_dirs=None):
        for source in self.get_template_sources(template_name):
            return source, template_name
        raise TemplateDoesNotExist(template_name)


# 配置自定义加载器
TEMPLATES = [{
    # ...
    'OPTIONS': {
        'loaders': [
            'myapp.loaders.DatabaseLoader',
            'django.template.loaders.filesystem.Loader',
            'django.template.loaders.app_directories.Loader',
        ],
    },
}]

7.3 Context对象详解

from django.template import Context, RequestContext

# 基本Context
def context_basics():
    """Context基础用法"""
    
    # 创建Context
    context = Context({
        'title': '文章标题',
        'articles': ['文章1', '文章2'],
    })
    
    # 访问值
    print(context['title'])
    
    # 更新值
    context['new_key'] = '新值'
    context.update({'key1': 'value1', 'key2': 'value2'})
    
    # 压平(合并所有字典)
    flat = context.flatten()
    
    # 迭代
    for key, value in context.items():
        print(f'{key}: {value}')


# RequestContext
def request_context_view(request):
    """RequestContext使用"""
    
    # RequestContext会自动应用Context处理器
    context = RequestContext(request, {
        'title': '页面标题',
    })
    
    # 等价于
    from django.template import engines
    engine = engines['django']
    context = engine.template_context_processors(request)
    context.update({'title': '页面标题'})
    
    return render(request, 'template.html', context)


# Context栈
def context_stack():
    """Context栈操作"""
    
    context = Context({'a': 1})
    
    # push新层
    with context.push():
        context['a'] = 2
        print(context['a'])  # 2
    
    # pop后恢复
    print(context['a'])  # 1
    
    # 使用update
    with context.update({'b': 2}):
        print(context['b'])  # 2

八、性能优化与最佳实践

8.1 模板性能优化

# 1. 使用模板缓存
TEMPLATES = [{
    # ...
    'OPTIONS': {
        'loaders': [
            ('django.template.loaders.cached.Loader', [
                'django.template.loaders.filesystem.Loader',
                'django.template.loaders.app_directories.Loader',
            ]),
        ],
    },
}]


# 2. 减少模板继承层级
# 不推荐:多层继承
# base.html -> base_with_sidebar.html -> base_with_nav.html -> page.html

# 推荐:扁平继承
# base.html -> page.html


# 3. 使用with标签缓存复杂计算
{% with article.get_related_articles as related %}
    {% for item in related %}
        {{ item.title }}
    {% endfor %}
{% endwith %}


# 4. 避免在模板中进行复杂计算
# 不推荐
{{ article.get_complex_score }}

# 推荐:在视图中计算
def article_detail(request, pk):
    article = Article.objects.get(pk=pk)
    article.complex_score = calculate_score(article)
    return render(request, 'article.html', {'article': article})


# 5. 使用select_related/prefetch_related
def article_list(request):
    articles = Article.objects.select_related('author').prefetch_related('tags')
    return render(request, 'articles.html', {'articles': articles})

8.2 模板安全最佳实践

# 1. 自动转义(默认开启)
TEMPLATES = [{
    # ...
    'OPTIONS': {
        'autoescape': True,  # 默认True
    },
}]


# 2. 谨慎使用safe过滤器
# 不推荐
{{ user_input|safe }}

# 推荐:先清理
{{ user_input|striptags|safe }}


# 3. 使用escapejs处理JS变量
<script>
var content = "{{ content|escapejs }}";
</script>


# 4. 使用json_script处理复杂数据
{{ data|json_script:"my-data" }}
<script>
var data = JSON.parse(document.getElementById('my-data').textContent);
</script>


# 5. CSRF保护
<form method="post">
    {% csrf_token %}
    ...
</form>

8.3 模板组织最佳实践

# 推荐的模板目录结构
templates/
├── base.html                 # 基础模板
├── base_with_sidebar.html    # 带侧边栏的基础模板
├── includes/                 # 可复用组件
│   ├── header.html
│   ├── footer.html
│   ├── sidebar.html
│   ├── pagination.html
│   └── article_card.html
├── myapp/                    # 应用模板
│   ├── article_list.html
│   ├── article_detail.html
│   └── article_form.html
└── errors/                   # 错误页面
    ├── 400.html
    ├── 403.html
    ├── 404.html
    └── 500.html
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐