专题:Python Web开发系统学习
关键词:Python, Web开发, Django模板, 模板继承, ModelForm, 表单验证, 自定义标签, Django表单
一、Django模板系统
Django的模板系统(Template System)是其"模型-模板-视图"(MTV)架构中的核心组件之一。它将页面的表现逻辑与业务逻辑分离,让开发者能够以声明式的方式生成动态HTML内容。Django模板使用自己专属的模板语言(Django Template Language, DTL),语法简洁且功能强大。
1.1 模板配置
在Django项目的 settings.py 中,通过 TEMPLATES 配置项对模板引擎进行全局设置。这是Django 1.8之后引入的统一模板配置入口:
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',
],
},
},
]
DIRS 是一个列表,用于指定除每个应用内部的 templates 目录之外的额外模板搜索路径。通常将项目级别的公共模板(如 base.html、导航栏、页脚等)放在此目录中。APP_DIRS 设置为 True 时,Django会自动在每个已安装的应用目录下查找 templates 子文件夹。两者配合使用,既支持全局模板复用,又允许每个应用维护自己的私有模板。
模板查找的顺序是:首先在 DIRS 指定的目录中依次查找,如果未找到,再在 APP_DIRS 启用的各个应用的 templates 目录中按 INSTALLED_APPS 的顺序查找。因此需要注意模板命名的唯一性——不同应用中同名的模板文件可能产生覆盖。
1.2 模板变量
模板变量使用双花括号语法 {{ variable }} 来输出视图传递过来的数据。变量名可以包含字母、数字和下划线,但不能以数字开头。Django支持点号(.)查找机制,可以对变量进行属性访问、字典键查找和列表索引操作:
{{ name }} {# 输出字符串变量 #}
{{ user.username }} {# 访问对象的属性 #}
{{ user.profile.bio }} {# 链式属性访问 #}
{{ article_list.0 }} {# 列表第一个元素 #}
{{ article_list.0.title }} {# 列表第一个元素的title属性 #}
{{ data.key }} {# 字典键查找 #}
{{ items|length }} {# 结合过滤器使用 #}
点号查找的执行顺序是:字典键查找 → 属性或方法查找 → 列表索引查找。如果最终未能找到对应值,Django不会直接抛出错误,而是使用 TEMPLATE_STRING_IF_INVALID 配置的值(默认为空字符串)。
1.3 模板标签
模板标签使用 {% tag %} 语法,用于执行控制流逻辑、循环、加载外部内容等操作。标签与变量不同,它不直接输出值,而是执行某种操作或控制模板的渲染流程。大部分标签需要闭合标记:
{% if user.is_authenticated %}
<p>欢迎回来,{{ user.username }}!</p>
{% endif %}
{% for article in articles %}
<h2>{{ article.title }}</h2>
{% endfor %}
{% url 'article_detail' article.id %}
1.4 过滤器
过滤器用于在变量输出前对其进行转换或格式化,使用管道符 | 连接。过滤器可以链式调用,也可以传递参数。Django内置了数十个常用过滤器:
{{ title|lower }} {# 全部转为小写 #}
{{ title|upper }} {# 全部转为大写 #}
{{ article.created_at|date:"Y-m-d H:i" }} {# 日期格式化 #}
{{ content|truncatewords:30 }} {# 截断至30个单词 #}
{{ content|linebreaks }} {# 将\n转换为<br>和<p> #}
{{ price|floatformat:2 }} {# 浮点数格式化,保留2位小数 #}
{{ bio|default:"这个用户很懒,什么都没写" }} {# 默认值 #}
{{ html_content|safe }} {# 标记为安全字符串,不转义 #}
{{ items|length }} {# 获取序列长度 #}
{{ items|join:", " }} {# 用逗号连接列表 #}
需要特别注意的是 |safe 过滤器。出于安全考虑,Django默认会对所有变量输出进行HTML转义(例如将 < 转为 <)。当确信内容是安全可信的HTML时,使用 |safe 可以关闭转义。此外,可以使用 {% autoescape off %} 标签块临时关闭某段模板内容的转义。
1.5 模板注释
Django模板支持单行注释 {# 注释内容 #} 和多行注释 {% comment %}...{% endcomment %}。注释中的内容不会出现在渲染后的HTML中,适用于在模板中添加开发说明或临时禁用某段代码:
{# 这是单行注释,不会出现在HTML中 #}
{% comment "可选的理由说明" %}
<div>
<p>这段内容被注释掉了,不会渲染</p>
</div>
{% endcomment %}
二、模板继承与包含
模板继承是Django模板系统最强大的特性之一。它允许创建一个包含通用结构和元素的"骨架"基础模板,然后让子模板继承这个骨架并填充各自特有的区块内容。这种机制极大地提高了代码复用率,减少了重复劳动。
2.1 基础模板与 extends
基础模板使用 {% block %} 标签定义可被子模板覆盖的区域。子模板在文件开头使用 {% extends "base.html" %} 声明继承关系,然后重新定义同名的 block 内容:
{# ===== base.html ===== #}
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}默认标题{% endblock %}</title>
<link rel="stylesheet" href="style.css">
{% block extra_head %}{% endblock %}
</head>
<body>
<header>
<h1>我的网站</h1>
<nav>{% block nav %}{% endblock %}</nav>
</header>
<main>
{% block content %}
<p>这里是默认内容</p>
{% endblock %}
</main>
<footer>{% block footer %}© 2026 版权所有{% endblock %}</footer>
</body>
</html>
{# ===== article_list.html ===== #}
{% extends "base.html" %}
{% block title %}文章列表 - 我的网站{% endblock %}
{% block content %}
<h2>最新文章</h2>
{% for article in articles %}
<div class="article-item">
<h3>{{ article.title }}</h3>
<p>{{ article.summary }}</p>
</div>
{% endfor %}
{% endblock %}
{% extends %} 必须是子模板中的第一个模板标签,否则Django将无法正确解析继承关系。一个子模板只能继承一个父模板,不支持多重继承。但可以通过多层继承链实现更灵活的结构:base.html → section_base.html → specific_page.html。
2.2 block.super 保留父块内容
当子模板需要保留父模板块中原有内容的基础上追加新内容时,可以使用 {{ block.super }} 变量。这个变量会渲染出父模板中对应 block 的内容:
{# 在子模板中保留父模板footer内容并追加 #}
{% block footer %}
{{ block.super }}
<p><a href="/about/">关于我们</a></p>
{% endblock %}
这在需要扩展而非完全覆盖父模板区块时非常有用,例如在 extra_head 区块中添加额外的CSS或JS文件,同时保留父模板中已有的资源引用。
2.3 包含标签与 include
{% include %} 标签允许将一个独立的模板文件嵌入到当前模板中。被包含的模板会使用当前模板的上下文进行渲染(除非使用 only 关键字隔离上下文):
{# ===== article_list.html ===== #}
{% for article in articles %}
{% include "article_item.html" %}
{% empty %}
<p>暂无文章</p>
{% endfor %}
{# ===== article_item.html ===== #}
<div class="article-card">
<h3><a href="{{ article.get_absolute_url }}">{{ article.title }}</a></h3>
<p class="meta">{{ article.created_at|date:"Y-m-d" }} | {{ article.author }}</p>
<p>{{ article.summary|truncatewords:50 }}</p>
</div>
使用 {% include "template_name" with key=value only %} 可以限制只传递特定变量到被包含的模板,避免变量名冲突或意外泄漏上下文。包含标签适合封装可复用的UI组件,如分页器、侧边栏小工具、评论区块等。
2.4 模板加载机制
Django的模板加载遵循一套明确的搜索规则。当调用 render(request, 'template_name.html', context) 时,Django的TemplateLoader按照以下顺序查找模板文件:
- filesystem.Loader:在
DIRS 配置的目录中按顺序查找
- app_directories.Loader:在每个已安装应用的
templates 子目录中查找
为了提高性能,模板加载器会缓存已解析的模板对象(当 DEBUG=False 时)。在开发环境中(DEBUG=True),每次请求都会重新加载模板文件以便于调试。此外,可以通过编写自定义的模板加载器(继承 django.template.loaders.base.Loader)来从数据库或其他存储后端加载模板。
三、内置模板标签
Django内置了丰富的模板标签,涵盖循环、条件判断、URL处理、静态文件管理、国际化等各个方面。熟练掌握这些内置标签是高效使用Django模板的基础。
3.1 循环与条件标签
{# {% for %} 循环 #}
{% for item in item_list %}
<li>{{ forloop.counter }}. {{ item.name }}</li>
{% empty %}
<li>列表为空</li>
{% endfor %}
{# forloop 内置变量 #}
{{ forloop.counter }} {# 从1开始的循环计数 #}
{{ forloop.counter0 }} {# 从0开始的循环计数 #}
{{ forloop.first }} {# 是否第一次迭代 #}
{{ forloop.last }} {# 是否最后一次迭代 #}
{{ forloop.revcounter }} {# 剩余迭代次数(从总数到1) #}
{# {% if %} 条件判断 #}
{% if user.is_authenticated and user.is_active %}
<p>欢迎,{{ user.username }}</p>
{% elif user.is_anonymous %}
<p>请先登录</p>
{% else %}
<p>账号异常</p>
{% endif %}
{% with %} 标签用于在模板中创建临时变量,减少重复的复杂表达式计算:
{% with total=items|length %}
<p>共 {{ total }} 条记录</p>
{% if total > 100 %}
<p>数据量较大,建议分页查看</p>
{% endif %}
{% endwith %}
3.2 URL 标签
{% url %} 标签根据视图函数的名称或URL模式的命名空间生成对应的绝对路径。这种方式避免了在模板中硬编码URL,当路由发生变化时只需修改 urls.py 而无需逐个修改模板:
{# 无参数 #}
<a href="{% url 'home' %}">首页</a>
{# 位置参数 #}
<a href="{% url 'article_detail' article.id %}">详情</a>
{# 关键字参数 #}
<a href="{% url 'article_detail' pk=article.id %}">详情</a>
{# 命名空间 #}
<a href="{% url 'blog:article_detail' article.id %}">详情</a>
{# 获取URL字符串并存入变量 #}
{% url 'article_edit' article.id as edit_url %}
<a href="{{ edit_url }}">编辑</a>
3.3 静态文件标签
{% static %} 标签用于生成静态文件的URL。在模板中使用此标签前,需要先执行 {% load static %} 加载 static 标签库:
{% load static %}
<link rel="stylesheet" href="{% static 'css/style.css' %}">
<script src="{% static 'js/app.js' %}"></script>
<img src="{% static 'images/logo.png' %}" alt="Logo">
静态文件的URL前缀由 STATIC_URL 配置项决定(通常为 /static/)。在生产环境中,配合 django.contrib.staticfiles 应用和 collectstatic 命令,可以将所有静态文件聚合到单一目录以便于Web服务器(如Nginx)高效托管。
3.4 国际化标签
Django内置了完善的国际化(i18n)支持,模板中使用 {% trans %} 和 {% blocktrans %} 标签来标记待翻译的字符串:
{% load i18n %}
{# 简单字符串翻译 #}
<h1>{% trans "Welcome to our site" %}</h1>
{# 带变量的翻译 #}
{% blocktrans with name=user.username %}
Hello, {{ name }}! You have {{ count }} messages.
{% endblocktrans %}
{# 指定目标语言 #}
{% language "zh-hans" %}
{% trans "Hello" %}
{% endlanguage %}
国际化的完整工作流程包括:在代码和模板中使用翻译标记 → 运行 makemessages 生成 .po 文件 → 翻译其中的字符串 → 运行 compilemessages 编译为 .mo 文件 → 在 settings.py 中配置 LOCALE_PATHS 和 LANGUAGES。
3.5 CSRF Token 标签
为了防止跨站请求伪造(CSRF)攻击,Django要求所有使用POST方法的表单必须包含 {% csrf_token %} 标签。该标签会在表单中生成一个隐藏的 input 字段,其值为一个随机生成的token:
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<input type="text" name="username" placeholder="用户名">
<input type="password" name="password" placeholder="密码">
<button type="submit">登录</button>
</form>
Django在服务端验证提交的CSRF token是否与当前会话中存储的token一致。如果验证失败,将返回403错误。CSRF保护默认是开启的,通过 django.middleware.csrf.CsrfViewMiddleware 中间件实现。如果需要关闭特定视图的CSRF保护(如构建API时),可以使用 @csrf_exempt 装饰器。
四、自定义模板标签与过滤器
当Django内置的模板标签和过滤器无法满足需求时,可以通过创建自定义标签和过滤器来扩展模板语言的功能。自定义标签和过滤器被组织在Python模块中,存放在每个应用的 templatetags 目录下。
4.1 标签库注册与目录结构
自定义标签库需要遵循特定的目录结构。在每个应用的目录中创建 templatetags 包(包含 __init__.py 文件),然后在此包中创建模块文件:
blog/
├── __init__.py
├── models.py
├── views.py
├── templatetags/
│ ├── __init__.py
│ └── blog_extras.py # 自定义标签/过滤器
└── templates/
└── blog/
└── article_list.html
在模板中使用 {% load blog_extras %} 即可加载自定义标签库中的所有标签和过滤器。
4.2 自定义过滤器
自定义过滤器本质上是一个接收一个或两个参数的Python函数,通过 @register.filter 装饰器注册:
# blog/templatetags/blog_extras.py
from django import template
from django.utils.html import format_html
register = template.Library()
@register.filter
def truncate_chars(value, max_length):
"""按字符数截断字符串,超出部分用...代替"""
if len(value) <= max_length:
return value
return value[:max_length] + '...'
@register.filter(name='format_date')
def format_date(value):
"""格式化日期为友好的中文格式"""
return value.strftime('%Y年%m月%d日')
@register.filter(is_safe=True)
def rich_text(value):
"""安全地将Markdown风格文本转为HTML"""
from markdown import markdown
return markdown(value)
# 在模板中使用
{% load blog_extras %}
<p>{{ article.content|truncate_chars:100 }}</p>
<p>发布于:{{ article.created_at|format_date }}</p>
自定义过滤器的 @register.filter 装饰器支持多个参数:name(模板中使用的名称,默认为函数名)、is_safe(标记结果为安全HTML,不转义)、needs_autoescape(是否需要自动转义上下文信息)。
4.3 简单标签
简单标签使用 @register.simple_tag 装饰器,可以接受任意数量的参数,并将处理结果作为字符串返回。适合生成非HTML片段的动态数据:
@register.simple_tag
def total_views(user):
"""计算用户所有文章的总阅读量"""
return Article.objects.filter(author=user).aggregate(
total=Sum('views')
)['total'] or 0
@register.simple_tag(name='query_url', takes_context=True)
def query_url(context, key, value):
"""在现有GET参数基础上追加或覆盖参数"""
request = context['request']
params = request.GET.copy()
params[key] = value
return '?' + params.urlencode()
# 在模板中使用
{% load blog_extras %}
<p>总阅读量:{% total_views request.user %}</p>
<a href="{% query_url 'page' 2 %}">下一页</a>
takes_context=True 参数使标签能够访问模板上下文(包括 request、user 等由上下文处理器注入的变量)。此时标签函数的第一个参数必须是 context。
4.4 包含标签
包含标签使用 @register.inclusion_tag 装饰器,它返回一个上下文字典并用指定的模板来渲染HTML片段。包含标签非常适合封装那些需要反复出现的复杂UI组件:
@register.inclusion_tag('blog/pagination.html')
def pagination(page_obj, nearby=2):
"""生成分页导航组件"""
page_range = []
num_pages = page_obj.paginator.num_pages
current = page_obj.number
start = max(1, current - nearby)
end = min(num_pages, current + nearby)
if start > 1:
page_range.append(1)
if start > 2:
page_range.append('...')
for i in range(start, end + 1):
page_range.append(i)
if end < num_pages:
if end < num_pages - 1:
page_range.append('...')
page_range.append(num_pages)
return {'page_range': page_range, 'current': current}
# blog/pagination.html(包含标签使用的模板)
<nav class="pagination">
{% for page in page_range %}
{% if page == '...' %}
<span>...</span>
{% elif page == current %}
<span class="current">{{ page }}</span>
{% else %}
<a href="?page={{ page }}">{{ page }}</a>
{% endif %}
{% endfor %}
</nav>
# 在模板中使用
{% load blog_extras %}
{% pagination articles 3 %}
五、Django表单系统
Django的表单系统提供了一套完整的工具链,用于处理HTML表单的生成、数据验证、错误处理和清洗。它将表单的渲染逻辑与验证逻辑统一封装在Form类中,大大简化了Web开发中常见的表单处理任务。
5.1 forms.Form 基础表单
forms.Form 是所有手动定义表单的基类。开发者通过声明式的方式定义表单字段,Django自动处理字段的验证、清洗和渲染:
# forms.py
from django import forms
from django.core.validators import RegexValidator
class RegistrationForm(forms.Form):
username = forms.CharField(
label='用户名',
max_length=30,
min_length=3,
required=True,
help_text='3-30个字符,字母、数字和下划线',
validators=[RegexValidator(r'^[a-zA-Z0-9_]+$',
'只能包含字母、数字和下划线')],
widget=forms.TextInput(attrs={'class': 'form-control',
'placeholder': '请输入用户名'})
)
email = forms.EmailField(
label='电子邮箱',
required=True,
widget=forms.EmailInput(attrs={'class': 'form-control',
'placeholder': '请输入邮箱'})
)
age = forms.IntegerField(
label='年龄',
required=True,
min_value=1,
max_value=150,
widget=forms.NumberInput(attrs={'class': 'form-control'})
)
gender = forms.ChoiceField(
label='性别',
choices=[('M', '男'), ('F', '女'), ('O', '其他')],
widget=forms.RadioSelect
)
bio = forms.CharField(
label='个人简介',
required=False,
max_length=500,
widget=forms.Textarea(attrs={'rows': 4, 'class': 'form-control'})
)
agree_terms = forms.BooleanField(
label='同意服务条款',
required=True,
error_messages={'required': '请先同意服务条款'}
)
5.2 常用表单字段
| 字段类 | 说明 | 默认Widget |
| CharField | 字符串字段,支持 max_length、min_length | TextInput |
| IntegerField | 整数字段,支持 max_value、min_value | NumberInput |
| FloatField | 浮点数字段 | NumberInput |
| DecimalField | 十进制数字段,支持 max_digits、decimal_places | NumberInput |
| BooleanField | 布尔字段(复选框) | CheckboxInput |
| ChoiceField | 单选下拉框,需要提供 choices 参数 | Select |
| MultipleChoiceField | 多选字段 | SelectMultiple |
| DateField | 日期字段,自动解析为 datetime.date | DateInput |
| DateTimeField | 日期时间字段 | DateTimeInput |
| EmailField | 邮箱字段,自带邮箱格式验证 | EmailInput |
| URLField | URL字段,自带URL格式验证 | URLInput |
| FileField | 文件上传字段 | ClearableFileInput |
| ImageField | 图片上传字段(需安装Pillow) | ClearableFileInput |
5.3 字段参数详解
每个表单字段类型都接受一组通用参数,用于控制字段的行为和外观:
- required:是否必填,默认为
True
- label:字段的人类可读标签,在模板中自动渲染
- initial:字段的初始值,支持可调用对象
- help_text:字段的帮助提示文字,显示在字段下方
- error_messages:自定义验证失败时的错误提示信息字典
- widget:指定渲染该字段时使用的HTML控件
- validators:自定义验证器列表
- disabled:是否禁用该字段
5.4 表单验证
Django表单的验证流程是分层进行的,理解这一流程对于正确实现自定义验证逻辑至关重要:
# forms.py — 表单验证示例
from django import forms
from django.core.exceptions import ValidationError
class RegistrationForm(forms.Form):
password = forms.CharField(
label='密码',
min_length=8,
widget=forms.PasswordInput
)
confirm_password = forms.CharField(
label='确认密码',
widget=forms.PasswordInput
)
# 阶段1:字段级验证 — clean_<fieldname>()
def clean_password(self):
password = self.cleaned_data.get('password')
# 自定义强度检查
if not any(char.isdigit() for char in password):
raise ValidationError('密码必须包含至少一个数字')
if not any(char.isupper() for char in password):
raise ValidationError('密码必须包含至少一个大写字母')
return password
# 阶段2:表单级验证 — clean()
def clean(self):
cleaned_data = super().clean()
password = cleaned_data.get('password')
confirm = cleaned_data.get('confirm_password')
if password and confirm and password != confirm:
raise ValidationError('两次输入的密码不一致')
return cleaned_data
# 阶段3:自定义验证器(在字段定义时指定)
phone = forms.CharField(
label='手机号',
validators=[RegexValidator(
r'^1[3-9]\d{9}$',
'请输入有效的11位手机号码'
)]
)
完整的验证流程是:先进行字段内置类型验证(如DateField检查日期格式)→ 调用 clean_<fieldname>() 方法 → 调用字段的 validators 列表中的验证器 → 最后调用表单的 clean() 方法进行跨字段验证。验证通过的数据存放于 cleaned_data 字典中。
最佳实践:字段级验证(clean_<field>)用于验证单个字段的约束条件,表单级验证(clean)用于验证字段之间的依赖关系(如密码一致性检查)。自定义验证器(Validator)适用于可复用的验证逻辑(如手机号格式、邮箱格式等)。
5.5 在模板中渲染表单
Django提供了多种预定义的表单渲染方式,同时也支持完全手动的字段渲染以实现定制化布局:
{# 方式一:快速渲染 — as_p / as_table / as_ul #}
<form method="post">
{% csrf_token %}
{{ form.as_p }} {# 每个字段用<p>包裹 #}
{{ form.as_table }} {# 每个字段用<tr>包裹 #}
{{ form.as_ul }} {# 每个字段用<li>包裹 #}
<button type="submit">提交</button>
</form>
{# 方式二:手动渲染(推荐,更灵活) #}
<form method="post" novalidate>
{% csrf_token %}
<div class="form-group">
{{ form.username.label_tag }}
{{ form.username }}
{% if form.username.errors %}
<ul class="error-list">
{% for error in form.username.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
{% if form.username.help_text %}
<small class="help-text">{{ form.username.help_text }}</small>
{% endif %}
</div>
<div class="form-group">
{{ form.email.label_tag }}
{{ form.email }}
{{ form.email.errors }}
</div>
<button type="submit">注册</button>
</form>
{# 方式三:循环渲染(适用于字段统一样式) #}
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="field-wrapper">
{{ field.label_tag }}
{{ field }}
{% if field.help_text %}
<p class="help">{{ field.help_text }}</p>
{% endif %}
{% for error in field.errors %}
<p class="error">{{ error }}</p>
{% endfor %}
</div>
{% endfor %}
<button type="submit">提交</button>
</form>
手动渲染方式不仅能够精确控制表单的HTML结构和CSS类,还能在每个字段周围自由添加交互元素、错误提示和帮助文本,是实现复杂UI设计的最佳选择。
六、ModelForm
ModelForm是Django表单系统中极为重要的组成部分,它能够根据数据库模型(Model)自动生成对应的表单字段,并处理数据持久化。ModelForm极大地减少了CRUD操作中的重复代码。
6.1 从模型自动生成表单
通过继承 forms.ModelForm 并在内部 Meta 类中指定模型,ModelForm可以自动分析模型的字段定义并生成匹配的表单字段:
# models.py
from django.db import models
class Article(models.Model):
title = models.CharField('标题', max_length=200)
slug = models.SlugField('URL别名', unique=True)
category = models.ForeignKey('Category', on_delete=models.CASCADE, verbose_name='分类')
tags = models.ManyToManyField('Tag', verbose_name='标签')
content = models.TextField('正文')
status = models.CharField('状态', max_length=10,
choices=[('draft', '草稿'), ('published', '已发布')],
default='draft')
views = models.IntegerField('阅读量', default=0)
created_at = models.DateTimeField('创建时间', auto_now_add=True)
updated_at = models.DateTimeField('更新时间', auto_now=True)
# forms.py
from django import forms
from .models import Article
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ['title', 'slug', 'category', 'tags', 'content', 'status']
# 或者用 exclude 排除不需要的字段
# exclude = ['views', 'created_at', 'updated_at']
widgets = {
'content': forms.Textarea(attrs={'rows': 10, 'class': 'editor'}),
'tags': forms.CheckboxSelectMultiple,
}
labels = {
'slug': 'URL别名',
'content': '文章正文',
}
help_texts = {
'slug': '用于生成URL的唯一标识,只能包含字母、数字和连字符',
}
error_messages = {
'slug': {
'unique': '此URL别名已被使用,请更换一个',
},
}
6.2 fields 与 exclude
ModelForm的 Meta 类中,fields 和 exclude 是两个互斥的属性,用于控制表单包含哪些模型字段:
- fields = '__all__':包含模型中所有非自动生成的字段(不包含auto_now_add等自动字段)
- fields = ['title', 'content']:白名单模式,仅包含指定字段
- exclude = ['views', 'created_at']:黑名单模式,排除指定字段,其余全部包含
推荐始终使用 fields 白名单模式而非 exclude 黑名单模式。这遵循了"显式优于隐式"的Python哲学,能够避免因模型字段变更而导致意外暴露敏感字段的安全风险。
6.3 save() 方法
ModelForm 的 save() 方法封装了模型实例的创建或更新逻辑。当传入 commit=False 参数时,save() 会返回模型实例但不提交到数据库,这对需要在保存前进行额外处理的情况非常有用:
# views.py
from django.shortcuts import render, redirect
from .forms import ArticleForm
def article_create(request):
if request.method == 'POST':
form = ArticleForm(request.POST)
if form.is_valid():
# 基础用法:直接保存
article = form.save()
# 进阶用法:commit=False,手动处理后再保存
# article = form.save(commit=False)
# article.author = request.user # 设置外键或额外字段
# article.save() # 保存模型实例
# form.save_m2m() # 保存多对多关系
return redirect('article_detail', pk=article.pk)
else:
form = ArticleForm()
return render(request, 'blog/article_form.html', {'form': form})
def article_update(request, pk):
article = Article.objects.get(pk=pk)
if request.method == 'POST':
form = ArticleForm(request.POST, instance=article)
if form.is_valid():
form.save()
return redirect('article_detail', pk=article.pk)
else:
form = ArticleForm(instance=article)
return render(request, 'blog/article_form.html', {'form': form})
当 save() 被调用时,如果表单没有 instance 参数,Django会创建一个新的模型实例(INSERT);如果传入了 instance,则更新该实例(UPDATE)。save(commit=False) 在需要设置模型自动字段(如 created_by 外键)时尤为重要。
关键提醒:使用 save(commit=False) 时,如果模型有多对多字段(ManyToManyField),必须在手动调用 instance.save() 之后再调用 form.save_m2m(),否则多对多关系不会被持久化。如果 commit=True(默认值),Django会自动处理多对多关系。
6.4 表单与文件上传
处理包含文件上传的表单时,需要在表单定义中使用 FileField 或 ImageField,同时在视图的 render 函数中设置 enctype="multipart/form-data",视图函数需要接收 request.FILES:
# models.py
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
avatar = models.ImageField('头像', upload_to='avatars/')
resume = models.FileField('简历', upload_to='resumes/', blank=True)
# forms.py
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields = ['avatar', 'resume']
# views.py
def profile_edit(request):
profile = request.user.profile
if request.method == 'POST':
form = ProfileForm(request.POST, request.FILES, instance=profile)
# 注意:必须传入 request.FILES 作为第二个参数
if form.is_valid():
form.save()
return redirect('profile_detail')
else:
form = ProfileForm(instance=profile)
return render(request, 'accounts/profile_form.html', {'form': form})
{# 模板中必须设置 enctype #}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">保存</button>
</form>
文件上传涉及的关键配置还包括:MEDIA_ROOT(文件存储的物理路径)、MEDIA_URL(文件访问的URL前缀)、以及 settings.py 中的文件大小限制配置。生产环境中通常使用专用的存储后端(如阿里云OSS、AWS S3)来托管用户上传的文件。
七、核心要点总结
1. 模板是MTV架构的"视图层":Django模板系统实现了表现逻辑与业务逻辑的分离,模板文件专注于HTML结构的设计,而视图函数负责数据处理和上下文准备。
2. 模板继承是代码复用的核心:通过 {% extends %} 和 {% block %} 实现模板继承,通过 {% include %} 实现组件化复用,通过 {{ block.super }} 保留父模板内容。
3. 内置标签覆盖了绝大部分场景:循环、条件、URL反转、静态文件、国际化、CSRF保护等内置标签能够满足90%以上的模板开发需求。
4. 自定义标签满足个性化需求:过滤器(filter)、简单标签(simple_tag)、包含标签(inclusion_tag)三种形式,按需选择,将重复逻辑封装到templatetags包中。
5. 表单系统的三层验证机制:字段级验证(clean_<field>)→ 验证器列表(validators)→ 表单级验证(clean),层层递进确保数据质量。
6. ModelForm 是CRUD开发的利器:自动从模型生成表单,处理字段类型映射、数据验证和持久化。使用 fields 白名单而非 exclude,使用 save(commit=False) 处理额外逻辑。