Appearance
章节3:视图进阶与系统功能
一、章节概述
本章在视图函数的基础上深入进阶,学习 Django 的类视图(CBV)、会话管理(Cookie/Session)、中间件机制、信号系统、缓存策略以及异步任务队列等高级功能。这些知识将帮助学生构建更健壮、可维护且高性能的 Django 应用。
二、学习目标
| 目标分类 | 具体目标 |
|---|---|
| 知识目标 | 理解 FBV 与 CBV 的区别及各自适用场景;掌握 Cookie 与 Session 的工作原理;理解中间件信号机制及缓存策略 |
| 技能目标 | 能熟练使用 ListView/DetailView 等通用类视图;能自定义中间件和信号处理器;能配置 Redis 缓存并集成 Celery 异步任务 |
| 素养目标 | 培养面向对象的设计思维;理解企业级 Web 应用的系统架构考量 |
三、核心知识点
3.1 FBV 与 CBV 类视图
定义
FBV(Function-Based View) 是以函数形式定义的视图,简单直观,适合处理简单逻辑。CBV(Class-Based View) 是以类形式定义的视图,通过继承 Django 内置的通用视图类,能够以更少的代码实现常见模式(列表展示、详情展示、表单处理等)。Django 提供了丰富的通用类视图:View、ListView、DetailView、CreateView、UpdateView、DeleteView、FormView 等。
语法
▶ 基础类视图 View
python
from django.views import View
from django.http import HttpResponse, JsonResponse
from django.shortcuts import render, get_object_or_404
# FBV 方式
def article_list_fbv(request):
if request.method == 'GET':
articles = Article.objects.all()
return render(request, 'articles.html', {'articles': articles})
elif request.method == 'POST':
# 处理创建
pass
# CBV 方式(等效)
class ArticleListView(View):
"""类视图通过不同方法处理不同 HTTP 方法"""
def get(self, request):
"""处理 GET 请求"""
articles = Article.objects.all()
return render(request, 'articles.html', {'articles': articles})
def post(self, request):
"""处理 POST 请求"""
# 创建文章
title = request.POST.get('title')
content = request.POST.get('content')
article = Article.objects.create(title=title, content=content)
return JsonResponse({'id': article.id, 'title': article.title})
def dispatch(self, request, *args, **kwargs):
"""请求分发钩子(可选重写)"""
# 在调用 get/post 之前执行的通用逻辑
if not request.user.is_authenticated:
return HttpResponse('请先登录', status=401)
return super().dispatch(request, *args, **kwargs)
# URL 配置
# path('articles/', ArticleListView.as_view(), name='article_list')▶ ListView —— 列表视图
python
from django.views.generic import ListView
from .models import Article
class ArticleListView(ListView):
"""通用列表视图:自动查询并渲染列表模板"""
# 必须指定:查询的模型
model = Article
# 可选:模板名称(默认:<app>/<model>_list.html → blog/article_list.html)
template_name = 'blog/article_list.html'
# 可选:上下文变量名(默认:object_list 或 article_list)
context_object_name = 'articles'
# 可选:过滤条件
queryset = Article.objects.filter(is_published=True)
# 可选:每页数量(启用分页)
paginate_by = 10
# 可选:默认排序
ordering = ['-pub_date']
# 可选:添加额外上下文数据
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['categories'] = Category.objects.all()
context['page_title'] = '文章列表'
return context
# 可选:自定义查询集
def get_queryset(self):
queryset = super().get_queryset()
keyword = self.request.GET.get('q')
if keyword:
queryset = queryset.filter(title__icontains=keyword)
return queryset▶ DetailView —— 详情视图
python
from django.views.generic import DetailView
from django.db.models import F
class ArticleDetailView(DetailView):
"""通用详情视图:自动查询单条记录并渲染详情模板"""
model = Article
template_name = 'blog/article_detail.html'
context_object_name = 'article'
# URL 中的 PK 参数名(默认:pk)
# pk_url_kwarg = 'pk'
def get_object(self, queryset=None):
"""获取对象(可在此处增加自定义逻辑)"""
article = super().get_object(queryset)
# 阅读量 +1(使用 F 表达式避免并发问题)
Article.objects.filter(pk=article.pk).update(views=F('views') + 1)
article.refresh_from_db() # 刷新以获取最新 views
return article
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# 上一篇、下一篇
current = self.object
context['prev_article'] = Article.objects.filter(
pub_date__lt=current.pub_date
).first()
context['next_article'] = Article.objects.filter(
pub_date__gt=current.pub_date
).last()
return context▶ CreateView / UpdateView / DeleteView
python
from django.views.generic import CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from .forms import ArticleForm # 需配合 Django 表单使用
class ArticleCreateView(CreateView):
"""创建视图:处理表单展示与提交"""
model = Article
form_class = ArticleForm # 关联的表单类
template_name = 'blog/article_form.html'
success_url = reverse_lazy('blog:article_list') # 成功后跳转
def form_valid(self, form):
"""表单验证通过后的处理"""
form.instance.author = self.request.user # 设置作者为当前用户
return super().form_valid(form)
class ArticleUpdateView(UpdateView):
"""更新视图:与 CreateView 类似,但预填充已有数据"""
model = Article
form_class = ArticleForm
template_name = 'blog/article_form.html'
def get_success_url(self):
return reverse_lazy('blog:article_detail', kwargs={'pk': self.object.pk})
class ArticleDeleteView(DeleteView):
"""删除视图:需要 POST 确认删除"""
model = Article
template_name = 'blog/article_confirm_delete.html'
success_url = reverse_lazy('blog:article_list')▶ FormView 与表单处理
python
from django.views.generic import FormView
from django.contrib import messages
from .forms import ContactForm
class ContactFormView(FormView):
"""表单视图:展示表单 + 处理提交"""
template_name = 'contact.html'
form_class = ContactForm
success_url = '/contact/success/'
def form_valid(self, form):
"""表单验证通过"""
form.send_email() # 提交表单后发送邮件
messages.success(self.request, '消息已发送!')
return super().form_valid(form)
def form_invalid(self, form):
"""表单验证失败"""
messages.error(self.request, '请检查表单错误')
return super().form_invalid(form)▶ CBV 路由配置
python
# urls.py
from django.urls import path
from .views import (
ArticleListView, ArticleDetailView,
ArticleCreateView, ArticleUpdateView, ArticleDeleteView
)
app_name = 'blog'
urlpatterns = [
path('', ArticleListView.as_view(), name='article_list'),
path('<int:pk>/', ArticleDetailView.as_view(), name='article_detail'),
path('create/', ArticleCreateView.as_view(), name='article_create'),
path('<int:pk>/update/', ArticleUpdateView.as_view(), name='article_update'),
path('<int:pk>/delete/', ArticleDeleteView.as_view(), name='article_delete'),
]FBV vs CBV 对比
| 对比维度 | FBV | CBV |
|---|---|---|
| 代码量 | 简单场景少,复杂场景多 | 继承通用视图,代码精简 |
| 复用性 | 通过装饰器复用 | 通过继承 Mixin 复用 |
| HTTP 方法处理 | 需手动判断 request.method | 自动分发到不同方法 |
| 学习曲线 | 简单直观 | 需要理解继承体系 |
| 灵活性 | 完全控制 | 受框架约定限制 |
| 适用场景 | 简单或高度定制化的视图 | 标准 CRUD 和列表展示 |
3.2 Cookie 与 Session 会话管理
定义
Cookie 是存储在客户端浏览器中的小数据块(通常 ≤4KB),由服务器通过 HTTP 响应头 Set-Cookie 设置,浏览器在后续请求中自动携带。Session 是存储在服务器端的用户会话数据,客户端仅保存 Session ID(通常存储在 Cookie 中),通过 Session ID 关联服务器端的数据。
语法
▶ Cookie 操作
python
from django.http import HttpResponse
def set_cookie(request):
"""设置 Cookie"""
response = HttpResponse('Cookie 已设置')
# 基本设置
response.set_cookie('username', 'alice')
# 带过期时间的 Cookie(单位:秒)
response.set_cookie('token', 'abcdef123', max_age=3600) # 1 小时后过期
# 带过期日期的 Cookie
expires = datetime.datetime(2025, 12, 31, 23, 59, 59)
response.set_cookie('preference', 'dark_mode', expires=expires)
# 安全选项
response.set_cookie(
'session_id', 'xxx',
max_age=86400, # 24 小时
httponly=True, # 禁止 JavaScript 访问(防 XSS)
secure=True, # 仅 HTTPS 传输
samesite='Lax', # CSRF 防护
path='/', # 路径范围
domain='.example.com', # 域名范围
)
return response
def get_cookie(request):
"""读取 Cookie"""
username = request.COOKIES.get('username', '匿名用户')
preference = request.COOKIES.get('preference', 'light_mode')
return HttpResponse(f'用户: {username}, 偏好: {preference}')
def delete_cookie(request):
"""删除 Cookie"""
response = HttpResponse('Cookie 已删除')
response.delete_cookie('username')
response.delete_cookie('token')
return response▶ Session 配置与操作
python
# settings.py —— Session 配置
# Django 默认使用数据库存储 Session(需执行 migrate)
INSTALLED_APPS = [
'django.contrib.sessions', # 默认已包含
]
# Session 引擎配置
SESSION_ENGINE = 'django.contrib.sessions.backends.db' # 数据库存储(默认)
# SESSION_ENGINE = 'django.contrib.sessions.backends.cache' # 缓存存储
# SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' # 缓存+数据库双写
# SESSION_ENGINE = 'django.contrib.sessions.backends.file' # 文件存储
SESSION_COOKIE_NAME = 'sessionid' # Cookie 名称(默认)
SESSION_COOKIE_AGE = 1209600 # Session 过期时间(秒,默认 2 周)
SESSION_EXPIRE_AT_BROWSER_CLOSE = False # 关闭浏览器是否过期
SESSION_SAVE_EVERY_REQUEST = False # 每次请求都保存 Sessionpython
# views.py —— Session 操作
def session_set(request):
"""存储 Session 数据"""
# 会话数据可以像字典一样操作
request.session['user_id'] = 1
request.session['username'] = 'alice'
request.session['roles'] = ['admin', 'editor']
request.session['preferences'] = {
'theme': 'dark',
'language': 'zh-CN',
}
# 设置过期时间
request.session.set_expiry(3600) # 1 小时后过期
# request.session.set_expiry(0) # 关闭浏览器时过期
# request.session.set_expiry(None) # 使用全局设置
return HttpResponse('Session 数据已保存')
def session_get(request):
"""读取 Session 数据"""
user_id = request.session.get('user_id')
username = request.session.get('username', '未登录')
# 安全获取并设置默认值
roles = request.session.get('roles', [])
# 检查键是否存在
if 'preferences' in request.session:
theme = request.session['preferences'].get('theme')
else:
theme = 'default'
return HttpResponse(f'用户: {username}, 角色: {roles}, 主题: {theme}')
def session_delete(request):
"""删除 Session 数据"""
# 删除单个键
if 'preferences' in request.session:
del request.session['preferences']
# 清空所有数据(保留 Session)
request.session.flush() # 删除 sesssion 数据并创建新 key
return HttpResponse('Session 已清空')
def session_test(request):
"""Session 测试与调试"""
# 测试 session 是否可用
request.session['test_key'] = 'test_value'
assert request.session['test_key'] == 'test_value'
# 获取 session 中的所有键
keys = list(request.session.keys())
# 获取 session key
session_key = request.session.session_key
# 循环遍历所有会话数据
for key, value in request.session.items():
print(f"{key}: {value}")
return HttpResponse(f'Session Key: {session_key}, Keys: {keys}')▶ 基于 Session 的用户认证
python
from django.contrib.auth import authenticate, login, logout
from django.shortcuts import render, redirect
from django.contrib import messages
def user_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:
# 登录(创建 Session)
login(request, user)
# 可选:在 Session 中存储额外信息
request.session['login_ip'] = request.META.get('REMOTE_ADDR')
request.session['login_time'] = str(timezone.now())
messages.success(request, '登录成功!')
return redirect('blog:article_list')
else:
messages.error(request, '用户名或密码错误')
return render(request, 'blog/login.html')
def user_logout(request):
"""用户登出"""
logout(request) # 清除 session
messages.info(request, '已退出登录')
return redirect('blog:index')3.3 中间件原理与自定义中间件
定义
中间件(Middleware) 是 Django 请求/响应处理过程中的钩子框架,是一个用于全局处理请求和响应的轻量级插件系统。每个中间件负责特定的功能(如认证、CSRF 保护、Session 管理等),请求在经过 URL 路由之前和视图处理之后都会经过中间件。
语法
▶ Django 内置中间件
python
# settings.py —— 默认中间件(按顺序执行)
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', # 安全防护
'django.contrib.sessions.middleware.SessionMiddleware', # Session 支持
'django.middleware.common.CommonMiddleware', # 通用功能(URL 规范化等)
'django.middleware.csrf.CsrfViewMiddleware', # CSRF 保护
'django.contrib.auth.middleware.AuthenticationMiddleware', # 用户认证
'django.contrib.messages.middleware.MessageMiddleware', # 消息提示
'django.middleware.clickjacking.XFrameOptionsMiddleware', # 点击劫持防护
]▶ 中间件执行顺序
请求进入
│
▼
SecurityMiddleware.process_request(request)
SessionMiddleware.process_request(request)
CommonMiddleware.process_request(request)
CsrfViewMiddleware.process_request(request)
AuthenticationMiddleware.process_request(request)
MessageMiddleware.process_request(request)
XFrameOptionsMiddleware.process_request(request)
│
▼
URL 路由 → 视图函数
│
▼
XFrameOptionsMiddleware.process_response(request, response)
MessageMiddleware.process_response(request, response)
AuthenticationMiddleware.process_response(request, response)
CsrfViewMiddleware.process_response(request, response)
CommonMiddleware.process_response(request, response)
SessionMiddleware.process_response(request, response)
SecurityMiddleware.process_response(request, response)
│
▼
响应返回▶ 自定义中间件
python
# blog/middleware.py —— 自定义中间件
import time
import logging
from django.http import HttpResponse
from django.utils.deprecation import MiddlewareMixin
logger = logging.getLogger(__name__)
# 方式一:使用 MiddlewareMixin(兼容新旧两种写法)
class RequestTimeMiddleware(MiddlewareMixin):
"""记录每个请求的处理时间"""
def process_request(self, request):
"""请求处理前执行"""
request.start_time = time.time()
def process_view(self, request, view_func, view_args, view_kwargs):
"""视图函数调用前执行"""
# 可以在此处检查权限、添加额外数据等
pass
def process_response(self, request, response):
"""响应返回前执行"""
if hasattr(request, 'start_time'):
duration = time.time() - request.start_time
logger.info(f'请求 {request.path} 耗时: {duration:.3f}秒')
response['X-Request-Time'] = f'{duration:.3f}s'
return response
def process_exception(self, request, exception):
"""视图函数抛出异常时执行"""
logger.error(f'请求 {request.path} 发生异常: {exception}')
return None # 返回 None 则继续向上抛异常
# 方式二:函数式中间件(Django 1.10+ 推荐)
class BlockSuspiciousIPMiddleware:
"""拦截可疑 IP 的中间件"""
def __init__(self, get_response):
self.get_response = get_response
# 初始化时执行一次(服务器启动时)
self.blocked_ips = ['192.168.1.100', '10.0.0.50']
def __call__(self, request):
# 请求处理前的代码
ip = request.META.get('REMOTE_ADDR')
if ip in self.blocked_ips:
return HttpResponse('您的 IP 已被禁止访问', status=403)
if request.path.startswith('/admin/') and not request.path.endswith('/login/'):
if not request.user.is_authenticated:
logger.warning(f'未认证用户尝试访问管理后台: {ip}')
# 调用下一个中间件或视图
response = self.get_response(request)
# 响应返回后的代码
response['X-Request-IP'] = ip
return response
class MaintenanceModeMiddleware:
"""维护模式中间件"""
def __init__(self, get_response):
self.get_response = get_response
self.maintenance_mode = False # 可通过数据库或环境变量控制
def __call__(self, request):
if self.maintenance_mode and not request.path.startswith('/admin/'):
return HttpResponse(
'<h1>网站维护中</h1><p>请稍后再访问</p>',
status=503
)
return self.get_response(request)
class CorsMiddleware:
"""跨域资源共享(CORS)中间件"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
# 添加 CORS 头
response['Access-Control-Allow-Origin'] = '*'
response['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
response['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
# 处理预检请求
if request.method == 'OPTIONS':
return response
return response▶ 注册自定义中间件
python
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
# 自定义中间件(按顺序添加)
'blog.middleware.RequestTimeMiddleware',
'blog.middleware.BlockSuspiciousIPMiddleware',
]3.4 信号机制与缓存系统
定义
信号(Signals) 是 Django 内置的发布-订阅模式实现,允许在特定事件发生时通知其他部分的代码。常见内置信号包括数据模型保存前后、删除前后、用户登录/登出等。缓存系统 用于临时存储计算结果或数据库查询结果,减少重复计算和数据库查询,显著提升应用性能。
语法
▶ 常用内置信号
| 信号 | 触发时机 | 参数 |
|---|---|---|
pre_save | 模型 save() 调用前 | sender, instance, raw, using, update_fields |
post_save | 模型 save() 调用后 | sender, instance, created, raw, using, update_fields |
pre_delete | 模型 delete() 调用前 | sender, instance, using |
post_delete | 模型 delete() 调用后 | sender, instance, using |
m2m_changed | ManyToMany 字段变化时 | sender, instance, action, reverse, model, pk_set |
user_logged_in | 用户登录成功 | sender, request, user |
user_logged_out | 用户登出 | sender, request, user |
user_login_failed | 用户登录失败 | sender, credentials, request |
request_started | HTTP 请求开始时 | sender, environ |
request_finished | HTTP 请求结束时 | sender |
▶ 信号接收器定义
python
# blog/signals.py —— 信号处理器
import logging
from django.db.models.signals import post_save, post_delete, pre_save
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.dispatch import receiver
from django.core.cache import cache
from .models import Article
logger = logging.getLogger(__name__)
# 方式一:使用 @receiver 装饰器(推荐)
@receiver(post_save, sender=Article)
def article_saved_handler(sender, instance, created, **kwargs):
"""文章保存后的处理"""
if created:
logger.info(f'新文章创建: {instance.title} (ID: {instance.pk})')
else:
logger.info(f'文章更新: {instance.title}')
# 清除文章列表缓存
cache.delete('article_list_page_1')
cache.delete_pattern('article_list_*') # 清除所有文章列表缓存
@receiver(post_delete, sender=Article)
def article_deleted_handler(sender, instance, **kwargs):
"""文章删除后的处理"""
logger.info(f'文章删除: {instance.title} (ID: {instance.pk})')
cache.delete('article_list_page_1')
# 方式二:定义接收器函数,然后在 apps.py 中连接
def clear_category_cache(sender, instance, **kwargs):
"""清除分类缓存"""
cache.delete('categories_all')
def update_article_count(sender, instance, created, **kwargs):
"""更新文章计数"""
if created:
from django.db.models import F
# 更新分类的文章总数
Category.objects.filter(pk=instance.category_id).update(
article_count=F('article_count') + 1
)
# 处理用户认证信号
@receiver(user_logged_in)
def user_logged_in_handler(sender, request, user, **kwargs):
"""用户登录成功"""
logger.info(f'用户登录: {user.username}')
request.session['last_login'] = str(timezone.now())
@receiver(user_logged_out)
def user_logged_out_handler(sender, request, user, **kwargs):
"""用户登出"""
logger.info(f'用户登出: {user.username}')▶ 信号注册(apps.py)
python
# blog/apps.py
from django.apps import AppConfig
class BlogConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'blog'
verbose_name = '博客管理'
def ready(self):
"""应用就绪时注册信号"""
import blog.signals # 导入信号处理器▶ 自定义信号
python
# blog/signals.py —— 自定义信号
import django.dispatch
# 定义自定义信号
article_viewed = django.dispatch.Signal()
comment_created = django.dispatch.Signal()
# 发送信号
def article_detail(request, pk):
article = get_object_or_404(Article, pk=pk)
# 发送自定义信号
article_viewed.send(
sender=Article,
instance=article,
request=request,
ip=request.META.get('REMOTE_ADDR'),
)
return render(request, 'blog/detail.html', {'article': article})
# 接收自定义信号
@receiver(article_viewed, sender=Article)
def article_viewed_handler(sender, instance, request, ip, **kwargs):
"""文章被浏览时增加阅读计数"""
logger.info(f'文章被浏览: {instance.title}, IP: {ip}')
Article.objects.filter(pk=instance.pk).update(views=F('views') + 1)▶ 缓存系统配置
python
# settings.py —— 缓存配置
# 方式一:本地内存缓存(开发/测试用)
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'unique-snowflake',
}
}
# 方式二:文件系统缓存
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
'LOCATION': BASE_DIR / 'cache',
}
}
# 方式三:数据库缓存
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
'LOCATION': 'django_cache_table',
}
}
# 方式四:Redis 缓存(生产环境推荐)
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache', # Django 4.0+
'LOCATION': 'redis://127.0.0.1:6379/1', # 使用 DB 1
'OPTIONS': {
'password': 'your_password', # 如果有密码
'timeout': 5, # 连接超时
},
'KEY_PREFIX': 'blog', # 键前缀
'TIMEOUT': 300, # 默认过期时间(秒)
}
}
# 方式五:使用 django-redis(功能更丰富)
# pip install django-redis
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'PASSWORD': 'your_password',
'CONNECTION_POOL_CLASS': 'redis.BlockingConnectionPool',
'CONNECTION_POOL_CLASS_KWARGS': {
'max_connections': 50,
'timeout': 20,
},
'MAX_ENTRIES': 1000, # 最大缓存条目数
},
'KEY_PREFIX': 'blog',
}
}▶ 缓存操作
python
from django.core.cache import cache
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_cookie
# ---- 基本缓存操作 ----
# 设置缓存
cache.set('article_count', 100, timeout=300) # 5 分钟过期
cache.set_many({
'categories': ['Python', 'Django', 'JavaScript'],
'tags': ['tutorial', 'guide'],
}, timeout=600)
# 获取缓存
count = cache.get('article_count', 0) # 不存在返回默认值 0
data = cache.get_many(['categories', 'tags'])
# 检查键是否存在
if cache.has_key('article_count'):
print('缓存存在')
# 递增/递减
cache.incr('article_count') # +1
cache.incr('article_count', 10) # +10
cache.decr('article_count') # -1
# 删除缓存
cache.delete('article_count')
cache.delete_many(['categories', 'tags'])
cache.clear() # 清空所有缓存(慎用)
# 模式删除(需 django-redis)
# cache.delete_pattern('article_list_*')
# ---- 缓存装饰器 ----
@cache_page(60 * 15) # 缓存整个视图 15 分钟
def article_list(request):
"""文章列表(整个视图响应被缓存)"""
articles = Article.objects.filter(is_published=True)
return render(request, 'blog/article_list.html', {'articles': articles})
@vary_on_cookie # 按 Cookie 区分缓存
@cache_page(60 * 15)
def article_detail(request, pk):
"""文章详情(不同用户的缓存不同)"""
article = get_object_or_404(Article, pk=pk)
return render(request, 'blog/article_detail.html', {'article': article})
# ---- 模板片段缓存 ----
"""
{% load cache %}
{% cache 300 sidebar %}
{# 缓存侧栏 5 分钟 #}
{% for category in categories %}
<li>{{ category.name }}</li>
{% endfor %}
{% endcache %}
{% cache 600 "article_detail" article.pk %}
{# 使用动态键缓存 #}
<h1>{{ article.title }}</h1>
<p>{{ article.content }}</p>
{% endcache %}
"""
# ---- 低级别缓存使用模式 ----
def get_article_with_cache(pk):
"""带缓存的文章查询"""
cache_key = f'article_detail_{pk}'
article = cache.get(cache_key)
if article is None:
# 缓存未命中,从数据库查询
article = get_object_or_404(Article, pk=pk)
cache.set(cache_key, article, timeout=300)
return article
def get_popular_articles(limit=10):
"""获取热门文章(缓存 1 小时)"""
cache_key = f'popular_articles_{limit}'
articles = cache.get(cache_key)
if not articles:
articles = list(
Article.objects.filter(is_published=True)
.order_by('-views')[:limit]
)
cache.set(cache_key, articles, timeout=3600)
return articles3.5 Celery 异步任务队列
定义
Celery 是一个分布式任务队列,用于在 Django 应用外异步执行耗时操作(如发送邮件、图片处理、生成报表、调用第三方 API 等)。它使用消息代理(Broker,如 Redis/RabbitMQ)来协调任务分发,Worker 进程负责执行任务。
架构
用户请求
│
▼
Django 视图 ──► 调用 task.delay() ──► 任务加入 Broker (Redis/RabbitMQ)
│
▼
Celery Worker
│
┌───────┼───────┐
│ │ │
▼ ▼ ▼
发送邮件 处理图片 生成报表安装与配置
bash
# 安装 Celery 和 Redis 作为 Broker
pip install celery[redis]python
# myproject/celery.py —— Celery 实例配置
import os
from celery import Celery
# 设置 Django 默认配置模块
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
app = Celery('myproject')
# 从 Django settings 读取配置(所有 CELERY_ 开头的配置项)
app.config_from_object('django.conf:settings', namespace='CELERY')
# 自动发现所有 app 中的 tasks.py
app.autodiscover_tasks()
@app.task(bind=True, ignore_result=True)
def debug_task(self):
"""调试任务"""
print(f'Request: {self.request!r}')python
# myproject/__init__.py —— 确保 Celery 随 Django 启动
from .celery import app as celery_app
__all__ = ('celery_app',)python
# settings.py —— Celery 配置
# Redis 作为消息代理
CELERY_BROKER_URL = 'redis://127.0.0.1:6379/0'
# 结果后端(存储任务执行结果)
CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/0'
# 时区设置
CELERY_TIMEZONE = 'Asia/Shanghai'
CELERY_ENABLE_UTC = False
# 任务序列化
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_ACCEPT_CONTENT = ['json']
# 任务结果过期时间(秒)
CELERY_RESULT_EXPIRES = 3600 * 24 # 1 天
# Worker 并发数
CELERY_WORKER_CONCURRENCY = 4
# 任务重试
CELERY_TASK_ACKS_LATE = True # 任务完成后再确认
CELERY_TASK_REJECT_ON_WORKER_LOST = True
CELERY_TASK_RETRY_LIMIT = 3 # 最大重试次数任务定义
python
# blog/tasks.py —— 异步任务定义
from celery import shared_task
from django.core.mail import send_mail
from django.conf import settings
from django.utils import timezone
import logging
logger = logging.getLogger(__name__)
@shared_task
def send_async_email(subject, message, recipient_list):
"""异步发送邮件"""
try:
send_mail(
subject=subject,
message=message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=recipient_list,
fail_silently=False,
)
logger.info(f'邮件发送成功: {recipient_list}')
return {'status': 'success', 'recipients': recipient_list}
except Exception as e:
logger.error(f'邮件发送失败: {e}')
raise # 触发 Celery 重试
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def process_image(self, image_path, operations):
"""处理图片(带重试机制)"""
try:
from PIL import Image
img = Image.open(image_path)
for op in operations:
if op['type'] == 'resize':
img = img.resize((op['width'], op['height']))
elif op['type'] == 'thumbnail':
img.thumbnail((op['width'], op['height']))
img.save(image_path)
logger.info(f'图片处理完成: {image_path}')
return {'status': 'success', 'path': image_path}
except Exception as exc:
logger.error(f'图片处理失败: {exc}')
# 重试任务
raise self.retry(exc=exc)
@shared_task
def generate_weekly_report():
"""生成周报(定时任务)"""
from datetime import timedelta
from .models import Article
week_ago = timezone.now() - timedelta(days=7)
stats = {
'new_articles': Article.objects.filter(created_at__gte=week_ago).count(),
'total_views': Article.objects.filter(created_at__gte=week_ago).aggregate(
total=Sum('views')
)['total'] or 0,
'top_articles': list(
Article.objects.filter(is_published=True)
.order_by('-views')[:10]
.values('title', 'views')
),
}
# 将报表保存到文件或发送邮件
logger.info(f'周报生成完成: {stats}')
return stats
@shared_task
def cleanup_expired_sessions():
"""清理过期 Session"""
from django.contrib.sessions.models import Session
deleted_count = Session.objects.filter(expire_date__lt=timezone.now()).count()
Session.objects.filter(expire_date__lt=timezone.now()).delete()
logger.info(f'清理过期 Session: {deleted_count} 条')
return {'deleted': deleted_count}任务调用
python
# blog/views.py —— 在视图中调用异步任务
from .tasks import send_async_email, process_image
from django.http import JsonResponse
def register_user(request):
"""用户注册"""
if request.method == 'POST':
# ... 处理用户注册逻辑
# 异步发送欢迎邮件(不阻塞响应)
send_async_email.delay(
subject='欢迎注册!',
message=f'亲爱的 {username},欢迎加入!',
recipient_list=[email],
)
return JsonResponse({'status': 'success', 'message': '注册成功!'})
def upload_avatar(request):
"""上传头像"""
if request.method == 'POST' and request.FILES.get('avatar'):
avatar = request.FILES['avatar']
# 保存文件
with open(f'media/avatars/{avatar.name}', 'wb+') as f:
for chunk in avatar.chunks():
f.write(chunk)
# 异步处理:生成缩略图
process_image.delay(
f'media/avatars/{avatar.name}',
[{'type': 'thumbnail', 'width': 200, 'height': 200}],
)
return JsonResponse({'status': 'success'})定时任务(Celery Beat)
python
# settings.py —— 定时任务配置
from celery.schedules import crontab
CELERY_BEAT_SCHEDULE = {
# 每周一生成周报
'generate-weekly-report': {
'task': 'blog.tasks.generate_weekly_report',
'schedule': crontab(hour=9, minute=0, day_of_week=1), # 周一早上 9:00
'args': (),
},
# 每天凌晨清理过期 Session
'cleanup-sessions-daily': {
'task': 'blog.tasks.cleanup_expired_sessions',
'schedule': crontab(hour=3, minute=0), # 每天凌晨 3:00
},
# 每隔 30 分钟执行一次
'check-health': {
'task': 'blog.tasks.check_system_health',
'schedule': 1800.0, # 秒为单位
},
}启动 Celery
bash
# 启动 Worker(处理异步任务)
celery -A myproject worker -l info
# 启动 Worker(生产模式,守护进程)
celery -A myproject worker -l info --concurrency=4 --max-tasks-per-child=1000
# 启动 Beat(定时任务调度器)
celery -A myproject beat -l info
# 同时启动 Worker 和 Beat
celery -A myproject worker -B -l info
# 查看任务状态
celery -A myproject status
# 查看正在运行的 Worker
celery -A myproject inspect active
# 清除所有任务
celery -A myproject purge任务状态与结果
python
# 在视图中查询任务状态
from celery.result import AsyncResult
from blog.tasks import send_async_email
def check_task_status(request, task_id):
"""查询异步任务状态"""
result = AsyncResult(task_id)
status_info = {
'PENDING': '等待执行',
'STARTED': '正在执行',
'SUCCESS': '执行成功',
'FAILURE': '执行失败',
'RETRY': '重试中',
'REVOKED': '已撤销',
}
response_data = {
'task_id': task_id,
'status': result.status,
'status_display': status_info.get(result.status, '未知'),
}
if result.successful():
response_data['result'] = result.result # 任务返回值
elif result.failed():
response_data['error'] = str(result.result) # 异常信息
return JsonResponse(response_data)四、小结
| 知识点 | 核心要点 |
|---|---|
| FBV vs CBV | FBV 函数式直观;CBV 通过继承复用代码;ListView/DetailView/CreateView 等通用视图大幅减少样板代码 |
| Cookie 与 Session | Cookie 存客户端(≤4KB),Session 存服务器;Cookie 通过 set_cookie() 操作,Session 通过 request.session 字典操作 |
| 中间件 | 每个请求/响应都会经过中间件链;可自定义 process_request/process_view/process_response/process_exception |
| 信号机制 | 发布-订阅模式;post_save/post_delete 等内置信号在模型事件触发;在 apps.py 中注册信号 |
| 缓存系统 | Redis 是生产环境首选;cache.get/set 低级别缓存;@cache_page 视图级别缓存;模板片段缓存 |
| Celery 异步任务 | 处理耗时操作(邮件、图片处理);delay() 调用任务;Celery Beat 实现定时任务 |
五、练习
基础练习
- CBV 实践:使用 ListView 和 DetailView 重构章节1中的博客文章列表和详情视图
- Session 计数器:创建一个视图,使用 Session 记录用户的访问次数,每次刷新页面次数 +1
进阶练习
- 自定义中间件:编写一个
RateLimitMiddleware,限制单个 IP 每分钟最多请求 60 次,超过返回 429 状态码 - 信号应用:使用
post_save信号,当文章被创建时自动发送新文章通知邮件(使用 Celery 异步发送)
挑战练习
- 完整的异步任务系统:
- 配置 Celery + Redis
- 定义一个任务:批量生成文章摘要(调用 AI API 或本地处理)
- 在 Admin 后台添加"批量生成摘要"操作按钮
- 显示任务执行进度和结果
参考代码(练习 2 Session 计数器)
python
def visit_counter(request):
count = request.session.get('visit_count', 0)
count += 1
request.session['visit_count'] = count
return HttpResponse(f'您已访问本页面 {count} 次')