跳到主要内容

模板系统

Django 模板系统是一个强大的文本渲染引擎,用于将数据展示为 HTML 页面。它提供了简洁的语法,让设计师和开发者可以协作开发。

模板基础

渲染模板

在视图中使用 render() 函数渲染模板:

from django.shortcuts import render


def post_list(request):
posts = Post.objects.all()
return render(request, 'blog/post_list.html', {
'posts': posts,
'title': '文章列表'
})

模板查找路径

Django 按以下顺序查找模板:

  1. 项目配置的模板目录(DIRS
  2. 各应用的 templates 目录
# settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'], # 项目级模板目录
'APP_DIRS': True, # 在应用的 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',
],
},
},
]

模板语法

变量

使用双花括号 {{ }} 输出变量:

<!-- 输出变量 -->
<h1>{{ title }}</h1>

<!-- 输出对象属性 -->
<p>作者:{{ post.author.username }}</p>
<p>邮箱:{{ post.author.email }}</p>

<!-- 输出字典值 -->
<p>{{ user.profile.bio }}</p>

<!-- 输出列表元素 -->
<p>{{ items.0 }}</p>

过滤器

使用管道符 | 应用过滤器:

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

<!-- 字符串处理 -->
{{ text|truncatewords:30 }} <!-- 截取 30 个单词 -->
{{ text|truncatechars:100 }} <!-- 截取 100 个字符 -->
{{ text|slice:":10" }} <!-- 切片 -->
{{ text|wordcount }} <!-- 单词数 -->
{{ text|linebreaks }} <!-- 换行转 <p> 和 <br> -->
{{ text|linebreaksbr }} <!-- 换行转 <br> -->

<!-- 日期格式化 -->
{{ post.created|date:"Y-m-d H:i" }}
{{ post.created|date:"Y年m月d日" }}
{{ post.created|timesince }} <!-- 时间差 -->

<!-- 数值格式化 -->
{{ price|floatformat:2 }} <!-- 保留 2 位小数 -->
{{ number|add:5 }} <!-- 加法 -->
{{ number|add:"-5" }} <!-- 减法 -->

<!-- 默认值 -->
{{ value|default:"无" }} <!-- 变量为 False 时显示默认值 -->
{{ value|default_if_none:"无" }} <!-- 变量为 None 时显示默认值 -->

<!-- 长度 -->
{{ items|length }} <!-- 长度 -->

<!-- 安全输出 -->
{{ html|safe }} <!-- 不转义 HTML -->
{{ html|escape }} <!-- 转义 HTML -->

<!-- 链接 -->
{{ text|urlize }} <!-- 将 URL 转为链接 -->

<!-- JSON -->
{{ data|json_script:"my-data" }} <!-- 生成 JSON script 标签 -->

标签

使用 {% %} 包裹标签:

if 条件判断

{% if user.is_authenticated %}
<p>欢迎,{{ user.username }}!</p>
{% else %}
<p><a href="/login/">登录</a></p>
{% endif %}

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

{% if items|length > 0 %}
<p>共有 {{ items|length }} 个项目</p>
{% endif %}

{% if user.is_staff and user.is_active %}
<p>活跃的管理员</p>
{% endif %}

{% if user.is_superuser or user.is_staff %}
<p>管理员用户</p>
{% endif %}

for 循环

<!-- 基本循环 -->
<ul>
{% for item in items %}
<li>{{ forloop.counter }}. {{ item.name }}</li>
{% endfor %}
</ul>

<!-- 循环变量 -->
{% for item in items %}
{{ forloop.counter }} <!-- 从 1 开始的索引 -->
{{ forloop.counter0 }} <!-- 从 0 开始的索引 -->
{{ forloop.revcounter }} <!-- 从末尾开始的索引(从 1 开始) -->
{{ forloop.revcounter0 }} <!-- 从末尾开始的索引(从 0 开始) -->
{{ forloop.first }} <!-- 是否是第一个元素 -->
{{ forloop.last }} <!-- 是否是最后一个元素 -->
{{ forloop.parentloop }} <!-- 父级循环(嵌套循环时) -->
{% endfor %}

<!-- 空列表处理 -->
<ul>
{% for item in items %}
<li>{{ item.name }}</li>
{% empty %}
<li>暂无数据</li>
{% endfor %}
</ul>

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

<!-- 嵌套循环 -->
<table>
{% for row in table %}
<tr>
{% for cell in row %}
<td>{{ cell }}</td>
{% endfor %}
</tr>
{% endfor %}
</table>

for 循环示例

<!-- 带序号的列表 -->
<ol>
{% for post in posts %}
<li>
<a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
{% if forloop.first %}<span class="badge">最新</span>{% endif %}
</li>
{% endfor %}
</ol>

<!-- 分组显示 -->
<div class="row">
{% for item in items %}
<div class="col-md-4">
{{ item.name }}
</div>
{% if forloop.counter|divisibleby:3 %}
</div><div class="row">
{% endif %}
{% endfor %}
</div>

url 标签

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

<!-- 带参数的 URL -->
<a href="{% url 'blog:post_detail' pk=post.pk %}">{{ post.title }}</a>

<!-- 保存 URL 到变量 -->
{% url 'blog:post_list' as post_list_url %}
<a href="{{ post_list_url }}">文章列表</a>

with 标签

<!-- 为复杂变量创建别名 -->
{% with total=items|length %}
<p>共有 {{ total }} 个项目</p>
{% endwith %}

<!-- 多个别名 -->
{% with name=user.get_full_name email=user.email %}
<p>{{ name }} ({{ email }})</p>
{% endwith %}

include 标签

<!-- 包含其他模板 -->
{% include 'sidebar.html' %}

<!-- 包含并传递变量 -->
{% include 'card.html' with title="推荐文章" posts=recommended_posts %}

<!-- 仅传递特定变量 -->
{% include 'card.html' with title="最新文章" only %}

load 标签

<!-- 加载自定义标签库 -->
{% load static %}
{% load custom_tags %}

<!-- 加载多个标签库 -->
{% load static custom_tags %}

模板继承

模板继承是 Django 模板系统最强大的功能之一,它允许创建一个基础模板,然后在其基础上扩展。

基础模板

<!-- 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 %}{% endblock %}
</head>
<body>
<header>
<nav>
<a href="{% url 'home' %}">首页</a>
<a href="{% url 'blog:post_list' %}">博客</a>
<a href="{% url 'about' %}">关于</a>
</nav>
</header>

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

<footer>
<p>&copy; 2024 我的网站</p>
</footer>

{% block extra_js %}{% endblock %}
</body>
</html>

子模板

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

{% block title %}文章列表 - 我的网站{% endblock %}

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

{% block content %}
<h1>文章列表</h1>
<ul>
{% for post in posts %}
<li>
<h2><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h2>
<p>{{ post.content|truncatewords:50 }}</p>
</li>
{% endfor %}
</ul>
{% endblock %}

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

多层继承

<!-- templates/base.html -->
<html>
<head>
<title>{% block title %}网站{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>

<!-- templates/blog/base.html -->
{% extends 'base.html' %}

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

{% block content %}
<div class="blog-container">
<aside>{% block sidebar %}{% endblock %}</aside>
<article>{% block article %}{% endblock %}</article>
</div>
{% endblock %}

<!-- templates/blog/post_detail.html -->
{% extends 'blog/base.html' %}

{% block title %}{{ post.title }} - {{ block.super }}{% endblock %}

{% block sidebar %}
<h3>相关文章</h3>
<ul>
{% for related in related_posts %}
<li><a href="{{ related.get_absolute_url }}">{{ related.title }}</a></li>
{% endfor %}
</ul>
{% endblock %}

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

block.super

使用 {{ block.super }} 保留父模板的内容:

<!-- 父模板 -->
{% block styles %}
<link rel="stylesheet" href="base.css">
{% endblock %}

<!-- 子模板 -->
{% block styles %}
{{ block.super }}
<link rel="stylesheet" href="custom.css">
{% endblock %}

静态文件

配置静态文件

# settings.py
STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR / 'static']
STATIC_ROOT = BASE_DIR / 'staticfiles'

在模板中使用静态文件

{% load static %}

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

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

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

<!-- 使用变量 -->
<img src="{% static image_path %}" alt="">

<!-- 生成静态文件 URL -->
{% get_static_prefix as STATIC_URL %}
<link rel="stylesheet" href="{{ STATIC_URL }}css/style.css">

自定义标签和过滤器

创建自定义标签库

在应用目录下创建 templatetags 目录:

blog/
├── templatetags/
│ ├── __init__.py
│ └── blog_tags.py
└── ...

自定义过滤器

# blog/templatetags/blog_tags.py
from django import template
from django.utils.safestring import mark_safe

register = template.Library()


@register.filter(name='markdown')
def markdown_filter(text):
"""将 Markdown 转换为 HTML"""
import markdown
return mark_safe(markdown.markdown(text))


@register.filter
def reading_time(text, wpm=200):
"""计算阅读时间(分钟)"""
word_count = len(text.split())
minutes = max(1, word_count // wpm)
return f'{minutes} 分钟阅读'


@register.filter(is_safe=True)
def highlight(text, word):
"""高亮关键词"""
return mark_safe(text.replace(word, f'<mark>{word}</mark>'))

使用自定义过滤器:

{% load blog_tags %}

{{ post.content|markdown }}
{{ post.content|reading_time }}
{{ post.content|highlight:"Django" }}

自定义简单标签

# blog/templatetags/blog_tags.py
from django import template
from ..models import Post, Category

register = template.Library()


@register.simple_tag
def total_posts():
"""返回文章总数"""
return Post.objects.filter(status='published').count()


@register.simple_tag
def recent_posts(count=5):
"""返回最新文章"""
return Post.objects.filter(status='published')[:count]


@register.simple_tag
def categories_with_count():
"""返回分类及其文章数"""
return Category.objects.annotate(
post_count=Count('posts')
).filter(post_count__gt=0)

使用简单标签:

{% load blog_tags %}

<p>共有 {% total_posts %} 篇文章</p>

{% recent_posts 10 as posts %}
<ul>
{% for post in posts %}
<li>{{ post.title }}</li>
{% endfor %}
</ul>

自定义包含标签

# blog/templatetags/blog_tags.py
@register.inclusion_tag('blog/tags/post_card.html')
def show_post_card(post):
"""显示文章卡片"""
return {'post': post}


@register.inclusion_tag('blog/tags/category_list.html')
def show_categories():
"""显示分类列表"""
categories = Category.objects.annotate(
post_count=Count('posts')
)
return {'categories': categories}

模板文件:

<!-- blog/templates/blog/tags/post_card.html -->
<div class="post-card">
<h3>{{ post.title }}</h3>
<p>{{ post.content|truncatewords:30 }}</p>
<a href="{{ post.get_absolute_url }}">阅读更多</a>
</div>

使用包含标签:

{% load blog_tags %}

{% show_post_card post %}
{% show_categories %}

模板最佳实践

1. 模板组织结构

templates/
├── base.html # 基础模板
├── base_blog.html # 博客基础模板
├── includes/ # 可复用组件
│ ├── header.html
│ ├── footer.html
│ ├── sidebar.html
│ └── pagination.html
└── blog/ # 应用模板
├── post_list.html
├── post_detail.html
└── tags/ # 标签模板
└── post_card.html

2. 使用模板继承

<!-- 好的做法:使用继承 -->
{% extends 'base.html' %}
{% block content %}
<!-- 页面内容 -->
{% endblock %}

<!-- 不好的做法:重复 HTML 结构 -->
<html>
<head>...</head>
<body>
<!-- 重复的内容 -->
</body>
</html>

3. 使用 include 复用组件

<!-- 分页组件 -->
<!-- templates/includes/pagination.html -->
<nav aria-label="分页">
<ul class="pagination">
{% if page_obj.has_previous %}
<li><a href="?page={{ page_obj.previous_page_number }}">上一页</a></li>
{% endif %}

{% for num in page_obj.paginator.page_range %}
<li {% if page_obj.number == num %}class="active"{% endif %}>
<a href="?page={{ num }}">{{ num }}</a>
</li>
{% endfor %}

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

使用:

{% include 'includes/pagination.html' with page_obj=posts %}

4. 避免在模板中写复杂逻辑

<!-- 不好的做法 -->
{% for post in posts %}
{% if post.status == 'published' and post.author.is_active %}
{% if post.category.name == '技术' %}
<li class="tech-post">{{ post.title }}</li>
{% else %}
<li>{{ post.title }}</li>
{% endif %}
{% endif %}
{% endfor %}

<!-- 好的做法:在视图中过滤 -->
{% for post in published_tech_posts %}
<li class="tech-post">{{ post.title }}</li>
{% endfor %}
{% for post in published_other_posts %}
<li>{{ post.title }}</li>
{% endfor %}