Django 教程
Django 是一个高级的 Python Web 框架,以"大而全"著称:自带 ORM、Admin 后台、认证系统、表单处理等全套组件,让你"开箱即用"地构建完整网站。
Django 的核心特点
| 特点 | 说明 |
|---|---|
| 全能框架 | ORM、模板、表单、Admin、认证一应俱全 |
| MTV 架构 | Model-Template-View,职责清晰 |
| 自带 Admin | 自动生成后台管理界面 |
| 安全默认 | 内置防 XSS、CSRF、SQL 注入保护 |
| 生态成熟 | 20 年历史,文档完善,社区庞大 |
一个 Django 应用的样子
# models.py - 定义数据模型
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
# views.py - 定义视图
from django.shortcuts import render
def article_list(request):
articles = Article.objects.all()
return render(request, "list.html", {"articles": articles})
Django 与 FastAPI/Flask 的定位差异
| 框架 | 定位 | 适合场景 |
|---|---|---|
| Django | 全栈、大而全 | 内容站、后台系统、大型应用 |
| FastAPI | 高性能 API | 前后端分离、高并发 API |
| Flask | 轻量灵活 | 小项目、微服务、原型 |
谁在用 Django
- Instagram(照片分享)
- Pinterest(灵感收藏)
- Mozilla(Firefox 相关站点)
- Disqus(评论系统)
Django 的哲学是"为完美主义者而生的框架"——约定优于配置,把常见 Web 开发问题都替你考虑好了。
Django 简介
深入了解 Django 的设计理念、MTV 架构,以及它与传统 MVC 的区别。
MTV 架构
Django 采用 MTV 模式(与 MVC 类似但命名不同):
| Django 术语 | 传统 MVC | 职责 |
|---|---|---|
| Model(模型) | Model | 数据层:数据库表映射 |
| Template(模板) | View | 表现层:HTML 渲染 |
| View(视图) | Controller | 逻辑层:处理请求、调数据 |
一次请求的生命周期
- 浏览器发起请求,Django 的 URLconf 匹配路由
- 调用对应的 View 函数
- View 通过 Model 查询数据库
- View 把数据传给 Template 渲染
- 返回 HTML 响应给浏览器
浏览器 → urls.py → views.py → models.py → 数据库
↓
浏览器 ← template.html ← views.py
Django 的设计哲学
- DRY(Don't Repeat Yourself):避免重复代码
- 约定优于配置:遵循约定即可快速开发
- 可插拔应用:项目由多个 app 组成
- 安全默认:默认开启各类防护
项目 vs 应用
| 概念 | 说明 | 示例 |
|---|---|---|
| 项目(Project) | 整个网站 | myblog |
| 应用(App) | 项目中的功能模块 | articles、users、comments |
理解 MTV 是 Django 开发的第一步——知道"数据去哪、逻辑在哪、页面怎么出",后面学起来就顺了。
Django 安装
安装 Django 并创建虚拟环境,准备开发环境。
环境要求
- Python 3.8+(推荐 3.10+)
- pip
- 推荐虚拟环境
创建虚拟环境
python3 -m venv venv
source venv/bin/activate # macOS/Linux
venv\Scripts\activate # Windows
安装 Django
pip install django
# 验证版本
python -m django --version
# 例如:5.1
数据库驱动(MySQL 为例)
pip install mysqlclient # 或 pymysql
安装后验证
python -c "import django; print(django.get_version())"
常见问题
| 问题 | 解决 |
|---|---|
| mysqlclient 编译失败 | 改用 pymysql:pip install pymysql,在 __init__.py 中 import pymysql; pymysql.install_as_MySQLdb() |
| pip 下载慢 | 用国内镜像:-i https://pypi.tuna.tsinghua.edu.cn/simple |
| Python 版本过低 | 升级 Python 3.8+ |
使用虚拟环境隔离项目依赖,是 Django 开发的标准做法;部署到服务器时也会用到同样的 venv。
Django 创建第一个项目
创建你的第一个 Django 项目,跑通"创建 → 启动 → 访问"全流程。
创建项目
django-admin startproject myblog
cd myblog
# 创建应用
python manage.py startapp articles
项目结构
myblog/
├── manage.py # 管理命令入口
├── myblog/ # 项目配置
│ ├── __init__.py
│ ├── settings.py # 全局配置
│ ├── urls.py # 根路由
│ └── wsgi.py # WSGI 入口
└── articles/ # 应用
├── models.py # 数据模型
├── views.py # 视图
├── urls.py # 应用路由
└── apps.py # 应用配置
注册应用
# myblog/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
...
'articles', # 添加自己的应用
]
初始化数据库
python manage.py migrate
启动开发服务器
python manage.py runserver
# 访问 http://127.0.0.1:8000
# 看到 "The install worked successfully!" 即成功
第一个视图
# articles/views.py
from django.http import HttpResponse
def index(request):
return HttpResponse("Hello, Django!")
# articles/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
]
# myblog/urls.py
from django.urls import include, path
urlpatterns = [
path('', include('articles.urls')),
]
manage.py 是 Django 的瑞士军刀:启动、迁移、创建应用、创建管理员等操作都通过它完成。
Django django-admin
掌握 django-admin 与 manage.py 的常用命令,管理 Django 项目。
两个命令入口
| 命令 | 用途 |
|---|---|
django-admin | 全局命令:创建项目等 |
manage.py | 项目内命令(含全部 django-admin 能力) |
常用命令速查
| 命令 | 说明 |
|---|---|
django-admin startproject myblog | 创建项目 |
python manage.py startapp articles | 创建应用 |
python manage.py runserver | 启动开发服务器 |
python manage.py migrate | 应用数据库迁移 |
python manage.py makemigrations | 根据模型生成迁移文件 |
python manage.py createsuperuser | 创建管理员账号 |
python manage.py shell | 交互式 Python 环境(带 Django 上下文) |
python manage.py collectstatic | 收集静态文件(部署用) |
python manage.py check | 检查项目配置 |
查看全部命令
python manage.py help
shell 调试数据
python manage.py shell
>>> from articles.models import Article
>>> Article.objects.create(title="测试", content="内容")
>>> Article.objects.all()
创建管理员
python manage.py createsuperuser
# 用户名:admin
# 邮箱:(可跳过)
# 密码:****
开发时常用 runserver + shell;上线部署用 collectstatic + migrate。多敲命令,自然就熟了。
Django 项目结构解析
逐层解析 Django 项目结构,明白每个文件和目录的职责。
项目根目录
myblog/ # 项目根目录
├── manage.py # 管理入口
├── myblog/ # 项目配置包
│ ├── __init__.py # 包标记
│ ├── settings.py # 核心配置
│ ├── urls.py # 根 URL 路由
│ ├── asgi.py # ASGI 入口(异步)
│ └── wsgi.py # WSGI 入口(同步部署)
├── articles/ # 应用包
│ ├── __init__.py
│ ├── admin.py # Admin 注册
│ ├── apps.py # 应用配置类
│ ├── models.py # 数据模型
│ ├── views.py # 视图
│ ├── urls.py # 应用路由(自建)
│ ├── migrations/ # 迁移文件
│ ├── templates/ # 模板目录(自建)
│ └── static/ # 静态文件(自建)
├── templates/ # 项目级模板(自建)
├── static/ # 项目级静态文件(自建)
└── db.sqlite3 # 默认 SQLite 数据库
settings.py 关键配置
INSTALLED_APPS # 已安装的应用列表
MIDDLEWARE # 中间件列表
DATABASES # 数据库配置
TEMPLATES # 模板配置
STATIC_URL # 静态文件 URL 前缀
MEDIA_URL # 上传文件 URL 前缀
LANGUAGE_CODE # 语言(zh-hans 中文)
TIME_ZONE # 时区(Asia/Shanghai)
配置中文与 MySQL
# settings.py
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'myblog',
'USER': 'root',
'PASSWORD': 'password',
'HOST': '127.0.0.1',
'PORT': '3306',
'OPTIONS': {'charset': 'utf8mb4'},
}
}
urls.py 路由层级
# 根 urls.py 负责分发
urlpatterns = [
path('admin/', admin.site.urls),
path('articles/', include('articles.urls')),
]
# 应用 urls.py 负责细节
urlpatterns = [
path('', views.index),
path('/', views.detail),
]
先弄清"配置在哪、路由怎么分、代码放哪",Django 项目的骨架就印在脑子里了。
Django 模板
Django 模板(Template)是 MTV 中的 T,负责把数据渲染成 HTML 页面。
模板基础语法
<!-- 输出变量 -->
<h1>{{ article.title }}</h1>
<!-- if 判断 -->
{% if user.is_authenticated %}
<p>欢迎,{{ user.username }}</p>
{% else %}
<p>请登录</p>
{% endif %}
<!-- for 循环 -->
<ul>
{% for a in articles %}
<li>{{ a.title }}</li>
{% empty %}
<li>暂无文章</li>
{% endfor %}
</ul>
创建模板
articles/templates/
└── articles/
├── list.html
└── detail.html
# views.py
from django.shortcuts import render
def list_view(request):
articles = Article.objects.all()
return render(request, 'articles/list.html', {'articles': articles})
模板标签速查
| 标签 | 作用 |
|---|---|
{{ var }} | 输出变量 |
{% if %} | 条件判断 |
{% for %} | 循环 |
{% url 'name' %} | 按名称生成 URL |
{% extends %} | 模板继承 |
{% block %} | 块定义/填充 |
{% include %} | 包含其他模板 |
{% csrf_token %} | CSRF 令牌(表单必加) |
过滤器
{{ title | upper }} <!-- 转大写 -->
{{ content | truncatechars:30 }} <!-- 截断 -->
{{ date | date:"Y-m-d" }} <!-- 日期格式化 -->
{{ count | default:"0" }} <!-- 默认值 -->
模板继承
<!-- base.html -->
<html><body>
<nav>网站导航</nav>
{% block content %}{% endblock %}
</body></html>
<!-- list.html -->
{% extends "base.html" %}
{% block content %}
<h1>文章列表</h1>
{% endblock %}
模板自动转义变量内容,防 XSS;确实需要输出 HTML 时用 |safe 过滤器(仅限可信内容)。
Django 模型
Django 模型(Model)是 MTV 中的 M,用 Python 类描述数据库表,实现"以对象方式操作数据"。
定义模型
# articles/models.py
from django.db import models
class Category(models.Model):
name = models.CharField(max_length=50, unique=True)
sort_order = models.IntegerField(default=0)
class Meta:
ordering = ['sort_order']
def __str__(self):
return self.name
class Article(models.Model):
category = models.ForeignKey(Category,
title = models.CharField(max_length=100)
content = models.TextField()
views = models.IntegerField(default=0)
is_published = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return self.title
常用字段类型
| 字段 | 用途 |
|---|---|
CharField(max_length) | 短文本 |
TextField() | 长文本 |
IntegerField() | 整数 |
FloatField() | 浮点数 |
BooleanField() | 布尔 |
DateField() / DateTimeField() | 日期时间 |
ForeignKey() | 外键(多对一) |
ManyToManyField() | 多对多 |
OneToOneField() | 一对一 |
生成迁移
python manage.py makemigrations
python manage.py migrate
常见字段参数
| 参数 | 说明 |
|---|---|
unique=True | 唯一约束 |
null=True | 数据库允许 NULL |
blank=True | 表单允许为空 |
default=值 | 默认值 |
on_delete=models.CASCADE | 外键关联删除(级联) |
auto_now_add=True | 创建时自动填时间 |
auto_now=True | 每次保存更新为当前时间 |
改模型后一定要执行 makemigrations + migrate,否则数据库不会同步你的改动。
Django 表单
Django 表单处理用户输入:渲染表单、校验数据、显示错误。本节介绍基础用法。
用 Django Form 定义表单
# forms.py
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(max_length=50, label="姓名")
email = forms.EmailField(label="邮箱")
message = forms.CharField(widget=forms.Textarea, label="留言")
视图处理
# views.py
from django.shortcuts import render, redirect
from .forms import ContactForm
def contact(request):
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid(): # 校验
# 取出清洗后的数据
name = form.cleaned_data['name']
email = form.cleaned_data['email']
# 业务处理(发邮件、存库等)
return redirect('success')
else:
form = ContactForm()
return render(request, 'contact.html', {'form': form})
模板渲染
<form method="post">
{% csrf_token %}
{{ form.as_p }} <!-- 自动渲染所有字段 -->
<button type="submit">提交</button>
</form>
自定义校验
class RegisterForm(forms.Form):
username = forms.CharField(min_length=3, max_length=20)
password = forms.CharField(widget=forms.PasswordInput)
confirm = forms.CharField(widget=forms.PasswordInput)
def clean_confirm(self):
# 字段级校验:clean_字段名
if self.cleaned_data['password'] != self.cleaned_data['confirm']:
raise forms.ValidationError("两次密码不一致")
return self.cleaned_data['confirm']
ModelForm(最常用)
from django import forms
from .models import Article
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ['title', 'content', 'category']
# 或 exclude = ['views']
def create_article(request):
if request.method == 'POST':
form = ArticleForm(request.POST)
if form.is_valid():
form.save() # 直接保存到数据库
return redirect('list')
else:
form = ArticleForm()
return render(request, 'form.html', {'form': form})
模板中的表单必须加 {% csrf_token %},否则 POST 会被 Django 的安全中间件拦截(403)。
Django 视图
Django 视图(View)是 MTV 中的 V,负责"接收请求 → 处理逻辑 → 返回响应"。
最简视图
# views.py
from django.http import HttpResponse
def hello(request):
return HttpResponse("Hello, World!")
常用响应类型
| 响应 | 用途 |
|---|---|
HttpResponse | 返回文本 |
render() | 渲染模板返回 HTML |
redirect() | 重定向 |
JsonResponse | 返回 JSON |
Http404 | 抛 404 异常 |
典型视图
from django.shortcuts import render, get_object_or_404, redirect
from django.http import JsonResponse
from .models import Article
# 列表
def article_list(request):
articles = Article.objects.filter(is_published=True)
return render(request, 'list.html', {'articles': articles})
# 详情
def article_detail(request, pk):
article = get_object_or_404(Article, pk=pk)
return render(request, 'detail.html', {'article': article})
# 重定向
def publish(request, pk):
article = get_object_or_404(Article, pk=pk)
article.is_published = True
article.save()
return redirect('article_detail', pk=pk)
# JSON 接口
def api_detail(request, pk):
article = get_object_or_404(Article, pk=pk)
return JsonResponse({
'id': article.id,
'title': article.title,
})
视图中的请求对象
def debug_request(request):
print(request.method) # GET / POST
print(request.GET) # 查询参数
print(request.POST) # 表单数据
print(request.user) # 当前用户
print(request.session) # Session
return HttpResponse("ok")
FBV 与 CBV
视图有两种写法:函数视图(FBV)和类视图(CBV)。本节先掌握 FBV,类视图在后续章节介绍。
get_object_or_404 是常用技巧:对象不存在时自动返回 404,省去手动判断。
Django 路由
URL 路由(URLconf)把 URL 映射到视图函数,是 Django 请求分发的核心。
基本路由
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
path('articles/', views.article_list, name='article_list'),
path('articles//', views.article_detail, name='article_detail'),
]
路径转换器
| 转换器 | 匹配 | 示例 |
|---|---|---|
<int:pk> | 整数 | /articles/42/ |
<str:slug> | 字符串 | /articles/hello/ |
<slug:slug> | 字母数字横线 | /articles/my-post/ |
<uuid:uid> | UUID | /articles/xxxx/ |
<path:rest> | 含斜杠路径 | /files/a/b.txt |
路由命名与反向解析
# 给路由命名(name),模板/视图用名称引用
path('articles//', views.detail, name='article_detail')
# 视图中使用
from django.urls import reverse
url = reverse('article_detail', args=[42])
# 模板中使用
<a href="{% url 'article_detail' article.pk %}">查看</a>
include 包含子路由
# 根 urls.py
from django.urls import include, path
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('articles.urls')), # 分发到应用
path('users/', include('users.urls')),
]
re_path 正则路由(旧式)
from django.urls import re_path
urlpatterns = [
re_path(r'^articles/(?P\d+)/$', views.detail),
]
默认 404/500 页面
Django 自动处理 404(Http404)与 500 错误,生产环境可自定义 templates/404.html、templates/500.html。
给每个路由起 name,模板和视图都用名称而不是硬编码 URL,改地址时只改 urls.py 一处即可。
Django Admin 管理工具
Django Admin 是自动生成的后台管理界面,无需写代码即可管理数据,是 Django 的招牌功能。
启用 Admin
# 创建管理员账号
python manage.py createsuperuser
# 启动后访问
# http://127.0.0.1:8000/admin/
注册模型
# articles/admin.py
from django.contrib import admin
from .models import Article, Category
admin.site.register(Article)
admin.site.register(Category)
自定义显示列
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
list_display = ('id', 'title', 'category', 'views', 'is_published', 'created_at')
list_filter = ('is_published', 'category') # 右侧筛选
search_fields = ('title', 'content') # 搜索框
ordering = ('-created_at',) # 排序
list_editable = ('is_published',) # 列表页直接编辑
date_hierarchy = 'created_at' # 日期钻取
readonly_fields = ('views',) # 只读字段
自定义表单布局
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
fieldsets = (
('基本信息', {'fields': ('name', 'sort_order')}),
)
list_display = ('id', 'name', 'sort_order')
Admin 常用选项
| 选项 | 作用 |
|---|---|
list_display | 列表显示的列 |
list_filter | 右侧筛选栏 |
search_fields | 搜索框搜索字段 |
list_per_page | 每页条数 |
ordering | 默认排序 |
actions | 批量操作 |
Admin 界面中文化
# settings.py
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
Admin 适合内部管理;面向用户的后台建议自己写(用 Django 表单 + 视图),界面和权限更可控。
Django ORM - 单表实例
通过单表实例掌握 Django ORM 的增删改查(CRUD),用 Python 代码操作数据库。
创建数据
# 方式一:create()
Article.objects.create(title="第一篇", content="内容1")
# 方式二:先建后存
a = Article(title="第二篇", content="内容2")
a.save()
# 方式三:get_or_create(存在则取,否则建)
obj, created = Article.objects.get_or_create(title="第三篇",
defaults={'content': '内容3'})
查询数据
# 全部
Article.objects.all()
# 过滤
Article.objects.filter(is_published=True)
# 排除
Article.objects.exclude(category_id=1)
# 单个
Article.objects.get(pk=1) # 不存在报错
Article.objects.filter(pk=1).first() # 不存在返回 None
# 排序
Article.objects.order_by('-created_at') # 倒序
Article.objects.order_by('title') # 正序
# 限制数量
Article.objects.all()[:10] # 前 10 条
更新数据
# 单条
a = Article.objects.get(pk=1)
a.title = "修改后的标题"
a.save()
# 批量更新
Article.objects.filter(is_published=False).update(is_published=True)
# 自增操作
Article.objects.filter(pk=1).update(views=models.F('views') + 1)
删除数据
# 单条
a = Article.objects.get(pk=1)
a.delete()
# 批量删除
Article.objects.filter(is_published=False).delete()
其他常用查询
# 数量
Article.objects.count()
# 存在性
Article.objects.filter(pk=1).exists()
# 去重 / 值列表
Article.objects.values_list('title', flat=True)
# 聚合
from django.db.models import Count, Sum, Avg
Article.objects.aggregate(total=Count('id'), avg_views=Avg('views'))
ORM 查询是惰性的:只有真正使用时才执行 SQL。加 print(queryset.query) 可查看生成的 SQL 语句,便于优化。
Django ORM - 多表实例
通过多表实例掌握 Django ORM 的关系查询:一对多、多对多、反向查询。
一对多(外键)
class Category(models.Model):
name = models.CharField(max_length=50)
class Article(models.Model):
category = models.ForeignKey(Category,
related_name='articles')
title = models.CharField(max_length=100)
# 正向查询:文章 → 分类
a = Article.objects.get(pk=1)
a.category.name
# 反向查询:分类 → 文章(related_name)
c = Category.objects.get(pk=1)
c.articles.all()
# 带条件反向查询
c.articles.filter(is_published=True)
# 通过外键过滤
Article.objects.filter(category__name='Python')
多对多
class Tag(models.Model):
name = models.CharField(max_length=30)
class Article(models.Model):
tags = models.ManyToManyField(Tag, related_name='articles')
# ...
# 添加标签
a = Article.objects.get(pk=1)
t = Tag.objects.get(name='python')
a.tags.add(t)
# 移除
a.tags.remove(t)
# 查询
a.tags.all() # 文章的标签
Tag.objects.filter(articles__id=1) # 含标签的文章
一对一
class Profile(models.Model):
user = models.OneToOneField(User,
bio = models.TextField(blank=True)
# profile.user 正向;user.profile 反向
跨表过滤(双下划线)
# 文章按分类名过滤
Article.objects.filter(category__name__icontains='py')
# 文章按标签名过滤
Article.objects.filter(tags__name='ai')
# 评论文章的用户
Comment.objects.filter(article__author__username='admin')
# 反向外键字段过滤
Category.objects.filter(articles__is_published=True).distinct()
select_related 与 prefetch_related
# select_related:外键(一对一/多对一)JOIN 预取
articles = Article.objects.select_related('category')
# prefetch_related:多对多/反向 预取
categories = Category.objects.prefetch_related('articles')
# 避免 N+1 查询,大幅提升性能
related_name 控制反向查询名;遇到列表页性能问题,优先加 select_related/prefetch_related。
Django ORM - 聚合查询
使用 Django ORM 聚合查询:分组统计、求和、计数、最大值最小值等。
聚合函数
from django.db.models import Count, Sum, Avg, Max, Min
# 总数
Article.objects.count()
# 求和(浏览量)
Article.objects.aggregate(total_views=Sum('views'))
# 平均
Article.objects.aggregate(avg_views=Avg('views'))
# 最大/最小
Article.objects.aggregate(max_views=Max('views'), min_views=Min('views'))
annotate 分组统计
from django.db.models import Count
# 每个分类下的文章数
categories = Category.objects.annotate(
article_count=Count('articles')
)
for c in categories:
print(c.name, c.article_count)
分组聚合示例
# 按是否发布分组统计
Article.objects.values('is_published').annotate(
total=Count('id'), total_views=Sum('views')
)
# 按分类分组统计浏览量
Article.objects.values('category__name').annotate(
total_views=Sum('views')
).order_by('-total_views')
条件聚合
from django.db.models import Count, Q
# 每个分类已发布文章数
Category.objects.annotate(
published_count=Count('articles', filter=Q(articles__is_published=True))
)
# 或先过滤再统计
Article.objects.filter(is_published=True).values('category').annotate(
count=Count('id')
)
实战:首页统计面板
def stats(request):
from django.db.models import Count, Sum
data = {
'total_articles': Article.objects.count(),
'total_views': Article.objects.aggregate(s=Sum('views'))['s'] or 0,
'top_categories': list(
Category.objects.annotate(
n=Count('articles')
).order_by('-n')[:5].values('name', 'n')
),
}
return JsonResponse(data)
annotate 生成 GROUP BY 查询,配合 values() 可实现任意分组统计;注意空值处理(用 or 0 兜底)。
Django Form 组件
深入 Django Form 组件:字段类型、Widget、样式定制与高级校验。
常用字段类型
| 字段 | HTML 控件 |
|---|---|
CharField | 单行输入 |
EmailField | 邮箱输入(自动校验格式) |
URLField | URL 输入 |
IntegerField | 数字输入 |
DecimalField | 小数输入 |
BooleanField | 复选框 |
ChoiceField | 下拉选择 |
MultipleChoiceField | 多选 |
DateField / DateTimeField | 日期输入 |
FileField / ImageField | 文件/图片上传 |
下拉选择
class ArticleForm(forms.Form):
CATEGORY_CHOICES = [
('frontend', '前端'),
('backend', '后端'),
('ai', 'AI'),
]
category = forms.ChoiceField(choices=CATEGORY_CHOICES)
Widget 定制样式
class StyledForm(forms.Form):
title = forms.CharField(
max_length=100,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '请输入标题',
}),
)
content = forms.CharField(
widget=forms.Textarea(attrs={
'rows': 8,
'class': 'form-control',
}),
)
publish = forms.BooleanField(
required=False,
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'}),
)
表单整体校验
class RegisterForm(forms.Form):
username = forms.CharField(min_length=3)
password = forms.CharField(min_length=6)
confirm = forms.CharField()
def clean(self):
# 跨字段校验
cleaned = super().clean()
if cleaned.get('password') != cleaned.get('confirm'):
self.add_error('confirm', '两次密码不一致')
return cleaned
动态下拉(查询数据库)
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ['title', 'category', 'content']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 数据库分类作为下拉选项
self.fields['category'].queryset = Category.objects.filter(is_active=True)
用 widget=forms.TextInput(attrs={...}) 加 Bootstrap/Tailwind 类名,表单样式就能和前端框架融合。
Django Auth
Django Auth 是内置的用户认证系统,提供用户模型、登录、注册、权限等完整能力。
认证系统的组成
- User 模型:默认用户模型(username/password/email)
- 登录/登出视图:
LoginView/LogoutView - 权限系统:permission + group
- 密码管理:哈希存储、重置
登录视图(内置)
# urls.py
from django.contrib.auth.views import LoginView, LogoutView
urlpatterns = [
path('login/', LoginView.as_view(
template_name='registration/login.html'), name='login'),
path('logout/', LogoutView.as_view(), name='logout'),
]
登录模板
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">登录</button>
</form>
手动登录逻辑
from django.contrib.auth import authenticate, login
def my_login(request):
if request.method == 'POST':
username = request.POST['username']
password = request.POST['password']
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
return redirect('home')
return render(request, 'login.html', {'error': '用户名或密码错误'})
return render(request, 'login.html')
注册功能
from django.contrib.auth.models import User
from django.contrib.auth import login
def register(request):
if request.method == 'POST':
username = request.POST['username']
password = request.POST['password']
if User.objects.filter(username=username).exists():
return render(request, 'register.html', {'error': '用户名已存在'})
user = User.objects.create_user(username=username, password=password)
login(request, user) # 注册后自动登录
return redirect('home')
return render(request, 'register.html')
模板中判断登录
{% if user.is_authenticated %}
<p>你好,{{ user.username }}</p>
<a href="{% url 'logout' %}">退出</a>
{% else %}
<a href="{% url 'login' %}">登录</a>
{% endif %}
视图权限控制
from django.contrib.auth.decorators import login_required
@login_required # 未登录跳转到登录页
def profile(request):
return render(request, 'profile.html')
# 类视图写法
from django.contrib.auth.mixins import LoginRequiredMixin
class ProfileView(LoginRequiredMixin, TemplateView):
template_name = 'profile.html'
自定义用户模型(推荐)
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
avatar = models.ImageField(upload_to='avatars/', blank=True)
bio = models.TextField(blank=True)
# settings.py
AUTH_USER_MODEL = 'accounts.User' # 第一个迁移前就要设置
自定义用户模型必须在第一次 migrate 之前配置好,否则后续更换会非常麻烦。
Django Cookie/Session
使用 Cookie 与 Session 实现状态保持,记录用户会话。
Cookie 与 Session 的关系
- Cookie:存在浏览器的小数据,自动随请求携带
- Session:存在服务器端的数据,通过 Cookie 中的 sessionid 关联
读取 Session
def view_session(request):
# 读取
count = request.session.get('visit_count', 0)
# 写入
request.session['visit_count'] = count + 1
request.session['user_id'] = 42
# 删除
# del request.session['user_id']
return HttpResponse(f"访问次数:{count + 1}")
登录状态保持
def login_with_session(request):
user = authenticate(...)
if user:
login(request, user) # Django 自动写 session
return redirect('home')
return render(request, 'login.html')
操作 Cookie(直接)
def set_cookie(request):
response = HttpResponse("ok")
response.set_cookie(
'theme', 'dark',
max_age=3600, # 1 小时
httponly=True, # JS 不可读
path='/',
)
return response
def get_cookie(request):
theme = request.COOKIES.get('theme', 'light')
return HttpResponse(f"主题:{theme}")
Session 配置
# settings.py
SESSION_ENGINE = 'django.contrib.sessions.backends.db' # 默认:存数据库
# 可选:cache / file / cookie
SESSION_COOKIE_AGE = 86400 # 会话时长(秒),默认 2 周
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
SESSION_COOKIE_HTTPONLY = True # 防 XSS 窃取
实战:购物车(Session)
def add_to_cart(request, product_id):
cart = request.session.get('cart', {})
cart[product_id] = cart.get(product_id, 0) + 1
request.session['cart'] = cart
return redirect('cart')
def show_cart(request):
cart = request.session.get('cart', {})
return render(request, 'cart.html', {'cart': cart})
清空会话
from django.contrib.auth import logout
def logout_view(request):
logout(request) # 清空 session 并登出
return redirect('home')
敏感数据存 Session(服务器端),非敏感偏好(主题等)可存 Cookie;不要用 Cookie 存密码等敏感信息。
Django 中间件
Django 中间件(Middleware)是请求/响应处理链上的钩子,在视图前后执行,用于日志、认证、安全等横切逻辑。
中间件工作原理
请求 → 中间件1 → 中间件2 → ... → 视图
响应 ← 中间件1 ← 中间件2 ← ... ← 视图
内置中间件
# settings.py 默认的 MIDDLEWARE
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', # 安全头
'django.contrib.sessions.middleware.SessionMiddleware', # Session
'django.middleware.common.CommonMiddleware', # 常用
'django.middleware.csrf.CsrfViewMiddleware', # CSRF 防护
'django.contrib.auth.middleware.AuthenticationMiddleware', # 用户
'django.contrib.messages.middleware.MessageMiddleware', # 消息
]
自定义中间件
# middleware.py
import time
class RequestTimeMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
start = time.time()
# 请求处理前
response = self.get_response(request) # 调用下一个
# 响应处理后
duration = time.time() - start
response['X-Request-Time'] = f'{duration:.3f}s'
return response
# settings.py 注册
MIDDLEWARE += [
'articles.middleware.RequestTimeMiddleware',
]
登录拦截中间件
class LoginRequiredMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# 白名单路径直接放行
allowed = ['/admin/', '/login/', '/static/']
if not request.user.is_authenticated and not any(request.path.startswith(p) for p in allowed):
return redirect('/login/')
return self.get_response(request)
维护模式中间件
class MaintenanceMiddleware:
def __call__(self, request):
if MAINTENANCE_MODE:
return HttpResponse("网站维护中,请稍后再来", status=503)
return self.get_response(request)
中间件执行顺序
请求按 MIDDLEWARE 列表从上到下执行 __call__,响应从下到上返回。加中间件时注意顺序(安全类放前面)。
中间件是全局的,影响所有请求;只针对部分接口的逻辑用装饰器(@login_required)更合适。
Django 视图 - FBV 与 CBV
Django 视图的两种写法:函数视图(FBV)与类视图(CBV),以及各自的适用场景。
FBV 函数视图
from django.shortcuts import render, redirect
def article_list(request):
articles = Article.objects.all()
return render(request, 'list.html', {'articles': articles})
CBV 类视图
from django.views import View
from django.shortcuts import render
class ArticleListView(View):
template_name = 'list.html'
def get(self, request):
# GET 请求
articles = Article.objects.all()
return render(request, self.template_name, {'articles': articles})
def post(self, request):
# POST 请求
Article.objects.create(title=request.POST['title'])
return redirect('list')
通用类视图(Generic CBV)
Django 提供了大量开箱即用的通用视图,极大减少样板代码:
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
class ArticleList(ListView):
model = Article
template_name = 'list.html'
context_object_name = 'articles'
paginate_by = 10
class ArticleDetail(DetailView):
model = Article
template_name = 'detail.html'
class ArticleCreate(CreateView):
model = Article
fields = ['title', 'content', 'category']
template_name = 'form.html'
success_url = '/articles/'
class ArticleUpdate(UpdateView):
model = Article
fields = ['title', 'content']
template_name = 'form.html'
success_url = '/articles/'
class ArticleDelete(DeleteView):
model = Article
success_url = '/articles/'
template_name = 'confirm_delete.html'
# urls.py 对应
path('', ArticleList.as_view()),
path('/', ArticleDetail.as_view()),
path('create/', ArticleCreate.as_view()),
path('/update/', ArticleUpdate.as_view()),
path('/delete/', ArticleDelete.as_view()),
FBV vs CBV 怎么选
| 对比项 | FBV | CBV |
|---|---|---|
| 上手难度 | 简单直观 | 需理解继承体系 |
| 代码复用 | 装饰器 | 继承/混入(更强) |
| 标准 CRUD | 手写多 | 通用视图极简 |
| 复杂逻辑 | 更直接 | 覆盖方法较绕 |
新手建议先用 FBV 理解流程;熟悉后对标准 CRUD 用 CBV 通用视图,效率翻倍。
Django Nginx+uwsgi
生产环境部署 Django:Nginx + uwsgi(或 gunicorn)的经典组合。
部署架构
浏览器 → Nginx(静态文件 + 反向代理)→ uwsgi → Django
安装 uwsgi
pip install uwsgi
# 测试 uwsgi 能否启动 Django
uwsgi --http :8000 --module myblog.wsgi
uwsgi 配置文件
# uwsgi.ini
[uwsgi]
chdir = /www/wwwroot/myblog
module = myblog.wsgi:application
master = true
processes = 4
threads = 2
socket = 127.0.0.1:8001
chmod-socket = 664
vacuum = true
die-on-term = true
# 日志
logto = /www/wwwroot/myblog/logs/uwsgi.log
# 启动
uwsgi --ini uwsgi.ini
收集静态文件
# settings.py
STATIC_ROOT = '/www/wwwroot/myblog/staticfiles'
STATIC_URL = '/static/'
# 收集所有应用静态文件到 STATIC_ROOT
python manage.py collectstatic
Nginx 配置
server {
listen 80;
server_name www.laowang.click;
# 静态文件由 Nginx 直接服务
location /static/ {
alias /www/wwwroot/myblog/staticfiles/;
expires 7d;
}
# 动态请求转发到 uwsgi
location / {
include uwsgi_params;
uwsgi_pass 127.0.0.1:8001;
}
}
gunicorn 替代方案
pip install gunicorn
gunicorn myblog.wsgi:application -w 4 -b 127.0.0.1:8001
# nginx 反代
# proxy_pass http://127.0.0.1:8001;
settings.py 生产配置
DEBUG = False # 必须关闭!
ALLOWED_HOSTS = ['www.laowang.click', 'laowang.click']
# 安全设置
SECURE_SSL_REDIRECT = True # 强制 HTTPS
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
宝塔面板部署
- 创建 Python 项目,上传代码
- 安装依赖,运行迁移
python manage.py migrate - 收集静态文件
python manage.py collectstatic - 启动命令设为 uwsgi/gunicorn
- 添加 Nginx 反代 + SSL 证书
生产环境必须 DEBUG=False 并配置 ALLOWED_HOSTS,否则存在严重安全风险(信息泄露、伪造 Host 攻击)。
Django 博客项目
综合运用 Django 全部知识,构建一个完整的博客项目。
项目规划
功能清单:
1. 文章列表(分页 + 分类筛选)
2. 文章详情(含浏览量统计)
3. 分类管理
4. 用户注册/登录
5. 文章发布(表单)
6. 收藏功能
7. 后台管理(Admin)
项目结构
myblog/
├── myblog/ # 项目配置
├── articles/ # 文章应用
│ ├── models.py # Article/Category
│ ├── views.py # 视图
│ ├── urls.py # 路由
│ ├── forms.py # 表单
│ └── admin.py
├── users/ # 用户应用
│ ├── views.py # 注册/登录
│ └── urls.py
├── templates/ # 模板
├── static/ # 静态文件
└── manage.py
核心模型
# articles/models.py
from django.db import models
from django.contrib.auth.models import User
class Category(models.Model):
name = models.CharField(max_length=50)
slug = models.SlugField(unique=True)
def __str__(self):
return self.name
class Article(models.Model):
title = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
content = models.TextField()
category = models.ForeignKey(Category, related_name='articles')
author = models.ForeignKey(User, related_name='articles')
views = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return self.title
class Favorite(models.Model):
user = models.ForeignKey(User,
article = models.ForeignKey(Article, related_name='favorites')
class Meta:
unique_together = ('user', 'article')
列表视图(分页 + 分类)
from django.core.paginator import Paginator
def article_list(request, category_slug=None):
articles = Article.objects.select_related('category', 'author').all()
if category_slug:
articles = articles.filter(category__slug=category_slug)
paginator = Paginator(articles, 10) # 每页 10 篇
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
return render(request, 'list.html', {
'page_obj': page_obj,
'categories': Category.objects.all(),
})
详情视图(浏览量+1)
from django.shortcuts import get_object_or_404
def article_detail(request, slug):
article = get_object_or_404(Article, slug=slug)
Article.objects.filter(pk=article.pk).update(views=models.F('views') + 1)
article.refresh_from_db()
return render(request, 'detail.html', {'article': article})
分页用 Django 内置 Paginator;列表页用 select_related 预取外键,避免 N+1 查询。
模型与数据库
深入博客项目的模型与数据库设计:字段选择、关系、索引与查询优化。
完善的模型设计
class Article(models.Model):
title = models.CharField(max_length=100, db_index=True) # 标题索引
slug = models.SlugField(unique=True, db_index=True) # 唯一+索引
summary = models.CharField(max_length=200, blank=True) # 摘要
content = models.TextField()
cover = models.ImageField(upload_to='covers/', blank=True) # 封面图
status = models.CharField(max_length=10, choices=[
('draft', '草稿'), ('published', '已发布'),
], default='draft', db_index=True)
views = models.PositiveIntegerField(default=0)
category = models.ForeignKey('Category', related_name='articles')
tags = models.ManyToManyField('Tag', blank=True, related_name='articles')
author = models.ForeignKey(User, related_name='articles')
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['status', '-created_at']), # 联合索引
]
on_delete 的选择
| 选项 | 行为 | 适用 |
|---|---|---|
CASCADE | 级联删除 | 文章删了评论跟着删 |
PROTECT | 有引用禁止删除 | 分类下有文章不可删 |
SET_NULL | 置空 | 需 null=True |
SET_DEFAULT | 设默认值 | 需 default=... |
迁移与回滚
python manage.py makemigrations articles
python manage.py migrate
python manage.py migrate articles 0001_initial # 回滚到指定迁移
查询优化技巧
# N+1 问题:循环里查库
# 坏:for a in articles: a.category.name → N 次查询
# 好:select_related 预取外键
articles = Article.objects.select_related('category', 'author')
# 多对多预取
articles = Article.objects.prefetch_related('tags')
# 只取需要的字段
Article.objects.only('title', 'slug')
# 查看 SQL
print(Article.objects.all().query)
媒体文件配置
# settings.py
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# urls.py(开发环境)
from django.conf import settings
from django.conf.urls.static import static
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
数据库设计先想清楚关系(1:N / M:N / 1:1)和查询路径,再加索引,比事后优化高效得多。
Django Admin
博客项目的 Django Admin 后台配置:让内容管理更方便。
完整 Admin 配置
# articles/admin.py
from django.contrib import admin
from .models import Article, Category, Tag
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'slug', 'article_count')
prepopulated_fields = {'slug': ('name',)} # slug 自动生成
def article_count(self, obj):
return obj.articles.count()
article_count.short_description = '文章数'
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
list_display = ('id', 'name')
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
list_display = ('id', 'title', 'category', 'status', 'views', 'author', 'created_at')
list_filter = ('status', 'category', 'created_at')
search_fields = ('title', 'content')
prepopulated_fields = {'slug': ('title',)}
list_editable = ('status',)
date_hierarchy = 'created_at'
autocomplete_fields = ('category', 'tags')
readonly_fields = ('views', 'created_at', 'updated_at')
# 表单分组
fieldsets = (
('基本信息', {'fields': ('title', 'slug', 'summary', 'cover')}),
('正文', {'fields': ('content',)}),
('分类与标签', {'fields': ('category', 'tags')}),
('发布', {'fields': ('status', 'author', 'views')}),
)
def save_model(self, request, obj, form, change):
if not obj.author_id:
obj.author = request.user # 自动填当前作者
super().save_model(request, obj, form, change)
Admin 常用装饰器
| 装饰器 | 作用 |
|---|---|
@admin.register(Model) | 注册模型(替代 admin.site.register) |
@admin.display(description='...') | 自定义列名 |
@admin.action(description='...') | 自定义批量操作 |
自定义批量操作
@admin.action(description='批量发布所选文章')
def make_published(modeladmin, request, queryset):
queryset.update(status='published')
class ArticleAdmin(admin.ModelAdmin):
actions = [make_published]
Inline 内联编辑
class CommentInline(admin.TabularInline): # 或 StackedInline
model = Comment
extra = 1
class ArticleAdmin(admin.ModelAdmin):
inlines = [CommentInline] # 文章编辑页直接管理评论
prepopulated_fields 自动根据标题生成 slug;autocomplete_fields 大数据量外键选择体验极佳。
视图与模板
博客项目的视图与模板:列表、详情、表单页的完整实现。
列表页视图
from django.views.generic import ListView
class ArticleListView(ListView):
model = Article
template_name = 'articles/list.html'
context_object_name = 'articles'
paginate_by = 10
def get_queryset(self):
qs = super().get_queryset() .filter(status='published') .select_related('category', 'author')
# 分类筛选
slug = self.kwargs.get('category_slug')
if slug:
qs = qs.filter(category__slug=slug)
# 搜索
q = self.request.GET.get('q')
if q:
qs = qs.filter(title__icontains=q)
return qs
列表模板
{% extends "base.html" %}
{% block content %}
<h1>文章列表</h1>
{% for article in articles %}
<article class="card">
<h2><a href="{% url 'detail' article.slug %}">{{ article.title }}</a></h2>
<p>{{ article.summary }}</p>
<small>
{{ article.category.name }} · {{ article.author.username }}
· {{ article.created_at|date:"Y-m-d" }} · {{ article.views }} 次浏览
</small>
</article>
{% empty %}
<p>暂无文章</p>
{% endfor %}
<!-- 分页 -->
{% if is_paginated %}
<div class="pagination">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">上一页</a>
{% endif %}
<span>{{ page_obj.number }} / {{ page_obj.paginator.num_pages }}</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">下一页</a>
{% endif %}
</div>
{% endif %}
{% endblock %}
详情页视图与模板
from django.views.generic import DetailView
class ArticleDetailView(DetailView):
model = Article
template_name = 'articles/detail.html'
slug_field = 'slug'
def get_object(self, queryset=None):
obj = super().get_object(queryset)
Article.objects.filter(pk=obj.pk).update(views=models.F('views') + 1)
return obj
<h1>{{ article.title }}</h1>
<div>{{ article.content|linebreaks }}</div>
<p>浏览量:{{ article.views }}</p>
路由
urlpatterns = [
path('', ArticleListView.as_view(), name='list'),
path('category/<slug:category_slug>/', ArticleListView.as_view(), name='list_by_category'),
path('<slug:slug>/', ArticleDetailView.as_view(), name='detail'),
]
CBV 的 get_queryset() 是过滤逻辑的入口;模板中 {% empty %} 处理空列表,分页模板可直接复用。
URL 路由
博客项目的 URL 路由 设计:命名空间、动态参数、反向解析。
带命名空间的路由
# articles/urls.py
from django.urls import path
from . import views
app_name = 'articles' # 命名空间
urlpatterns = [
path('', views.article_list, name='list'),
path('category//', views.article_list, name='list_by_category'),
path('/', views.article_detail, name='detail'),
path('create/', views.article_create, name='create'),
path('/update/', views.article_update, name='update'),
path('/delete/', views.article_delete, name='delete'),
path('/favorite/', views.toggle_favorite, name='favorite'),
]
# 根 urls.py
urlpatterns = [
path('admin/', admin.site.urls),
path('articles/', include('articles.urls')),
path('', include('articles.urls')), # 首页即文章列表
]
反向解析(视图中)
from django.urls import reverse
# 带命名空间
url = reverse('articles:detail', args=[article.slug])
url = reverse('articles:list')
# 带查询参数
url = reverse('articles:list') + '?page=2'
反向解析(模板中)
<a href="{% url 'articles:detail' article.slug %}">详情</a>
<a href="{% url 'articles:list_by_category' article.category.slug %}">{{ article.category.name }}</a>
<!-- 带 GET 参数 -->
<a href="{% url 'articles:list' %}?q=python">搜索 Python</a>
路由匹配规则
- 按顺序匹配,第一个匹配成功的生效
- 动态路由(
<slug:slug>)放静态路由(create/)之后 - 否则
create/会被当作 slug 匹配
URL 命名最佳实践
- 小写 + 下划线:
article_detail - 命名空间避免 app 间冲突:
articles:detail - 模板永远用
{% url %},不硬编码路径
用 app_name 声明命名空间后,模板/视图里的 url 都要带前缀(articles:detail)。
分类筛选
博客项目的分类筛选:按分类过滤文章,展示分类列表与计数。
模型
class Category(models.Model):
name = models.CharField(max_length=50)
slug = models.SlugField(unique=True)
class Meta:
ordering = ['id']
分类筛选视图
from django.views.generic import ListView
class ArticleListByCategory(ListView):
model = Article
template_name = 'articles/list.html'
context_object_name = 'articles'
paginate_by = 10
def get_queryset(self):
return Article.objects.filter(
category__slug=self.kwargs['slug'],
status='published',
).select_related('category')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['current_category'] = get_object_or_404(
Category, slug=self.kwargs['slug'])
context['categories'] = Category.objects.all()
return context
分类带计数(annotate)
from django.db.models import Count
def sidebar_categories():
return Category.objects.annotate(
article_count=Count('articles', filter=Q(articles__status='published'))
).order_by('-article_count')
模板侧边栏
<aside>
<h3>分类</h3>
<ul>
<li>
<a href="{% url 'articles:list' %}">全部</a>
</li>
{% for cat in categories %}
<li>
<a href="{% url 'articles:list_by_category' cat.slug %}"
{% if current_category.id == cat.id %}class="active"{% endif %}>
{{ cat.name }} ({{ cat.article_count }})
</a>
</li>
{% endfor %}
</ul>
</aside>
筛选 + 搜索 + 分页组合
class ArticleFilterView(ListView):
model = Article
template_name = 'articles/list.html'
paginate_by = 10
def get_queryset(self):
qs = Article.objects.filter(status='published')
# 分类
if slug := self.kwargs.get('slug'):
qs = qs.filter(category__slug=slug)
# 搜索词
if q := self.request.GET.get('q'):
qs = qs.filter(title__icontains=q)
# 排序
order = self.request.GET.get('order', 'newest')
qs = qs.order_by('-created_at' if order == 'newest' else 'created_at')
return qs
侧边栏分类用 annotate 一次性带出文章计数,避免循环中 cat.articles.count() 的 N+1 查询。
用户系统
博客项目的用户系统:注册、登录、个人中心、权限控制。
注册视图
# users/views.py
from django.contrib.auth.models import User
from django.contrib.auth import login
from django.shortcuts import render, redirect
from .forms import RegisterForm
def register(request):
if request.method == 'POST':
form = RegisterForm(request.POST)
if form.is_valid():
user = User.objects.create_user(
username=form.cleaned_data['username'],
password=form.cleaned_data['password'],
email=form.cleaned_data.get('email', ''),
)
login(request, user) # 注册后自动登录
return redirect('articles:list')
else:
form = RegisterForm()
return render(request, 'users/register.html', {'form': form})
注册表单
# users/forms.py
from django import forms
class RegisterForm(forms.Form):
username = forms.CharField(min_length=3, max_length=20)
email = forms.EmailField(required=False)
password = forms.CharField(widget=forms.PasswordInput, min_length=6)
confirm = forms.CharField(widget=forms.PasswordInput)
def clean_username(self):
username = self.cleaned_data['username']
if User.objects.filter(username=username).exists():
raise forms.ValidationError('用户名已被占用')
return username
def clean(self):
cleaned = super().clean()
if cleaned.get('password') != cleaned.get('confirm'):
self.add_error('confirm', '两次密码不一致')
return cleaned
个人中心
from django.contrib.auth.decorators import login_required
@login_required
def profile(request):
my_articles = Article.objects.filter(author=request.user)
my_favorites = Article.objects.filter(favorites__user=request.user)
return render(request, 'users/profile.html', {
'my_articles': my_articles,
'my_favorites': my_favorites,
})
发布文章权限
@login_required
def article_create(request):
if request.method == 'POST':
form = ArticleForm(request.POST)
if form.is_valid():
article = form.save(commit=False)
article.author = request.user # 作者取当前用户
article.save()
return redirect('articles:detail', slug=article.slug)
else:
form = ArticleForm()
return render(request, 'articles/form.html', {'form': form})
作者权限校验
def article_update(request, slug):
article = get_object_or_404(Article, slug=slug)
# 只有作者或管理员能改
if request.user != article.author and not request.user.is_staff:
raise PermissionDenied
if request.method == 'POST':
form = ArticleForm(request.POST, instance=article)
if form.is_valid():
form.save()
return redirect('articles:detail', slug=article.slug)
else:
form = ArticleForm(instance=article)
return render(request, 'articles/form.html', {'form': form})
模板用户菜单
{% if user.is_authenticated %}
<a href="{% url 'users:profile' %}">{{ user.username }}</a>
<a href="{% url 'users:logout' %}">退出</a>
{% else %}
<a href="{% url 'users:login' %}">登录</a>
<a href="{% url 'users:register' %}">注册</a>
{% endif %}
权限校验三件套:@login_required(登录)、request.user.is_staff(管理员)、request.user == obj.author(本人)。
表单与收藏功能
博客项目的表单与收藏功能:发布/编辑文章表单、收藏/取消收藏。
文章表单(ModelForm)
# articles/forms.py
from django import forms
from .models import Article
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ['title', 'slug', 'summary', 'content', 'category', 'cover']
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
'summary': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 12}),
}
def clean_title(self):
title = self.cleaned_data['title']
if len(title) < 5:
raise forms.ValidationError('标题至少 5 个字')
return title
收藏模型
class Favorite(models.Model):
user = models.ForeignKey(User, related_name='favorites')
article = models.ForeignKey(Article, related_name='favorite_set')
class Meta:
unique_together = ('user', 'article') # 防重复收藏
ordering = ['-id']
收藏切换视图
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required
@login_required
def toggle_favorite(request, slug):
article = get_object_or_404(Article, slug=slug)
fav, created = Favorite.objects.get_or_create(
user=request.user, article=article)
if not created:
fav.delete() # 已收藏则取消
return JsonResponse({
'favorited': created,
'count': article.favorite_set.count(),
})
模板收藏按钮
<button id="favBtn" data-url="{% url 'articles:favorite' article.slug %}">
{% if is_favorited %}取消收藏{% else %}收藏{% endif %}
</button>
<script>
document.getElementById('favBtn').addEventListener('click', async function() {
const res = await fetch(this.dataset.url, {method: 'POST'});
const data = await res.json();
this.textContent = data.favorited ? '取消收藏' : '收藏';
});
</script>
文章列表展示收藏数
from django.db.models import Count
articles = Article.objects.annotate(fav_count=Count('favorite_set'))
我的收藏页
@login_required
def my_favorites(request):
articles = Article.objects.filter(
favorite_set__user=request.user
).select_related('category')
return render(request, 'articles/favorites.html', {'articles': articles})
收藏切换用 get_or_create 一行搞定"存在则删、不存在则建";unique_together 保证数据层不重复。
类视图(CBV)
深入类视图(CBV):通用视图的覆盖点、混入(Mixin)、以及复杂场景实现。
ListViews 常用覆盖点
class ArticleList(ListView):
model = Article
template_name = 'articles/list.html'
context_object_name = 'articles'
paginate_by = 10
# 1. 覆盖查询集
def get_queryset(self):
return Article.objects.filter(status='published')
# 2. 覆盖上下文(传额外变量)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['categories'] = Category.objects.all()
context['hot_articles'] = Article.objects.order_by('-views')[:5]
return context
# 3. 控制分页数量
def get_paginate_by(self, queryset):
return self.request.GET.get('size', 10)
CreateView 完整流程
from django.urls import reverse_lazy
class ArticleCreate(LoginRequiredMixin, CreateView):
model = Article
form_class = ArticleForm
template_name = 'articles/form.html'
success_url = reverse_lazy('articles:list')
def form_valid(self, form):
form.instance.author = self.request.user # 自动填作者
return super().form_valid(form)
UpdateView 权限控制
class ArticleUpdate(LoginRequiredMixin, UpdateView):
model = Article
form_class = ArticleForm
template_name = 'articles/form.html'
def dispatch(self, request, *args, **kwargs):
# 非作者/管理员禁止修改
if request.user != self.get_object().author and not request.user.is_staff:
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)
DeleteView 确认删除
class ArticleDelete(LoginRequiredMixin, DeleteView):
model = Article
success_url = reverse_lazy('articles:list')
template_name = 'articles/confirm_delete.html'
<!-- confirm_delete.html -->
<form method="post">
{% csrf_token %}
<p>确定删除 "{{ object.title }}" 吗?</p>
<button type="submit">确认删除</button>
<a href="{% url 'articles:detail' object.slug %}">取消</a>
</form>
混入(Mixin)复用
class AuthorRequiredMixin:
# 只有作者或管理员可操作
def dispatch(self, request, *args, **kwargs):
if request.user != self.get_object().author and not request.user.is_staff:
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)
class ArticleUpdate(LoginRequiredMixin, AuthorRequiredMixin, UpdateView):
model = Article
form_class = ArticleForm
template_name = 'articles/form.html'
Mixin 顺序有讲究:LoginRequiredMixin 要放最左(最先执行),确保认证先于权限判断。
打包上线
博客项目打包上线:从开发到生产的完整流程。
上线检查清单
- 关闭 DEBUG:
DEBUG = False - 配置 ALLOWED_HOSTS
- 收集静态文件:
collectstatic - 执行数据库迁移:
migrate - 配置 HTTPS + 安全头
- 设置日志
生产 settings 分离
myblog/
├── settings/
│ ├── __init__.py # from .prod import *
│ ├── base.py # 公共配置
│ ├── dev.py # 开发配置
│ └── prod.py # 生产配置
# prod.py(节选)
from .base import *
DEBUG = False
ALLOWED_HOSTS = ['www.laowang.click']
import os
SECRET_KEY = os.environ['DJANGO_SECRET_KEY'] # 从环境变量读取
STATIC_ROOT = '/www/wwwroot/myblog/staticfiles'
MEDIA_ROOT = '/www/wwwroot/myblog/media'
# 安全
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
# 日志
LOGGING = {
'version': 1,
'handlers': {
'file': {
'class': 'logging.FileHandler',
'filename': '/www/wwwroot/myblog/logs/django.log',
},
},
'root': {'handlers': ['file'], 'level': 'INFO'},
}
部署步骤(宝塔)
# 1. 上传代码到服务器
# 2. 创建 venv 并安装依赖
cd /www/wwwroot/myblog
python3 -m venv venv
venv/bin/pip install -r requirements.txt
# 3. 迁移与收集静态
venv/bin/python manage.py migrate
venv/bin/python manage.py collectstatic --noinput
# 4. 启动 gunicorn(通过面板 Python 项目管理器)
gunicorn myblog.wsgi:application -w 4 -b 127.0.0.1:8001
Nginx 配置
server {
listen 443 ssl http2;
server_name www.laowang.click;
ssl_certificate /www/server/panel/vhost/cert/laowang/fullchain.pem;
ssl_certificate_key /www/server/panel/vhost/cert/laowang/privkey.pem;
location /static/ {
alias /www/wwwroot/myblog/staticfiles/;
expires 7d;
}
location /media/ {
alias /www/wwwroot/myblog/media/;
}
location / {
proxy_pass http://127.0.0.1:8001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
上线后验证
curl -I https://www.laowang.click/ # 200
python manage.py check --deploy # 部署安全检查
上线后执行 python manage.py check --deploy 做安全自查,重点确认 DEBUG=False、CSRF/HTTPS 配置。