Django 模板系统
2026/3/20大约 15 分钟
Django 模板系统
一、模板基础
1.1 模板引擎概述
Django 内置了强大的模板引擎(DTL - Django Template Language),用于将动态数据渲染到 HTML 页面中。模板引擎实现了表现层与业务逻辑的分离,遵循 MVC/MTV 设计模式。
# 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',
],
},
},
]
1.2 模板目录结构
project/
├── templates/ # 全局模板目录
│ ├── base.html # 基础模板
│ ├── includes/ # 可复用片段
│ │ ├── header.html
│ │ ├── footer.html
│ │ └── sidebar.html
│ └── errors/ # 错误页面
│ ├── 404.html
│ └── 500.html
├── blog/
│ └── templates/
│ └── blog/ # 应用模板(推荐二级目录)
│ ├── article_list.html
│ ├── article_detail.html
│ └── partials/
│ └── article_card.html
└── users/
└── templates/
└── users/
├── login.html
└── profile.html
1.3 渲染模板
from django.shortcuts import render
from django.template import loader
from django.http import HttpResponse
# 方式一:使用 render 快捷函数(推荐)
def article_list(request):
articles = Article.objects.all()
return render(request, 'blog/article_list.html', {
'articles': articles,
'title': '文章列表',
})
# 方式二:使用 loader
def article_detail(request, pk):
template = loader.get_template('blog/article_detail.html')
context = {'article': Article.objects.get(pk=pk)}
return HttpResponse(template.render(context, request))
# 方式三:直接渲染字符串
from django.template import Template, Context
def dynamic_template(request):
template_string = "Hello, {{ name }}!"
template = Template(template_string)
context = Context({'name': 'World'})
return HttpResponse(template.render(context))
二、模板语法
2.1 变量
<!-- 基本变量 -->
<h1>{{ article.title }}</h1>
<p>作者:{{ article.author.username }}</p>
<!-- 字典访问 -->
<p>{{ user_info.name }}</p>
<p>{{ user_info.email }}</p>
<!-- 列表访问 -->
<p>第一项:{{ items.0 }}</p>
<p>最后一项:{{ items.last }}</p>
<!-- 方法调用(不能带参数) -->
<p>{{ article.get_absolute_url }}</p>
<p>{{ name.upper }}</p>
<!-- 变量不存在时显示空字符串 -->
<p>{{ undefined_variable }}</p>
<!-- 使用 default 过滤器设置默认值 -->
<p>{{ user.nickname|default:"匿名用户" }}</p>
2.2 标签(Tags)
条件判断
<!-- if 语句 -->
{% if user.is_authenticated %}
<p>欢迎回来,{{ user.username }}!</p>
{% elif user.is_anonymous %}
<p>请先登录</p>
{% else %}
<p>未知状态</p>
{% endif %}
<!-- 比较运算符 -->
{% if article.views > 1000 %}
<span class="hot">热门文章</span>
{% endif %} {% if status == "published" %}
<span class="badge">已发布</span>
{% endif %} {% if article.category in featured_categories %}
<span>推荐分类</span>
{% endif %}
<!-- 逻辑运算符 -->
{% if user.is_staff and user.is_active %}
<a href="/admin/">管理后台</a>
{% endif %} {% if not article.is_draft %}
<p>{{ article.content }}</p>
{% endif %} {% if article.status == 'published' or user == article.author %}
<p>{{ article.content }}</p>
{% endif %}
循环
<!-- 基本循环 -->
<ul>
{% for article in articles %}
<li>{{ article.title }}</li>
{% endfor %}
</ul>
<!-- 带 empty 的循环 -->
<ul>
{% for article in articles %}
<li>{{ article.title }}</li>
{% empty %}
<li>暂无文章</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 }}
<!-- 父循环的 forloop 对象 -->
{% endfor %}
<!-- 循环字典 -->
{% for key, value in data.items %}
<p>{{ key }}: {{ value }}</p>
{% endfor %}
<!-- 嵌套循环 -->
{% for category in categories %}
<h2>{{ category.name }}</h2>
<ul>
{% for article in category.articles.all %}
<li>
{{ forloop.parentloop.counter }}.{{ forloop.counter }} {{ article.title }}
</li>
{% endfor %}
</ul>
{% endfor %}
<!-- 反向循环 -->
{% for article in articles reversed %}
<li>{{ article.title }}</li>
{% endfor %}
其他常用标签
<!-- 注释 -->
{# 这是单行注释 #} {% comment "可选的注释说明" %} 这是多行注释 可以包含任何内容
{{ variable }} 不会被渲染 {% endcomment %}
<!-- with 临时变量 -->
{% with total=articles.count %}
<p>共 {{ total }} 篇文章</p>
{% endwith %} {% with article.author.profile as profile %}
<p>{{ profile.bio }}</p>
<img src="{{ profile.avatar.url }}" />
{% endwith %}
<!-- url 反向解析 -->
<a href="{% url 'article_detail' pk=article.pk %}">查看详情</a>
<a href="{% url 'blog:article_list' %}">文章列表</a>
<a href="{% url 'category' slug=category.slug %}?page=2">分类</a>
<!-- static 静态文件 -->
{% load static %}
<link rel="stylesheet" href="{% static 'css/style.css' %}" />
<img src="{% static 'images/logo.png' %}" />
<!-- csrf_token -->
<form method="post">
{% csrf_token %}
<input type="text" name="title" />
<button type="submit">提交</button>
</form>
<!-- now 当前时间 -->
<p>当前时间:{% now "Y-m-d H:i:s" %}</p>
<p>今年是 {% now "Y" %} 年</p>
<!-- spaceless 移除空白 -->
{% spaceless %}
<p>
<a href="#">链接</a>
</p>
{% endspaceless %}
<!-- 输出:<p><a href="#">链接</a></p> -->
<!-- verbatim 原样输出 -->
{% verbatim %} {{ 这不会被解析 }} {% 这也不会被解析 %} {% endverbatim %}
<!-- cycle 循环值 -->
{% for item in items %}
<tr class="{% cycle 'odd' 'even' %}">
<td>{{ item }}</td>
</tr>
{% endfor %}
<!-- firstof 第一个真值 -->
{% firstof var1 var2 var3 "默认值" %}
<!-- lorem 生成占位文本 -->
{% lorem %}
<!-- 一段随机文本 -->
{% lorem 2 %}
<!-- 两段随机文本 -->
{% lorem 10 w %}
<!-- 10 个随机单词 -->
{% lorem 3 p %}
<!-- 3 个段落 -->
2.3 过滤器(Filters)
字符串过滤器
<!-- 大小写转换 -->
{{ name|upper }}
<!-- HELLO -->
{{ name|lower }}
<!-- hello -->
{{ name|title }}
<!-- Hello World -->
{{ name|capfirst }}
<!-- Hello world -->
<!-- 截断 -->
{{ text|truncatechars:100 }}
<!-- 截断到100字符 -->
{{ text|truncatewords:20 }}
<!-- 截断到20个单词 -->
{{ html|truncatechars_html:200 }}
<!-- 保留HTML标签的截断 -->
<!-- 字符串处理 -->
{{ text|striptags }}
<!-- 移除HTML标签 -->
{{ text|linebreaks }}
<!-- \n 转 <p> 和 <br> -->
{{ text|linebreaksbr }}
<!-- \n 转 <br> -->
{{ text|safe }}
<!-- 标记为安全,不转义 -->
{{ text|escape }}
<!-- HTML转义 -->
{{ text|escapejs }}
<!-- JavaScript转义 -->
{{ text|slugify }}
<!-- URL友好格式 -->
{{ text|wordwrap:80 }}
<!-- 每80字符换行 -->
{{ text|center:20 }}
<!-- 居中对齐 -->
{{ text|ljust:20 }}
<!-- 左对齐 -->
{{ text|rjust:20 }}
<!-- 右对齐 -->
{{ text|cut:" " }}
<!-- 移除所有空格 -->
<!-- 默认值 -->
{{ value|default:"暂无" }}
<!-- 假值时显示默认 -->
{{ value|default_if_none:"未设置" }}
<!-- None时显示默认 -->
<!-- 字符串格式化 -->
{{ text|addslashes }}
<!-- 添加转义斜杠 -->
{{ text|urlize }}
<!-- URL转链接 -->
{{ text|urlizetrunc:50 }}
<!-- URL转链接并截断 -->
数字过滤器
<!-- 数字格式化 -->
{{ number|add:10 }}
<!-- 加法 -->
{{ number|floatformat }}
<!-- 浮点格式化 -->
{{ number|floatformat:2 }}
<!-- 保留2位小数 -->
{{ number|filesizeformat }}
<!-- 文件大小格式 -->
{{ number|intcomma }}
<!-- 千位分隔符 -->
{{ number|ordinal }}
<!-- 序数词 -->
<!-- 整除 -->
{{ number|divisibleby:3 }}
<!-- 是否能被3整除 -->
<!-- 示例 -->
{{ 1234567|intcomma }}
<!-- 1,234,567 -->
{{ 1024|filesizeformat }}
<!-- 1.0 KB -->
{{ 3.14159|floatformat:2 }}
<!-- 3.14 -->
日期过滤器
<!-- 日期格式化 -->
{{ date|date:"Y-m-d" }}
<!-- 2024-06-15 -->
{{ date|date:"Y年m月d日" }}
<!-- 2024年06月15日 -->
{{ date|date:"F j, Y" }}
<!-- June 15, 2024 -->
{{ date|time:"H:i:s" }}
<!-- 14:30:00 -->
{{ date|date:"D, M d" }}
<!-- Sat, Jun 15 -->
<!-- 常用格式化字符 -->
<!-- Y: 四位年份, y: 两位年份 -->
<!-- m: 月份(01-12), n: 月份(1-12), F: 月份全名, M: 月份缩写 -->
<!-- d: 日期(01-31), j: 日期(1-31) -->
<!-- H: 小时(00-23), h: 小时(01-12), i: 分钟, s: 秒 -->
<!-- D: 星期缩写, l: 星期全名, w: 星期数字(0-6) -->
<!-- 时间差 -->
{{ date|timesince }}
<!-- 3 天前 -->
{{ date|timeuntil }}
<!-- 还有 2 小时 -->
{{ date|timesince:other_date }}
<!-- 相对于另一个日期 -->
<!-- 使用 settings 中的格式 -->
{{ date|date:"SHORT_DATE_FORMAT" }} {{ date|date:"DATE_FORMAT" }}
列表过滤器
<!-- 列表操作 -->
{{ list|first }}
<!-- 第一个元素 -->
{{ list|last }}
<!-- 最后一个元素 -->
{{ list|length }}
<!-- 长度 -->
{{ list|length_is:5 }}
<!-- 长度是否为5 -->
{{ list|join:", " }}
<!-- 连接成字符串 -->
{{ list|slice:":5" }}
<!-- 切片 -->
{{ list|random }}
<!-- 随机元素 -->
<!-- 排序 -->
{{ list|dictsort:"name" }}
<!-- 按字典键排序 -->
{{ list|dictsortreversed:"date" }}
<!-- 按字典键倒序 -->
<!-- 去重和展平 -->
{{ list|unordered_list }}
<!-- 生成无序列表 -->
其他过滤器
<!-- 类型判断和转换 -->
{{ value|yesno:"是,否,不确定" }}
<!-- 布尔值转换 -->
{{ value|pluralize }}
<!-- 复数形式 -->
{{ value|pluralize:"es" }}
<!-- item/items -->
{{ value|pluralize:"y,ies" }}
<!-- story/stories -->
<!-- JSON -->
{{ data|json_script:"data-id" }}
<!-- 输出到 script 标签 -->
<!-- 链式过滤器 -->
{{ text|lower|truncatewords:10|safe }}
2.4 自动转义
<!-- 默认情况下,变量会被自动HTML转义 -->
{{ user_input }}
<!-- <script>alert('xss')</script> 会被转义 -->
<!-- 禁用自动转义 -->
{{ html_content|safe }} {% autoescape off %} {{ html_content }} {{ another_html
}} {% endautoescape %}
<!-- 强制转义 -->
{% autoescape off %} {{ user_input|escape }} {% endautoescape %}
<!-- 或使用 force_escape -->
{{ user_input|force_escape }}
三、模板继承
3.1 基础模板
<!-- 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 %} - MySite</title>
{% load static %}
<link rel="stylesheet" href="{% static 'css/base.css' %}" />
{% block extra_css %}{% endblock %}
</head>
<body>
<!-- 头部 -->
<header>
{% block header %}
<nav>
<a href="{% url 'home' %}">首页</a>
<a href="{% url 'blog:list' %}">博客</a>
</nav>
{% endblock %}
</header>
<!-- 主内容 -->
<main>
{% block breadcrumb %}{% endblock %}
<div class="container">
{% block content %}
<p>默认内容</p>
{% endblock %}
</div>
</main>
<!-- 侧边栏 -->
<aside>
{% block sidebar %}
<div class="widget">
<h3>热门文章</h3>
<!-- 默认侧边栏内容 -->
</div>
{% endblock %}
</aside>
<!-- 底部 -->
<footer>
{% block footer %}
<p>© 2024 MySite</p>
{% endblock %}
</footer>
<script src="{% static 'js/base.js' %}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
3.2 子模板
<!-- templates/blog/article_list.html -->
{% extends "base.html" %} {% load static %} {% block title %}文章列表{% endblock
%} {% block extra_css %}
<link rel="stylesheet" href="{% static 'blog/css/article.css' %}" />
{% endblock %} {% block breadcrumb %}
<nav class="breadcrumb">
<a href="{% url 'home' %}">首页</a> >
<span>文章列表</span>
</nav>
{% endblock %} {% block content %}
<h1>文章列表</h1>
{% for article in articles %}
<article class="article-card">
<h2>{{ article.title }}</h2>
<p class="meta">
{{ article.created_at|date:"Y-m-d" }} | {{ article.author.username }}
</p>
<p>{{ article.content|truncatewords:30 }}</p>
<a href="{{ article.get_absolute_url }}">阅读更多</a>
</article>
{% empty %}
<p>暂无文章</p>
{% endfor %}
<!-- 分页 -->
{% include "includes/pagination.html" %} {% endblock %} {% block sidebar %} {{
block.super }}
<!-- 保留父模板的侧边栏内容 -->
<div class="widget">
<h3>分类</h3>
<ul>
{% for category in categories %}
<li><a href="{{ category.get_absolute_url }}">{{ category.name }}</a></li>
{% endfor %}
</ul>
</div>
{% endblock %} {% block extra_js %}
<script src="{% static 'blog/js/article.js' %}"></script>
{% endblock %}
3.3 多级继承
<!-- templates/base.html - 第一级 -->
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}{% endblock %}</title>
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
<!-- templates/base_blog.html - 第二级 -->
{% extends "base.html" %} {% block body %}
<div class="blog-layout">
<header>{% block header %}博客{% endblock %}</header>
<main>{% block content %}{% endblock %}</main>
<aside>{% block sidebar %}{% endblock %}</aside>
</div>
{% endblock %}
<!-- templates/blog/article_detail.html - 第三级 -->
{% extends "base_blog.html" %} {% block title %}{{ article.title }}{% endblock
%} {% block content %}
<article>
<h1>{{ article.title }}</h1>
{{ article.content|safe }}
</article>
{% endblock %}
四、模板包含
4.1 include 标签
<!-- 基本包含 -->
{% include "includes/header.html" %}
<!-- 传递变量 -->
{% include "includes/article_card.html" with article=featured_article %}
<!-- 只传递指定变量(限制上下文) -->
{% include "includes/user_card.html" with user=article.author only %}
<!-- 条件包含 -->
{% if user.is_authenticated %} {% include "includes/user_menu.html" %} {% else
%} {% include "includes/login_prompt.html" %} {% endif %}
4.2 可复用组件
<!-- templates/includes/pagination.html -->
{% if page_obj.has_other_pages %}
<nav class="pagination">
{% if page_obj.has_previous %}
<a href="?page=1">« 首页</a>
<a href="?page={{ page_obj.previous_page_number }}">上一页</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 }}">下一页</a>
<a href="?page={{ page_obj.paginator.num_pages }}">末页 »</a>
{% endif %}
</nav>
{% endif %}
<!-- templates/includes/message.html -->
{% if messages %}
<div class="messages">
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
{{ message }}
<button class="close">×</button>
</div>
{% endfor %}
</div>
{% endif %}
<!-- templates/includes/form_field.html -->
<div class="form-group {% if field.errors %}has-error{% endif %}">
<label for="{{ field.id_for_label }}">
{{ field.label }} {% if field.field.required %}<span class="required"
>*</span
>{% endif %}
</label>
{{ field }} {% if field.help_text %}
<small class="help-text">{{ field.help_text }}</small>
{% endif %} {% for error in field.errors %}
<span class="error">{{ error }}</span>
{% endfor %}
</div>
五、自定义模板标签
5.1 创建模板标签
# blog/templatetags/blog_tags.py
from django import template
from django.utils.html import format_html
from blog.models import Article, Category
register = template.Library()
# 简单标签
@register.simple_tag
def total_articles():
"""返回文章总数"""
return Article.objects.count()
# 带参数的简单标签
@register.simple_tag
def recent_articles(count=5):
"""返回最近的文章"""
return Article.objects.order_by('-created_at')[:count]
# 带上下文的简单标签
@register.simple_tag(takes_context=True)
def current_time(context, format_string='%Y-%m-%d %H:%M'):
"""返回当前时间"""
from django.utils import timezone
request = context['request']
return timezone.now().strftime(format_string)
# 赋值标签(结果存入变量)
@register.simple_tag
def get_categories():
return Category.objects.all()
# 使用方式:
# {% get_categories as categories %}
# {% for cat in categories %}...{% endfor %}
5.2 包含标签(Inclusion Tag)
# blog/templatetags/blog_tags.py
@register.inclusion_tag('blog/includes/sidebar.html')
def show_sidebar():
"""渲染侧边栏"""
return {
'categories': Category.objects.all(),
'recent_articles': Article.objects.order_by('-created_at')[:5],
'tags': Tag.objects.all()[:20],
}
@register.inclusion_tag('blog/includes/article_card.html')
def article_card(article, show_excerpt=True):
"""渲染文章卡片"""
return {
'article': article,
'show_excerpt': show_excerpt,
}
@register.inclusion_tag('includes/pagination.html', takes_context=True)
def pagination(context, page_obj, adjacent_pages=2):
"""渲染分页组件"""
page_numbers = []
for i in range(page_obj.number - adjacent_pages,
page_obj.number + adjacent_pages + 1):
if 1 <= i <= page_obj.paginator.num_pages:
page_numbers.append(i)
return {
'page_obj': page_obj,
'page_numbers': page_numbers,
'request': context['request'],
}
<!-- blog/includes/article_card.html -->
<div class="article-card">
<h3><a href="{{ article.get_absolute_url }}">{{ article.title }}</a></h3>
<p class="meta">
<span>{{ article.author.username }}</span>
<span>{{ article.created_at|date:"Y-m-d" }}</span>
</p>
{% if show_excerpt %}
<p class="excerpt">{{ article.content|truncatewords:30 }}</p>
{% endif %}
</div>
<!-- 使用 -->
{% load blog_tags %} {% article_card article %} {% article_card article
show_excerpt=False %}
5.3 赋值标签
@register.simple_tag
def query_articles(**kwargs):
"""查询文章,返回 QuerySet"""
qs = Article.objects.filter(status='published')
if 'category' in kwargs:
qs = qs.filter(category__slug=kwargs['category'])
if 'author' in kwargs:
qs = qs.filter(author__username=kwargs['author'])
if 'limit' in kwargs:
qs = qs[:kwargs['limit']]
return qs
{% load blog_tags %} {% query_articles category="python" limit=5 as
python_articles %} {% for article in python_articles %}
<li>{{ article.title }}</li>
{% endfor %}
5.4 块标签(Block Tag)
# blog/templatetags/blog_tags.py
from django import template
from django.utils.safestring import mark_safe
register = template.Library()
@register.tag(name='card')
def do_card(parser, token):
"""
自定义卡片块标签
用法:{% card "标题" %}内容{% endcard %}
"""
try:
tag_name, title = token.split_contents()
except ValueError:
title = None
nodelist = parser.parse(('endcard',))
parser.delete_first_token()
return CardNode(nodelist, title)
class CardNode(template.Node):
def __init__(self, nodelist, title):
self.nodelist = nodelist
self.title = title
def render(self, context):
content = self.nodelist.render(context)
title_html = ''
if self.title:
# 解析可能的变量
title_value = self.title.strip('"\'')
title_html = f'<div class="card-header">{title_value}</div>'
return mark_safe(f'''
<div class="card">
{title_html}
<div class="card-body">{content}</div>
</div>
''')
{% load blog_tags %} {% card "文章信息" %}
<p>作者:{{ article.author }}</p>
<p>发布时间:{{ article.created_at }}</p>
{% endcard %}
六、自定义过滤器
6.1 创建过滤器
# blog/templatetags/blog_filters.py
from django import template
from django.utils.html import format_html
from django.utils.safestring import mark_safe
import markdown
import re
register = template.Library()
@register.filter
def markdown_to_html(text):
"""将 Markdown 转换为 HTML"""
md = markdown.Markdown(extensions=[
'markdown.extensions.fenced_code',
'markdown.extensions.tables',
'markdown.extensions.toc',
])
return mark_safe(md.convert(text))
@register.filter(name='reading_time')
def calculate_reading_time(text, wpm=200):
"""计算阅读时间(分钟)"""
word_count = len(text.split())
minutes = word_count / wpm
if minutes < 1:
return "不到 1 分钟"
return f"{int(minutes)} 分钟"
@register.filter
def highlight(text, keyword):
"""高亮关键词"""
if not keyword:
return text
pattern = re.compile(f'({re.escape(keyword)})', re.IGNORECASE)
return mark_safe(pattern.sub(r'<mark>\1</mark>', text))
@register.filter(is_safe=True)
def add_class(field, css_class):
"""为表单字段添加 CSS 类"""
return field.as_widget(attrs={'class': css_class})
@register.filter
def get_item(dictionary, key):
"""从字典获取值"""
return dictionary.get(key)
@register.filter
def phone_format(phone):
"""格式化手机号"""
if len(phone) == 11:
return f"{phone[:3]}-{phone[3:7]}-{phone[7:]}"
return phone
@register.filter
def percentage(value, total):
"""计算百分比"""
try:
return f"{(value / total) * 100:.1f}%"
except (ZeroDivisionError, TypeError):
return "0%"
6.2 使用过滤器
{% load blog_filters %}
<!-- Markdown 渲染 -->
<div class="content">{{ article.content|markdown_to_html }}</div>
<!-- 阅读时间 -->
<span>预计阅读:{{ article.content|reading_time }}</span>
<!-- 搜索高亮 -->
<p>{{ article.title|highlight:query }}</p>
<!-- 表单字段样式 -->
{{ form.username|add_class:"form-control" }} {{
form.email|add_class:"form-control form-control-lg" }}
<!-- 字典访问 -->
{% for key in keys %}
<p>{{ data|get_item:key }}</p>
{% endfor %}
6.3 带参数的过滤器
@register.filter(expects_localtime=True)
def time_ago(value):
"""显示相对时间"""
from django.utils import timezone
now = timezone.now()
diff = now - value
if diff.days > 365:
return f"{diff.days // 365} 年前"
elif diff.days > 30:
return f"{diff.days // 30} 个月前"
elif diff.days > 0:
return f"{diff.days} 天前"
elif diff.seconds > 3600:
return f"{diff.seconds // 3600} 小时前"
elif diff.seconds > 60:
return f"{diff.seconds // 60} 分钟前"
else:
return "刚刚"
@register.filter
def pluralize_zh(count, word):
"""中文复数形式"""
return f"{count} {word}"
@register.filter
def mask_email(email):
"""隐藏邮箱中间部分"""
if '@' not in email:
return email
name, domain = email.split('@')
if len(name) <= 2:
return f"{name[0]}***@{domain}"
return f"{name[:2]}***@{domain}"
七、上下文处理器
7.1 创建上下文处理器
# myapp/context_processors.py
from django.conf import settings
from blog.models import Category, Tag
def site_info(request):
"""站点信息"""
return {
'SITE_NAME': 'MySite',
'SITE_DESCRIPTION': '一个技术博客',
'SITE_KEYWORDS': 'Python, Django, Web开发',
}
def categories(request):
"""全局分类列表"""
return {
'nav_categories': Category.objects.filter(is_active=True)[:10]
}
def user_settings(request):
"""用户设置"""
if request.user.is_authenticated:
return {
'user_theme': request.user.profile.theme,
'user_language': request.user.profile.language,
}
return {
'user_theme': 'light',
'user_language': 'zh-CN',
}
def debug_info(request):
"""调试信息(仅开发环境)"""
if settings.DEBUG:
return {
'sql_queries': connection.queries,
'request_path': request.path,
}
return {}
7.2 注册上下文处理器
# settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
# Django 内置
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.media',
'django.template.context_processors.static',
# 自定义
'myapp.context_processors.site_info',
'myapp.context_processors.categories',
'myapp.context_processors.user_settings',
],
},
},
]
7.3 使用上下文变量
<!-- 在任何模板中都可以使用 -->
<head>
<title>{% block title %}{{ SITE_NAME }}{% endblock %}</title>
<meta name="description" content="{{ SITE_DESCRIPTION }}" />
<meta name="keywords" content="{{ SITE_KEYWORDS }}" />
</head>
<body class="theme-{{ user_theme }}">
<nav>
{% for cat in nav_categories %}
<a href="{{ cat.get_absolute_url }}">{{ cat.name }}</a>
{% endfor %}
</nav>
</body>
八、静态文件处理
8.1 静态文件配置
# settings.py
# 静态文件 URL 前缀
STATIC_URL = '/static/'
# 额外的静态文件目录
STATICFILES_DIRS = [
BASE_DIR / 'static',
BASE_DIR / 'assets',
]
# 生产环境静态文件收集目录
STATIC_ROOT = BASE_DIR / 'staticfiles'
# 静态文件查找器
STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
]
# 媒体文件配置
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
8.2 使用静态文件
{% load static %}
<!DOCTYPE html>
<html>
<head>
<!-- CSS -->
<link rel="stylesheet" href="{% static 'css/style.css' %}" />
<link rel="stylesheet" href="{% static 'blog/css/blog.css' %}" />
<!-- Favicon -->
<link rel="icon" href="{% static 'images/favicon.ico' %}" />
</head>
<body>
<!-- 图片 -->
<img src="{% static 'images/logo.png' %}" alt="Logo" />
<!-- 媒体文件(用户上传) -->
<img src="{{ article.cover.url }}" alt="{{ article.title }}" />
<!-- JavaScript -->
<script src="{% static 'js/main.js' %}"></script>
<!-- 动态构建路径 -->
<script>
const staticUrl = "{% static '' %}";
const imageUrl = staticUrl + "images/" + imageName + ".png";
</script>
</body>
</html>
8.3 静态文件版本控制
# settings.py
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
<!-- 使用 ManifestStaticFilesStorage 后 -->
<!-- style.css 会变成 style.abc123.css -->
<link rel="stylesheet" href="{% static 'css/style.css' %}" />
九、表单渲染
9.1 基本表单渲染
<!-- 自动渲染整个表单 -->
<form method="post">
{% csrf_token %} {{ form.as_p }}
<!-- 每个字段包装在 <p> 中 -->
{{ form.as_table }}
<!-- 表格形式 -->
{{ form.as_ul }}
<!-- 无序列表形式 -->
<button type="submit">提交</button>
</form>
<!-- 手动渲染字段 -->
<form method="post">
{% csrf_token %}
<div class="form-group">
<label for="{{ form.username.id_for_label }}">用户名</label>
{{ form.username }} {% if form.username.errors %}
<div class="errors">{{ form.username.errors }}</div>
{% endif %}
<small>{{ form.username.help_text }}</small>
</div>
<div class="form-group">
<label for="{{ form.email.id_for_label }}">邮箱</label>
{{ form.email }}
</div>
<button type="submit">提交</button>
</form>
9.2 使用 widget_tweaks
pip install django-widget-tweaks
# settings.py
INSTALLED_APPS = [
...
'widget_tweaks',
]
{% load widget_tweaks %}
<form method="post">
{% csrf_token %}
<!-- 添加 CSS 类 -->
{% render_field form.username class="form-control" %}
<!-- 添加多个属性 -->
{% render_field form.email class="form-control" placeholder="请输入邮箱" %}
<!-- 添加 data 属性 -->
{% render_field form.password class="form-control" data-toggle="password" %}
<!-- 条件样式 -->
{% render_field form.username class="form-control"|add:' is-invalid' if
form.username.errors %}
<button type="submit" class="btn btn-primary">提交</button>
</form>
9.3 循环渲染表单
<form method="post">
{% csrf_token %} {% for field in form %}
<div class="form-group {% if field.errors %}has-error{% endif %}">
<label for="{{ field.id_for_label }}">
{{ field.label }} {% if field.field.required %}<span class="required"
>*</span
>{% endif %}
</label>
{{ field }} {% 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">提交</button>
</form>
9.4 表单集渲染
<form method="post">
{% csrf_token %} {{ formset.management_form }}
<table>
<thead>
<tr>
<th>标题</th>
<th>作者</th>
<th>删除</th>
</tr>
</thead>
<tbody>
{% for form in formset %}
<tr>
{{ form.id }}
<td>{{ form.title }}</td>
<td>{{ form.author }}</td>
<td>{{ form.DELETE }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<button type="submit">保存</button>
</form>
十、性能优化
10.1 模板缓存
# settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'OPTIONS': {
'loaders': [
('django.template.loaders.cached.Loader', [
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
]),
],
'context_processors': [...],
},
},
]
# 或者在 DEBUG=False 时自动启用
if not DEBUG:
TEMPLATES[0]['OPTIONS']['loaders'] = [...]
10.2 片段缓存
{% load cache %}
<!-- 缓存整个片段 -->
{% cache 500 sidebar %}
<div class="sidebar">
{% for item in expensive_query %}
<p>{{ item }}</p>
{% endfor %}
</div>
{% endcache %}
<!-- 带参数的缓存键 -->
{% cache 300 article_list page_number %} {% for article in articles %} {%
include "blog/article_card.html" %} {% endfor %} {% endcache %}
<!-- 用户相关的缓存 -->
{% cache 600 user_sidebar request.user.id %}
<div class="user-sidebar">{{ request.user.profile.bio }}</div>
{% endcache %}
10.3 减少数据库查询
# views.py
def article_list(request):
# 使用 select_related 和 prefetch_related
articles = Article.objects.select_related(
'author', 'category'
).prefetch_related(
'tags'
)
# 只查询需要的字段
articles = Article.objects.only(
'title', 'slug', 'created_at', 'author__username'
)
return render(request, 'blog/list.html', {'articles': articles})
<!-- 避免在模板中进行额外查询 -->
{% for article in articles %}
<!-- 好:使用预加载的数据 -->
<p>{{ article.author.username }}</p>
<!-- 差:每次循环都会查询数据库 -->
<p>{{ article.comments.count }}</p>
{% endfor %}
<!-- 使用 with 缓存计算结果 -->
{% with articles.count as total %}
<p>共 {{ total }} 篇文章</p>
{% if total > 10 %}
<p>文章很多!</p>
{% endif %} {% endwith %}
十一、调试技巧
11.1 django-debug-toolbar
# settings.py
if DEBUG:
INSTALLED_APPS += ['debug_toolbar']
MIDDLEWARE.insert(0, 'debug_toolbar.middleware.DebugToolbarMiddleware')
INTERNAL_IPS = ['127.0.0.1']
11.2 调试模板变量
<!-- 打印变量 -->
<pre>{{ variable|pprint }}</pre>
<!-- 显示所有上下文变量 -->
{% if debug %}
<pre>
{% for key, value in request.items %}
{{ key }}: {{ value }}
{% endfor %}
</pre>
{% endif %}
<!-- 使用 django-debug-toolbar 的模板面板 -->
11.3 模板调试设置
# settings.py
TEMPLATES = [
{
'OPTIONS': {
'debug': DEBUG, # 启用模板调试
'string_if_invalid': 'INVALID: %s', # 无效变量标记
},
},
]
十二、最佳实践
12.1 命名规范
templates/
├── base.html # 基础模板
├── base_blog.html # 博客基础模板
├── includes/ # 可复用片段
│ ├── _header.html # 下划线开头表示片段
│ ├── _footer.html
│ └── _pagination.html
├── blog/
│ ├── article_list.html # 模型名_动作
│ ├── article_detail.html
│ ├── article_form.html
│ ├── article_confirm_delete.html
│ └── partials/ # 应用级片段
│ └── _article_card.html
└── errors/
├── 404.html
└── 500.html
12.2 模板组织原则
# 1. 使用模板继承减少重复
# 2. 使用 include 复用组件
# 3. 将复杂逻辑移到视图或标签中
# 4. 保持模板简洁,避免复杂的业务逻辑
# 5. 使用命名空间避免冲突
12.3 安全注意事项
<!-- 1. 始终使用 CSRF token -->
<form method="post">{% csrf_token %} ...</form>
<!-- 2. 谨慎使用 safe 过滤器 -->
{{ user_content }}
<!-- 自动转义,安全 -->
{{ trusted_html|safe }}
<!-- 仅用于可信内容 -->
<!-- 3. 避免在模板中暴露敏感信息 -->
{% if debug %} {{ secret_key }}
<!-- 不要这样做! -->
{% endif %}
<!-- 4. 使用 escapejs 处理 JS 中的数据 -->
<script>
var data = "{{ user_input|escapejs }}";
</script>