Django 模板系统和模板标签

🎨 Django 模板语言 (DTL)

Django模板系统允许你将业务逻辑与表现层分离,使用简单而强大的模板语言来创建动态HTML页面。

什么是Django模板?

Django模板是包含特殊语法标记的文本文件,用于:

  • 显示变量 - 将Python变量插入HTML
  • 控制逻辑 - 条件判断、循环等
  • 模板继承 - 创建可重用的布局
  • 过滤器 - 格式化变量显示

Django 模板系统架构

视图

准备上下文数据

模板引擎

解析模板语法

HTML输出

生成最终页面

🔤
变量

使用双花括号显示变量值

{{ variable }}
🏷️
标签

使用花括号和百分号执行逻辑

{% tag %}
🔧
过滤器

使用管道符号修改变量显示

{{ variable|filter }}
💬
注释

使用花括号和井号添加注释

{# comment #}

模板基础语法

视图 (views.py)
from django.shortcuts import render
from datetime import datetime

def post_list(request):
    context = {
        'title': '我的博客',
        'posts': [
            {'title': '文章1', 'content': '这是第一篇文章'},
            {'title': '文章2', 'content': '这是第二篇文章'},
        ],
        'current_time': datetime.now(),
        'user': {'name': '张三', 'is_authenticated': True},
        'page_views': 1500,
    }
    return render(request, 'blog/post_list.html', context)
模板 (post_list.html)
<!DOCTYPE html>
<html>
<head>
    <title>{{ title }}</title>
</head>
<body>
    <h1>欢迎来到{{ title }}</h1>

    {# 条件判断 #}
    {% if user.is_authenticated %}
        <p>欢迎, {{ user.name }}!</p>
    {% else %}
        <p>请<a href="/login/">登录</a></p>
    {% endif %}

    {# 循环 #}
    <div class="posts">
        {% for post in posts %}
            <article>
                <h2>{{ post.title }}</h2>
                <p>{{ post.content|truncatewords:10 }}</p>
            </article>
        {% empty %}
            <p>暂无文章</p>
        {% endfor %}
    </div>

    {# 使用过滤器 #}
    <footer>
        <p>当前时间: {{ current_time|date:"Y-m-d H:i" }}</p>
        <p>访问量: {{ page_views|intcomma }}</p>
    </footer>
</body>
</html>

模板继承

🏗️ DRY原则:不要重复自己

模板继承允许你创建一个基础骨架模板,然后在子模板中覆盖特定的部分。

模板继承层次结构

base.html

基础模板

blog_base.html

博客应用基础模板

post_list.html

具体页面模板

{# templates/base.html #}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}我的网站{% endblock %}</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    {% block extra_css %}{% endblock %}
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
            <div class="container">
                <a class="navbar-brand" href="/">我的网站</a>
                {% block navigation %}{% endblock %}
            </div>
        </nav>
    </header>

    <main class="container my-4">
        {% block content %}
        <!-- 主要内容区域 -->
        {% endblock %}
    </main>

    <footer class="bg-light py-4 mt-5">
        <div class="container text-center">
            {% block footer %}
                <p>© 2023 我的网站. 版权所有.</p>
            {% endblock %}
        </div>
    </footer>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
    {% block extra_js %}{% endblock %}
</body>
</html>
{# templates/blog/post_list.html #}
{% extends "base.html" %}
{% load static %}

{% block title %}博客 - {{ block.super }}{% endblock %}

{% block extra_css %}
    <link rel="stylesheet" href="{% static 'blog/css/blog.css' %}">
{% endblock %}

{% block navigation %}
    <ul class="navbar-nav me-auto">
        <li class="nav-item">
            <a class="nav-link active" href="{% url 'post_list' %}">博客</a>
        </li>
    </ul>
{% endblock %}

{% block content %}
    <h1 class="mb-4">博客文章</h1>

    {% for post in posts %}
        <article class="card mb-3">
            <div class="card-body">
                <h2 class="card-title">{{ post.title }}</h2>
                <p class="card-text">{{ post.content|truncatewords:30 }}</p>
                <a href="{% url 'post_detail' post.id %}" class="btn btn-primary">阅读更多</a>
            </div>
        </article>
    {% empty %}
        <div class="alert alert-info">暂无文章</div>
    {% endfor %}
{% endblock %}

{% block footer %}
    <p>© 2023 我的博客. 共 {{ posts|length }} 篇文章.</p>
    {{ block.super }}
{% endblock %}
{# templates/blog/base_blog.html #}
{% extends "base.html" %}

{% block navigation %}
    <ul class="navbar-nav me-auto">
        <li class="nav-item">
            <a class="nav-link active" href="{% url 'post_list' %}">博客</a>
        </li>
        <li class="nav-item">
            <a class="nav-link" href="{% url 'category_list' %}">分类</a>
        </li>
    </ul>

    {% if user.is_authenticated %}
        <div class="navbar-nav">
            <a class="nav-link" href="{% url 'post_create' %}">写文章</a>
        </div>
    {% endif %}
{% endblock %}

{% block extra_css %}
    {{ block.super }}
    <link rel="stylesheet" href="{% static 'blog/css/blog.css' %}">
{% endblock %}

内置标签和过滤器

常用模板标签
{# 条件判断 #}
{% if user.is_authenticated %}
    欢迎回来!
{% elif user.is_anonymous %}
    请登录
{% else %}
    未知状态
{% endif %}

{# 循环 #}
{% for item in items %}
    <li>{{ forloop.counter }}. {{ item.name }}</li>
{% empty %}
    <li>没有项目</li>
{% endfor %}

{# URL反向解析 #}
<a href="{% url 'post_detail' post.id %}">详情</a>
<a href="{% url 'blog:post_detail' post.id %}">详情(命名空间)</a>

{# 静态文件 #}
{% load static %}
<img src="{% static 'images/logo.png' %}" alt="Logo">

{# 包含其他模板 #}
{% include "includes/header.html" %}
{% include "includes/footer.html" with year=2023 %}

{# 注释 #}
{# 这行不会在最终HTML中显示 #}

{# 分组内容 #}
{% with total=items|length %}
    共有 {{ total }} 个项目
{% endwith %}
常用模板过滤器
{# 字符串处理 #}
{{ name|lower }}          {# 转为小写 #}
{{ title|upper }}         {# 转为大写 #}
{{ content|truncatewords:30 }}  {# 截断单词 #}
{{ text|truncatechars:100 }}    {# 截断字符 #}
{{ slug|slugify }}        {# 转为slug格式 #}

{# 日期时间处理 #}
{{ created_at|date:"Y-m-d" }}    {# 日期格式化 #}
{{ updated_at|time:"H:i" }}      {# 时间格式化 #}
{{ post_date|timesince }}        {# 相对时间 #}

{# 数字处理 #}
{{ price|floatformat:2 }} {# 浮点数格式化 #}
{{ count|intcomma }}      {# 千位分隔符 #}
{{ percentage|floatformat:"0" }}% {# 百分比 #}

{# 列表处理 #}
{{ items|length }}        {# 列表长度 #}
{{ list|first }}          {# 第一个元素 #}
{{ list|last }}           {# 最后一个元素 #}
{{ list|join:", " }}      {# 连接列表 #}
{{ list|slice:":3" }}     {# 切片 #}

{# 布尔值处理 #}
{{ is_published|yesno:"是,否" }} {# 布尔值转中文 #}
{{ value|default:"暂无" }} {# 默认值 #}

{# 安全相关 #}
{{ user_input|escape }}   {# HTML转义 #}
{{ html_content|safe }}   {# 标记为安全HTML #}
{{ script|escapejs }}     {# JavaScript转义 #}

{# 链式过滤器 #}
{{ name|title|truncatewords:2 }}
                                        {{ content|striptags|truncatechars:100 }}

模板变量和上下文

📋 理解模板上下文

模板上下文是视图传递给模板的变量字典,决定了模板中可以访问哪些数据。

1

视图准备上下文

def post_detail(request, post_id):
    post = get_object_or_404(Post, id=post_id)
    comments = post.comments.all()

    context = {
        'post': post,
        'comments': comments,
        'comment_count': comments.count(),
        'current_time': timezone.now(),
        'user': request.user,
    }
    return render(request, 'blog/post_detail.html', context)
2

模板访问变量

<h1>{{ post.title }}</h1>
<p>发布于 {{ post.created_at|date }}</p>
<div>{{ post.content }}</div>

{% if user.is_authenticated %}
    <p>欢迎, {{ user.username }}!</p>
{% endif %}

<h2>评论 ({{ comment_count }})</h2>
{% for comment in comments %}
    <div class="comment">
        <strong>{{ comment.author }}</strong>
        <p>{{ comment.content }}</p>
    </div>
{% endfor %}

变量查找机制

Django模板使用点号查找语法,按照特定顺序查找变量:

查找类型 示例 说明
字典查找 {{ user.name }} 查找字典的键
属性查找 {{ post.title }} 查找对象的属性
方法调用 {{ post.get_absolute_url }} 调用无参数方法
列表索引 {{ items.0 }} 列表索引查找

自定义模板标签和过滤器

🔧 扩展模板功能

当内置标签和过滤器不能满足需求时,可以创建自定义的模板标签和过滤器。

🏷️
自定义模板过滤器

创建用于修改变量显示的自定义过滤器:

# blog/templatetags/blog_extras.py
from django import template

register = template.Library()

@register.filter
def markdown(value):
    """将Markdown文本转换为HTML"""
    import markdown as md
    return md.markdown(value, extensions=['extra'])

@register.filter
def pluralize_chinese(value, arg='个'):
    """中文复数形式"""
    if value > 1:
        return f"{value}{arg}"
    return f"{value}{arg}"

@register.filter
def percentage(value, decimal_places=1):
    """将小数转换为百分比格式"""
    if value is None:
        return "0%"
    return f"{value * 100:.{decimal_places}f}%"

# 在模板中使用
# {% load blog_extras %}
# {{ content|markdown }}
# {{ count|pluralize_chinese:"篇文章" }}
# {{ ratio|percentage:2 }}
🔧
简单标签 (Simple Tags)

创建返回内容的简单标签:

# blog/templatetags/blog_extras.py
from django.utils import timezone

@register.simple_tag
def current_time(format_string):
    """返回当前时间的格式化字符串"""
    return timezone.now().strftime(format_string)

@register.simple_tag
def banner_message():
    """返回横幅消息"""
    from django.conf import settings
    return getattr(settings, 'BANNER_MESSAGE', '')

@register.simple_tag(takes_context=True)
def user_greeting(context):
    """根据上下文返回用户问候语"""
    request = context['request']
    if request.user.is_authenticated:
        return f"欢迎回来,{request.user.username}!"
    return "您好,请登录或注册。"

# 在模板中使用
# {% current_time "%Y-%m-%d %H:%M:%S" as the_time %}
# <p>当前时间: {{ the_time }}</p>
# {% banner_message %}
# {% user_greeting %}
包含标签 (Inclusion Tags)

创建渲染其他模板的标签:

# blog/templatetags/blog_extras.py
@register.inclusion_tag('blog/includes/recent_posts.html')
def show_recent_posts(count=5):
    """显示最近的文章"""
    recent_posts = Post.objects.filter(
        is_published=True
    ).order_by('-created_at')[:count]
    return {'recent_posts': recent_posts}

@register.inclusion_tag('blog/includes/categories.html')
def show_categories():
    """显示分类列表"""
    from .models import Category
    categories = Category.objects.all()
    return {'categories': categories}

# templates/blog/includes/recent_posts.html
# <div class="recent-posts">
#   <h3>最近文章</h3>
#   <ul>
#     {% for post in recent_posts %}
#       <li><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></li>
#     {% endfor %}
#   </ul>
# </div>

# 在模板中使用
# {% show_recent_posts 10 %}
# {% show_categories %}

模板结构和组织

📁 合理的模板组织

良好的模板组织结构可以提高项目的可维护性。

templates/
base.html
404.html
500.html
includes/
header.html
footer.html
navigation.html
pagination.html
blog/
base_blog.html
post_list.html
post_detail.html
post_form.html
blog/includes/
post_card.html
comment_list.html
comment_form.html
users/
login.html
register.html
profile.html

模板加载顺序

Django按照settings.TEMPLATES中定义的顺序查找模板:

# settings.py
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            BASE_DIR / 'templates',  # 项目级模板目录
        ],
        'APP_DIRS': True,  # 启用应用模板目录
        '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',
            ],
            'libraries': {
                'blog_extras': 'blog.templatetags.blog_extras',
            }
        },
    },
]

# 模板查找顺序:
# 1. 项目级 templates 目录
# 2. 已安装应用的 templates 目录(按 INSTALLED_APPS 顺序)

模板安全和性能

🔒 模板安全注意事项

正确处理用户输入,防止XSS等安全漏洞。

❌ 不安全的做法
{# 直接输出用户输入,存在XSS风险 #}
<div>{{ user_input }}</div>

{# 错误使用safe过滤器 #}
<div>{{ user_input|safe }}</div>

{# 在JavaScript中直接使用用户输入 #}
<script>
    var username = "{{ username }}";  // 危险!
</script>
✅ 安全的做法
{# 自动转义用户输入 #}
<div>{{ user_input }}</div>

{# 明确标记可信内容为safe #}
<div>{{ trusted_html|safe }}</div>

{# 在JavaScript中正确转义 #}
<script>
    var username = "{{ username|escapejs }}";
</script>

{# 使用专门的转义过滤器 #}
<div>{{ user_input|force_escape }}</div>
⚡ 模板性能优化

优化模板渲染性能,提高网站响应速度。

{# 避免在模板中进行复杂计算 #}
{# 不好的做法 #}
{% for post in posts %}
    <div>{{ post.comments.count }} 条评论</div>
{% endfor %}

{# 好的做法 - 在视图中预计算 #}
{% for post in posts %}
    <div>{{ post.comment_count }} 条评论</div>
{% endfor %}

{# 使用with标签缓存复杂查询 #}
{% with total=user.posts.count %}
    <p>发表了 {{ total }} 篇文章</p>
{% endwith %}

{# 合理使用include标签 #}
{# 避免在循环中include复杂模板 #}
{% for post in posts %}
    {# 避免这样做 #}
    {% include "includes/post_detail.html" %}
{% endfor %}

{# 使用模板片段缓存 #}
{% load cache %}
{% cache 600 post_detail post.id %}
    <div class="post-detail">
        {# 复杂的渲染逻辑 #}
    </div>
{% endcache %}

交互式模板编辑器

✍️ 实时模板预览

在下方编辑Django模板代码,实时查看渲染结果:

模板代码
渲染结果
模板渲染结果将显示在这里...

模板开发最佳实践

🏗️
使用模板继承

创建基础模板,避免代码重复

📁
合理组织模板

按功能模块组织模板文件

🔒
注意安全转义

谨慎使用safe过滤器

优化性能

避免在模板中进行复杂计算

🎯
保持模板简洁

复杂的逻辑放在视图或模型中

🔧
使用自定义标签

封装可重用的模板逻辑

💡 实用技巧
  • 使用{% with %}标签缓存复杂查询结果
  • 合理使用{% include %}拆分复杂模板
  • 利用{% block.super %}扩展父模板内容
  • 为模板标签和过滤器编写文档
  • 使用模板片段缓存提高性能
  • 编写模板测试用例

模板调试和问题排查

🐛 常见模板问题排查

掌握模板调试技巧,快速定位和解决问题。

{# 1. 使用debug标签查看变量 #}
{% debug %}

{# 2. 检查变量是否存在 #}
{% if some_variable %}
    <p>{{ some_variable }}</p>
{% else %}
    <p>变量不存在或为空</p>
{% endif %}

{# 3. 使用default过滤器处理空值 #}
<p>{{ possibly_none_variable|default:"默认值" }}</p>

{# 4. 在开发环境中显示详细错误 #}
{# 在settings.py中设置: #}
{# DEBUG = True #}
{# TEMPLATES[0]['OPTIONS']['debug'] = True #}

{# 5. 使用Django Debug Toolbar #}
{# 安装并配置django-debug-toolbar #}

{# 6. 自定义模板错误页面 #}
{# 创建400.html, 403.html, 404.html, 500.html #}

常见错误和解决方案

错误类型 可能原因 解决方案
TemplateDoesNotExist 模板路径错误或模板不存在 检查模板路径和DIRS设置
TemplateSyntaxError 模板语法错误 检查标签和过滤器语法
VariableDoesNotExist 引用了不存在的变量 检查视图中的上下文变量
Invalid block tag 使用了未注册的标签 检查标签名称和{% load %}

下一步学习

📝
表单处理

学习Django表单的创建和验证

学习表单
🔐
用户认证

掌握Django内置的用户认证系统

学习认证
🧪
测试和部署

学习如何测试Django应用并部署

学习测试