Django从零开始(四):模板系统与前端渲染

一、Django模板系统概述

Django模板引擎(Template Engine)是一个强大的文本渲染系统,用于将Python数据与HTML结合生成动态网页。它遵循"逻辑与展示分离"的设计理念,在模板中只能执行展示相关的逻辑,不能编写复杂的Python代码。

模板系统的核心概念

  • 变量(Variables):从视图中传入的动态数据,使用 {{ variable }} 语法
  • 标签(Tags):控制模板逻辑,使用 {% tag %} 语法
  • 过滤器(Filters):对变量进行格式化处理,使用 {{ variable|filter }} 语法
  • 注释(Comments):模板注释,使用 {# comment #} 语法

二、模板配置

2.1 模板目录结构

# 推荐的模板目录结构
mysite/
├── templates/                  # 项目级模板目录
│   ├── base.html              # 基础模板
│   ├── navbar.html            # 导航栏组件
│   └── errors/                # 错误页面
│       ├── 404.html
│       └── 500.html
└── blog/
    └── templates/             # App级模板目录
        └── blog/
            ├── article_list.html
            ├── article_detail.html
            ├── article_form.html
            └── includes/      # 可复用的模板片段
                ├── pagination.html
                ├── sidebar.html
                └── article_card.html

2.2 settings.py配置

# settings.py

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],  # 项目级模板目录
        'APP_DIRS': True,                   # 自动搜索App的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',
                # 自定义上下文处理器
                'blog.context_processors.site_settings',
            ],
        },
    },
]

三、模板语法详解

3.1 变量输出

<!-- 基本变量 -->
<h1>{{ article.title }}</h1>
<p>作者:{{ article.author.username }}</p>
<span>{{ article.created_at|date:"Y-m-d" }}</span>

<!-- 字典取值 -->
<p>{{ settings.site_name }}</p>

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

<!-- 方法调用(不需要加括号) -->
<p>{{ article.get_status_display }}</p>

3.2 模板标签

<!-- if/elif/else 条件判断 -->
{% if article.status == 'published' %}
    <span class="badge badge-success">已发布</span>
{% elif article.status == 'draft' %}
    <span class="badge badge-warning">草稿</span>
{% else %}
    <span class="badge badge-secondary">已归档</span>
{% endif %}

<!-- ifequal/ifnotequal -->
{% ifequal article.status 'published' %}
    <span>已发布</span>
{% endifequal %}

<!-- for 循环 -->
{% for article in articles %}
    <div class="article-card">
        <h2>{{ article.title }}</h2>
        <p>{{ article.summary }}</p>
        <!-- forloop内置变量 -->
        <span>第 {{ forloop.counter }} 篇</span>
        <span>共 {{ forloop.revcounter }} 篇剩余</span>
        {% if forloop.first %}<span>第一篇</span>{% endif %}
        {% if forloop.last %}<span>最后一篇</span>{% endif %}
    </div>
{% empty %}
    <p>暂无文章</p>
{% endfor %}

<!-- for...reverse 反向循环 -->
{% for article in articles reversed %}
    <p>{{ article.title }}</p>
{% endfor %}

<!-- 遍历字典 -->
{% for key, value in settings.items %}
    <p>{{ key }}: {{ value }}</p>
{% endfor %}

3.3 常用过滤器

<!-- 日期格式化 -->
{{ article.created_at|date:"Y年m月d日 H:i" }}
{{ article.created_at|timesince }}  {# "3小时前" #}

<!-- 字符串处理 -->
{{ article.title|truncatewords:30 }}     {# 截取30个单词 #}
{{ article.title|truncatechars:50 }}     {# 截取50个字符 #}
{{ article.content|truncatewords_html:50 }} {# 安全截取HTML #}
{{ user.email|lower }}                   {# 转小写 #}
{{ user.username|upper }}                {# 转大写 #}
{{ article.title|title }}                {# 标题格式 #}
{{ text|capfirst }}                      {# 首字母大写 #}
{{ text|slugify }}                       {# 转为slug格式 #}
{{ text|striptags }}                     {# 去除HTML标签 #}

<!-- 数值处理 -->
{{ article.views|floatformat:2 }}        {# 保留2位小数 #}
{{ price|floatformat }}                  {# 默认保留1位 #}

<!-- 默认值 -->
{{ article.summary|default:"暂无摘要" }}
{{ article.cover|default_if_none:"/static/default.jpg" }}

<!-- 安全输出 -->
{{ article.content|safe }}               {# 不转义HTML #}
{{ article.content|escape }}             {# 转义HTML(默认行为) #}
{{ article.content|escapejs }}           {# 转义JS #}

<!-- 列表操作 -->
{{ articles|length }}                    {# 列表长度 #}
{{ articles|join:", " }}                 {# 用逗号连接 #}
{{ articles|first }}                     {# 第一个元素 #}
{{ articles|last }}                      {# 最后一个元素 #}

<!-- 链式使用过滤器 -->
{{ article.content|striptags|truncatewords:50|safe }}

<!-- 自定义过滤器参数 -->
{{ value|cut:" " }}                      {# 去除所有空格 #}
{{ value|add:5 }}                        {# 加5 #}

四、模板继承

模板继承是Django模板系统最强大的特性之一,它允许你定义一个基础模板,然后子模板只需要填充特定的区块。

4.1 基础模板(base.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>
    {% block extra_css %}
    <link rel="stylesheet" href="{% static 'css/style.css' %}">
    {% endblock %}
</head>
<body>
    <!-- 导航栏 -->
    {% include "includes/navbar.html" %}

    <!-- 主内容区 -->
    <div class="container">
        {% block content %}
        {# 子模板将覆盖此区块 #}
        {% endblock %}
    </div>

    <!-- 侧边栏 -->
    {% block sidebar %}
    {% include "includes/sidebar.html" %}
    {% endblock %}

    <!-- 页脚 -->
    {% include "includes/footer.html" %}

    {% block extra_js %}
    <script src="{% static 'js/main.js' %}"></script>
    {% endblock %}
</body>
</html>

4.2 子模板继承

<!-- blog/templates/blog/article_list.html -->
{% extends "base.html" %}
{% load static %}

{% block title %}文章列表 - {{ block.super }}{% endblock %}

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

{% block content %}
<div class="article-list">
    {% for article in articles %}
    <article class="article-card">
        {% include "blog/includes/article_card.html" with article=article show_category=True %}
    </article>
    {% empty %}
    <div class="empty-state">
        <p>暂无文章</p>
    </div>
    {% endfor %}
</div>

{% include "includes/pagination.html" with page_obj=page_obj %}
{% endblock %}

{% block sidebar %}
{{ block.super }}
<div class="widget">
    <h3>热门标签</h3>
    {% for tag in tags %}
    <a href="{% url 'blog:tag_articles' tag.slug %}">{{ tag.name }}</a>
    {% endfor %}
</div>
{% endblock %}

4.3 三层继承

<!-- templates/base.html -->
{# 第一层:全局基础 #}
{% block title %}{% endblock %}
{% block content %}{% endblock %}

<!-- templates/blog_base.html -->
{# 第二层:博客专用基础 #}
{% extends "base.html" %}
{% block content %}
<div class="blog-layout">
    <main>{% block main_content %}{% endblock %}</main>
    <aside>{% block blog_sidebar %}{% endblock %}</aside>
</div>
{% endblock %}

<!-- blog/templates/blog/article_detail.html -->
{# 第三层:具体页面 #}
{% extends "blog_base.html" %}
{% block main_content %}
<h1>{{ article.title }}</h1>
{{ article.content|safe }}
{% endblock %}

五、模板包含(include)

<!-- 基本包含 -->
{% include "includes/pagination.html" %}

<!-- 传递变量 -->
{% include "includes/article_card.html" with article=article %}

<!-- 传递多个变量 -->
{% include "includes/article_card.html" with article=article show_tags=True show_author=True %}

<!-- 只传递特定变量(隔离上下文) -->
{% include "includes/widget.html" with title="热门文章" items=hot_articles only %}

可复用模板片段示例:

<!-- templates/includes/pagination.html -->
{% if page_obj.has_other_pages %}
<nav class="pagination">
    {% if page_obj.has_previous %}
    <a href="?page={{ page_obj.previous_page_number }}">&laquo; 上一页</a>
    {% endif %}

    {% for num in page_obj.paginator.page_range %}
        {% if page_obj.number == num %}
        <span class="current">{{ num }}</span>
        {% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
        <a href="?page={{ num }}">{{ num }}</a>
        {% endif %}
    {% endfor %}

    {% if page_obj.has_next %}
    <a href="?page={{ page_obj.next_page_number }}">下一页 &raquo;</a>
    {% endif %}
</nav>
{% endif %}

六、自定义模板标签和过滤器

6.1 创建自定义标签库

# blog/templatetags/__init__.py  (空文件)
# blog/templatetags/blog_extras.py

from django import template
from django.utils.safestring import mark_safe
import markdown

register = template.Library()


# ===== 自定义过滤器 =====

@register.filter(name='truncate_cn')
def truncate_cn(value, length):
    """截取中文字符串"""
    if len(value) > length:
        return value[:length] + '...'
    return value


@register.filter
def multiply(value, arg):
    """乘法过滤器"""
    try:
        return float(value) * float(arg)
    except (ValueError, TypeError):
        return ''


@register.filter
def markdown_to_html(text):
    """Markdown转HTML"""
    return mark_safe(markdown.markdown(text, extensions=['codehilite', 'fenced_code']))


# ===== 自定义标签 =====

@register.simple_tag
def get_recent_articles(count=5):
    """获取最近发布的文章"""
    return Article.objects.filter(
        status='published'
    ).order_by('-created_at')[:count]


@register.simple_tag(takes_context=True)
def total_articles(context):
    """获取当前用户的文章总数"""
    user = context['request'].user
    if user.is_authenticated:
        return Article.objects.filter(author=user).count()
    return 0


@register.inclusion_tag('blog/includes/article_card.html')
def show_article_card(article, show_tags=False):
    """渲染文章卡片组件"""
    return {
        'article': article,
        'show_tags': show_tags,
    }


@register.inclusion_tag('blog/includes/archive_widget.html')
def show_archive():
    """归档组件"""
    from django.db.models import Count
    archives = Article.objects.filter(
        status='published'
    ).dates('created_at', 'month', order='DESC')
    return {'archives': archives}

6.2 在模板中使用自定义标签

{% load blog_extras %}

{# 使用自定义过滤器 #}
<p>{{ article.content|truncate_cn:100 }}</p>
<p>{{ price|multiply:1.08 }}</p>

{# 使用simple_tag #}
{% get_recent_articles 5 as recent_articles %}
{% for article in recent_articles %}
    <a href="{{ article.get_absolute_url }}">{{ article.title }}</a>
{% endfor %}

{# 使用inclusion_tag(自动渲染关联模板) #}
{% show_article_card article show_tags=True %}

七、静态文件管理

{% load static %}

<!-- 引用CSS -->
<link rel="stylesheet" href="{% static 'css/style.css' %}">

<!-- 引用JS -->
<script src="{% static 'js/main.js' %}"></script>

<!-- 引用图片 -->
<img src="{% static 'images/logo.png' %}" alt="Logo">

<!-- 动态静态文件路径 -->
{% static "css/" as css_path %}
<link rel="stylesheet" href="{{ css_path }}theme-{{ user.theme }}.css">

八、上下文处理器

上下文处理器可以让所有模板都自动获取某些变量,无需在每个视图中手动传递:

# blog/context_processors.py

from .models import Category, Tag

def site_settings(request):
    """全局站点设置(所有模板都可访问)"""
    return {
        'site_name': '我的博客',
        'site_description': '一个关于技术的博客',
        'categories': Category.objects.all(),
        'popular_tags': Tag.objects.all()[:20],
    }

def user_notifications(request):
    """用户通知"""
    if request.user.is_authenticated:
        unread_count = request.user.notifications.filter(read=False).count()
        return {'unread_notifications': unread_count}
    return {}

九、总结

本章我们学习了Django模板系统的核心知识:

  • 模板语法:变量、标签、过滤器、注释
  • 模板继承机制(extends/block)和三层继承策略
  • 模板包含(include)和可复用组件
  • 自定义模板标签和过滤器的开发
  • 静态文件管理和上下文处理器

下一章我们将学习 Django表单处理与数据验证,掌握如何安全地处理用户输入。

发表回复

后才能评论