Django 模板层 — 让页面更丰富
🕐 预计用时:2-3 小时 | 🎯 目标:掌握 Django 模板语法、过滤器、继承、include 和自定义标签
📖 今日目录
1. Django 模板基础
🎨 render() 的工作方式
# views.py
from django.shortcuts import render
def index(request):
context = {
'title': '我的博客',
'posts': Post.objects.all()[:5],
'user': request.user,
}
return render(request, 'blog/index.html', context)
# 模板文件路径 传入的变量
<!-- templates/blog/index.html -->
<h1>{{ title }}</h1>
<p>欢迎,{{ user.username }}</p>
{% for post in posts %}
<h2>{{ post.title }}</h2>
{% endfor %}
🔍 DTL(Django Template Language)vs Jinja2
| | |
|---|
| {{ var }} | {{ var }} |
| {% tag %} | {% tag %} |
| {{ var|upper }} | {{ var|upper }} |
| {# comment #} | {# comment #} |
| url_for('name') | {% url 'name' %} |
| url_for('static') | {% static 'path' %} |
| | |
| | |
2. 变量与标签
📤 变量输出
<!-- 简单变量 -->
{{ username }}
<!-- 字典访问 -->
{{ user.name }} 或 {{ user.email }}
<!-- 列表访问 -->
{{ items.0 }} <!-- 第一个元素 -->
{{ items.2 }} <!-- 第三个元素 -->
<!-- 方法调用(Django 自动调用无参数方法)-->
{{ user.get_full_name }} <!-- 自动调用方法 -->
{{ post.created_at|date:"Y-m-d" }} <!-- 但不能传参数 -->
<!-- 转义控制 -->
{{ html_content }} <!-- 自动转义:<b>显示为文本 -->
{{ html_content|safe }} <!-- 不转义:渲染为 HTML -->
🔄 控制标签
{# for 循环 #}
<ul>
{% for item in items %}
<li>{{ forloop.counter }}. {{ item.name }}</li> {# 从 1 开始计数 #}
<li>{{ forloop.counter0 }}. {{ item.name }}</li> {# 从 0 开始 #}
<li>{{ forloop.revcounter }}</li> {# 倒计数 #}
<li>{{ forloop.first }}</li> {# 是否是第一个 #}
<li>{{ forloop.last }}</li> {# 是否是最后一个 #}
<li>{{ forloop.parentloop }}</li> {# 嵌套时访问外层循环 #}
{% empty %}
<li>暂无数据</li> {# 列表为空时显示 #}
{% endfor %}
</ul>
{# if 条件 #}
{% if user.is_authenticated %}
<p>欢迎,{{ user.username }}</p>
{% elif user.is_staff %}
<p>管理员面板</p>
{% else %}
<p>请登录</p>
{% endif %}
{# if 支持的运算 #}
{% if score > 90 %}优秀{% endif %}
{% if name == 'admin' %}管理员{% endif %}
{% if items %}有数据{% endif %} {# 非空 = True #}
{% if not items %}无数据{% endif %}
{% if a and b %}都为真{% endif %}
{% if a or b %}至少一个为真{% endif %}
{# with 变量缓存(避免重复查询) #}
{% with total=posts.count %}
<p>共 {{ total }} 篇文章</p>
{% endwith %}
3. 内置过滤器
| | |
|---|
default | | {{ val|default:"N/A" }} |
length | | {{ list|length }} |
upper/lower | | {{ name|upper }} |
title | | {{ name|title }} |
truncatewords | | {{ text|truncatewords:30 }} |
truncatechars | | {{ text|truncatechars:100 }} |
linebreaks | | {{ text|linebreaks }} |
date | | {{ dt|date:"Y-m-d H:i" }} |
timesince | | {{ dt|timesince }} |
filesizeformat | | {{ size|filesizeformat }} |
join | | {{ list|join:", " }} |
add | | {{ count|add:5 }} |
first/last | | {{ list|first }} |
json_script | | {{ data|json_script:"mydata" }} |
safe | | {{ html|safe }} |
striptags | | {{ html|striptags }} |
urlize | | {{ text|urlize }} |
📅 日期过滤器详解
{{ post.created_at|date:"Y年m月d日 H:i" }} <!-- 2026年05月12日 13:15 -->
{{ post.created_at|date:"Y-m-d" }} <!-- 2026-05-12 -->
{{ post.created_at|timesince }} <!-- 2 hours, 30 minutes -->
{{ post.created_at|timesince:now }} <!-- 同上 -->
{{ post.created_at|timeuntil }} <!-- 距离未来时间 -->
4. 模板继承
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}我的网站{% endblock %}</title>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
{% block extra_css %}{% endblock %}
</head>
<body>
<nav>
<a href="{% url 'main:index' %}">首页</a>
<a href="{% url 'blog:post_list' %}">博客</a>
{% if user.is_authenticated %}
<a href="{% url 'blog:post_create' %}">写文章</a>
<a href="{% url 'auth:logout' %}">退出</a>
{% else %}
<a href="{% url 'auth:login' %}">登录</a>
{% endif %}
</nav>
{% block content %}{% endblock %}
<footer>© 2026 我的网站</footer>
{% block extra_js %}{% endblock %}
</body>
</html>
{# 子模板继承 #}
{% extends "base.html" %}
{% block title %}文章列表 - 我的博客{% endblock %}
{% block content %}
<h1>最新文章</h1>
{% for post in posts %}
<article>
<h2>{{ post.title }}</h2>
<p>{{ post.summary }}</p>
</article>
{% endfor %}
{% endblock %}
5. include 包含
{# 创建可复用的组件 #}
{# templates/includes/pagination.html #}
<div class="pagination">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">«</a>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<span class="current">{{ num }}</span>
{% else %}
<a href="?page={{ num }}">{{ num }}</a>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">»</a>
{% endif %}
</div>
{# 在其他模板中使用 #}
{% include "includes/pagination.html" %}
{# 带变量的 include #}
{% include "includes/alert.html" with message="操作成功" type="success" %}
💡 include vs 继承 vs 自定义标签:
• {% extends %}:页面级复用,定义骨架
• {% include %}:组件级复用,插入固定片段
• {% load %} + {% tag %}:逻辑级复用,可传参、可计算
6. 静态文件
<!-- settings.py 配置 -->
STATIC_URL = '/static/' # URL 前缀
STATIC_ROOT = BASE_DIR / 'staticfiles' # collectstatic 收集目录
STATICFILES_DIRS = [
BASE_DIR / 'static', # 开发时的静态文件目录
]
{# 在模板中引用静态文件 #}
{% load static %}
<link rel="stylesheet" href="{% static 'css/style.css' %}">
<script src="{% static 'js/main.js' %}"></script>
<img src="{% static 'images/logo.png' %}" alt="Logo">
{# 动态静态文件路径 #}
<img src="{% static user.avatar_url %}" alt="头像">
7. 自定义过滤器
# blog/templatetags/blog_tags.py
from django import template
from django.utils.timesince import timesince
from datetime import datetime
register = template.Library()
@register.filter
def time_ago(value):
"""自定义过滤器:距今多久"""
if isinstance(value, datetime):
return timesince(value) + ' 前'
return value
@register.filter
def censor(value, word='***'):
"""敏感词过滤"""
bad_words = ['敏感词1', '敏感词2']
for w in bad_words:
value = value.replace(w, word)
return value
@register.filter(name='add_class')
def add_class(field, css_class):
"""给表单字段添加 CSS 类"""
return field.as_widget(attrs={"class": css_class})
{# 使用自定义过滤器 #}
{% load blog_tags %}
<p>{{ post.created_at|time_ago }}</p> <!-- 2 hours, 30 minutes 前 -->
<p>{{ content|censor }}</p>
{# 表单中 #}
{{ form.username|add_class:"form-control" }}
8. 自定义模板标签
# blog/templatetags/blog_tags.py
@register.simple_tag
def total_posts():
"""返回文章总数"""
from blog.models import Post
return Post.objects.count()
@register.simple_tag
def current_time(format_string):
"""返回当前时间"""
return datetime.now().strftime(format_string)
@register.inclusion_tag('includes/recent_posts.html')
def show_recent_posts(count=5):
"""显示最近文章(inclusion tag)"""
from blog.models import Post
posts = Post.objects.order_by('-created_at')[:count]
return {'posts': posts}
{# 使用自定义标签 #}
{% load blog_tags %}
{# simple_tag #}
<p>共 {% total_posts %} 篇文章</p>
<p>当前时间:{% current_time "%Y-%m-%d %H:%M" %}</p>
{# inclusion_tag(自动渲染模板) #}
{% show_recent_posts 5 %}
💡 三种自定义标签对比:
• @register.filter:过滤器,用 | 调用,只能传 1-2 个参数
• @register.simple_tag:简单标签,用 {% tag %} 调用,可传多个参数
• @register.inclusion_tag:包含标签,自动渲染子模板,适合复用组件
9. 今日练习
🏋️ 练习 1:模板继承
# 创建 base.html + 3 个子模板
# - base.html:导航栏 + content block + 页脚
# - index.html:首页(欢迎信息 + 最新文章)
# - post_list.html:文章列表(分页)
# - about.html:关于页面
# 所有页面共享导航栏和页脚
🏋️ 练习 2:自定义过滤器
# 创建 blog_tags.py:
# 1. @register.filter('truncate_cn') → 中文截断(按字符而非单词)
# 2. @register.filter('highlight') → 高亮关键词(用 <mark> 包裹)
# 3. @register.inclusion_tag → 显示侧边栏(最新文章 + 分类列表)
🏋️ 练习 3:日期显示
# 用 timesince 过滤器实现:
# - 1 分钟内 → "刚刚"
# - 1 小时内 → "X 分钟前"
# - 24 小时内 → "X 小时前"
# - 30 天内 → "X 天前"
# - 超过 30 天 → 显示具体日期
10. 今日小结
| |
|---|
| {{ 变量 }} / {% 标签 %} / {# 注释 #} |
| for/empty/endfor、if/elif/else/endif、with |
| default/length/date/truncatewords/safe/urlize |
| {% extends %} + {% block %},一个 base.html 管理全局 |
| |
| {% load static %} + {% static 'path' %} |
| |
| simple_tag(多参数)/ inclusion_tag(带子模板) |
🚀 明日预告:Day 63 — Django 表单与认证
模板能渲染页面了,但还不能接收用户输入。明天学 Django 表单系统(ModelForm)和内置的用户认证系统——注册、登录、权限控制,Django 全帮你搞定!