1. 项目概述:官方教程跑通之后,真正的实战才刚刚开始
如果你刚把 Django 官方教程里那个投票应用从头到尾做完,可能会有一种微妙的错觉:觉得自己已经会 Django 了。但等你打开浏览器,看着那个勉强能用的后台和几个表单页面,又会很快清醒——距离一个能真正上线的项目,中间还隔着无数个“细节地狱”。这篇博文就是记录我完成官方教程之后,继续用这个底子去做真实项目的全过程。包含了我是如何整理目录结构、怎么理解 QuerySet 的增删改查、如何给接口补上 token 认证,以及最后怎么用 WebSocket 把后端数据实时推到浏览器前端。
这篇内容适合两类人:一类是刚做完官方教程、想继续往前走的新手,另一类是写过一点 Django 但总觉得基础不扎实、想系统梳理一下查询和认证细节的同学。我会把每一步背后的理由也讲清楚,而不是只丢结论。官方教程给的是骨架,这篇文章补的是血肉。
先说一个总体判断:官方教程的投票应用虽然简单,但它搭建的路是正路——MTV 分层、ORM 映射、模板渲染、Admin 后台、表单处理全都有了。问题在于教程把这些串得太顺,导致你容易忽略很多设计上的取舍。而在真实项目里,几乎每一步都要你亲自做决策。
2. 整体设计思路:先看懂官方教程在教什么,再思考怎么改
2.1 官方教程真正教会你的四件事
不要小看这个投票应用。它把 Django 的使用链路完整走了一遍:创建项目、创建应用、编写模型、迁移数据库、注册后台、写视图、配路由、用模板渲染数据、处理表单提交。这套链路就是你未来所有 Django 项目的基本盘。官方教程还有一个容易被忽略的细节:它对 URL 命名使用了name参数,在模板中用{% url %}反向解析路径——这个习惯很多人做项目时反而会丢掉,喜欢硬编码 URL,结果改路由之后满屏报错。
另外教程里的self.question_text[:50]这类__str__魔方法写法也值得学到。后台列表页显示的不是对象内存地址,而是有意义的描述文本,全靠这个。我后来在真实项目里维护十几个模型,每次都会庆幸当初养成了给模型写__str__的习惯。
2.2 从 demo 到项目:目录结构先重组
跑完教程后我做的第一件事不是急着加功能,而是重新检查项目根目录。官方教程默认生成的结构里,polls应用和mysite项目配置放在同一层。小 demo 没问题,但真实项目一般会调整成apps目录统一管理多个子应用,再配一个common或utils目录放通用模块。
我的做法是建一个apps文件夹,把业务应用全部移进去,然后在settings.py里加一行:
import sys from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent sys.path.insert(0, str(BASE_DIR / "apps"))这样INSTALLED_APPS里写"user_app"、"blog_app"就能直接识别,不用写一长串相对路径。看起来是小改动,但对后续项目扩展很关键。你要是用 PyCharm 打开项目,记得把apps目录标记为 Sources Root,不然 IDE 会提示找不到模块。
2.3 视图层选型:函数视图还是类视图
官方教程用的是函数视图(FBV),写起来直观,适合理解请求处理流程。但真实项目里我大量使用类视图(CBV),尤其是ListView、DetailView、CreateView这一组。核心原因是 CBV 把常见的 CRUD 流程抽象成了类属性,代码量大幅减少。
不过我的建议是:新手时期先用函数视图写几个页面,把手动获取数据、渲染模板、处理 POST 请求的完整流程走一遍。因为 CBV 封装太狠,很容易出现“代码能跑但完全不知道发生了什么”的情况。等 FBV 手感有了,再切 CBV 就会觉得是在简化工作而不是黑魔法。
具体到一个页面怎么选?我的习惯是:
- 需要自定义复杂业务逻辑、多步骤表单、依赖外部 API 的——用 FBV,好调试。
- 纯数据库读取、模板展示、权限控制简单的——用 CBV,代码少,且自带
get_queryset扩展点。
有同学说 CBV 不好调试,其实django.views.generic的源码并不难读,花半小时把View、TemplateView、ListView三个类看一遍,你会打开新世界。
3. 核心细节解析:创建 app 的正确姿势与 settings 配置避坑
3.1 一条命令背后发生了什么
python manage.py startapp myapp是每个 Django 新手最先接触的命令之一。它会在当前目录下生成一个包含models.py、views.py、admin.py、apps.py、migrations/等文件的包结构。但很多人没注意到,这个命令只是生成文件,不会帮你注册到项目。
你需要去settings.py的INSTALLED_APPS里手动添加应用名,Django 才知道要同步这个 app 的数据表、读取它的模板和静态文件、加载它的路由。如果漏了这一步,你migrate时会发现数据表没有创建,python manage.py startapp生成的migrations/目录里也没有任何迁移记录——不是命令坏了,是你根本没告诉项目这个应用的存在。
另一个坑是 app 名称与 Python 包名冲突。我见过有人把一个内部业务应用命名为test,结果导入test.py模块时和 Python 内置测试库撞车。遇到奇怪导入错误时,先怀疑你的命名有没有撞标准库。
3.2 settings.py 里最容易翻车的三个配置项
第一是STATIC_URL和静态文件目录。官方教程只用了少量内联样式,真实项目里 CSS、JS、图片都需要单独管理。推荐在项目根目录建static/,在settings.py中配置:
STATIC_URL = '/static/' STATICFILES_DIRS = [BASE_DIR / 'static']开发环境这样够了。部署时的collectstatic是另一个大坑,后面会提。
第二是TEMPLATES里的DIRS。虽然 Django 默认会去每个 app 下的templates/找模板,但如果你想放全局模板(比如base.html、通用 404 页面),就要在DIRS里加上全局模板目录。否则模板继承时extends "base.html"会报找不到模板。
第三是ALLOWED_HOSTS。本地开发写空列表没问题,但一旦部署到服务器,不把域名或 IP 加进去,访问就会得到Bad Request (400)。这个报错新手几乎必遇一次。
3.3 官方教程没讲的 app 划分逻辑
真实项目里一个常见问题是:一个models.py里堆了太多模型。官方教程只有一个Question和Choice,看不出问题。但当你做一个博客系统时,如果用户、文章、评论、标签、友链全塞在一个 app 里,代码会迅速失控。
推荐思路是按业务边界切分:
user_app:用户扩展信息、登录注册相关逻辑blog_app:文章、分类、标签comment_app:评论、点赞
每个 app 有自己的models.py、views.py、urls.py,应用之间通过外键或明确接口关联。你要知道 Django 的 app 不是微服务,项目内部它们共享同一个数据库,但代码边界清晰能让你后期维护时少掉头发。官方教程里没有讲这套组织方法论,但这恰恰是实战结果里最有价值的部分。
4. 查询与删除对象:ORM 实操里那些教程没强调的细节
4.1 懒加载:为什么查完数据库没被执行
官方教程里写了Question.objects.all()和filter(),但没有明确告诉你 QuerySet 是惰性的。简单说:Question.objects.filter(pub_date__year=2024)这一行不会立刻打 SQL,只有当你真正“使用”这个结果时(遍历它、取长度、判断是否存在),Django 才去数据库查询。
这意味着两件事。第一,你可以链式叠加过滤条件,最后只执行一次查询:
qs = Question.objects.filter(pub_date__year=2024) qs = qs.filter(question_text__contains="投票") qs = qs.order_by("-pub_date")第二,如果你不小心把 QuerySet 存起来,在模板或视图里多次使用它,每次使用都可能触发新查询。比如在if判断里用一次,在 for 循环里又用一次,底层就是两次 SQL。小项目没问题,数据量大了以后这就是性能隐患。解决办法是及时list()转成列表,或者在循环外先取一次。
4.2 删除对象:delete() 到底删了什么
官方教程的投票应用没有删除功能,所以很多新手对删除只有model.objects.get(id=1).delete()这一步的理解。这个理解太浅了。
delete()会真正从数据库里删掉这一行数据。如果你是ForeignKey的“一”方,被删后与之关联的“多”方会根据on_delete参数行动。官方教程的Question和Choice之间用的就是级联删除(models.CASCADE),删掉问题,选项会被一起带走。
但在真实项目里,直接物理删除很多时候不是好选择。比如用户注销账号,你真的希望把用户的所有评论、订单历史全删干净吗?正常做法是软删除:给模型加一个is_deleted字段或者deleted_at时间字段,删除时只更新这个标记,查询时默认过滤掉已标记的。这样数据还在,需要审计时能查到,想恢复也容易。
我这里给你一个软删除常用的设计思路:
class Article(models.Model): title = models.CharField(max_length=200) content = models.TextField() is_deleted = models.BooleanField(default=False) deleted_at = models.DateTimeField(null=True, blank=True)查询时这样过滤:
Article.objects.filter(is_deleted=False)再用自定义管理器把这条规则固化:
class ArticleManager(models.Manager): def get_queryset(self): return super().get_queryset().filter(is_deleted=False)之后所有业务代码里写Article.objects.all()就自动排除已删除数据。这是我从实际项目里体会最深的一个模式:ORM 的默认查询入口就是软删除过滤层,比你在每个视图里手动加filter(is_deleted=False)可靠得多。
4.3 实战里最有用的几个 QuerySet 技巧
values()和values_list()是官方教程没教但实战高频使用的。当你只需要模型里的几个字段,不想加载整个对象时,用values()返回字典列表,values_list()返回元组列表。这样省内存、省时间,适合导出报表或 AJAX 接口。
Question.objects.values("id", "question_text") # 输出: [{'id': 1, 'question_text': '今天吃什么?'}, ...]select_related和prefetch_related是用来处理关联对象查询性能的。简单来说,select_related适合“一对一”和“外键”这种单条关联,一次 JOIN 查出来;prefetch_related适合“多对多”和“反向外键”,先查主表再批量查关联表。官方教程里看不到这两者的必要,因为数据少,等你页面一卡,通常就是 N+1 查询在作怪。
F 表达式我也提一下。它用于在查询中引用字段值本身,不查出来再赋值,直接由数据库完成运算。比如给阅读量加一:
from django.db.models import F Article.objects.filter(id=1).update(read_count=F("read_count") + 1)这个路径不会出现并发时先读后写导致计数丢失的问题。
5. 登录认证与 Cookie Token 设置:给投票应用补上用户体系
5.1 官方教程留的坑:登录只做了一半
官方教程的投票应用没有用户注册和登录,只有一个 Admin 后台的登录。Admin 登录是 Django 内置的,开箱即用。但真实项目需要用户在站前端登录、注册,并且后续请求能识别“你是谁”。
Django 内置的django.contrib.auth包含完整的认证框架:用户模型、会话、权限。官方教程没讲透的是它如何在 HTTP 无状态请求里维持登录状态——靠的是 Session,默认存在数据库的django_session表中,浏览器记住的是 sessionid 这个 Cookie。
方案一:保持使用服务端 Session。对于小型项目,这是最省事的,安全性也有保证。你在视图里写request.user.is_authenticated就能判断是否登录。
方案二:前端需要频繁访问这些接口但不想每次都带 CSRF token、不想依赖浏览器 Cookie 同源策略时,就要考虑 token 认证。这个方法在前后端分离项目里用得最多。
5.2 用 Cookie 设置 token 的完整做法
这里用一个实战思路:用户登录成功后,我生成一个 token 写入数据库(或缓存),同时通过response.set_cookie()把它写到浏览器。
为什么要用 Cookie 而不是让前端把 token 存到 localStorage?因为 localStorage 的 token 很容易被第三方脚本通过 XSS 窃取;Cookie 设置了HttpOnly后,JavaScript 读不到它的内容,能有效降低 XSS 风险。
简单实现如下:
import uuid from django.http import JsonResponse from django.contrib.auth import authenticate def login_view(request): if request.method == "POST": username = request.POST.get("username") password = request.POST.get("password") user = authenticate(request, username=username, password=password) if user is not None: token = uuid.uuid4().hex # 存到缓存,有效期 7 天 cache.set(f"auth_token:{token}", user.id, timeout=7 * 24 * 3600) response = JsonResponse({"code": 0, "msg": "ok"}) response.set_cookie( "auth_token", token, httponly=True, samesite="Lax", max_age=7 * 24 * 3600, secure=False, # 生产环境记得改为 True,走 HTTPS path="/", ) return response return JsonResponse({"code": 1, "msg": "用户名或密码错误"})取用户时写一个简单的认证后端,或者直接在视图中解析 Cookie:
from django.core.cache import cache from django.contrib.auth.models import User def get_login_user(request): token = request.COOKIES.get("auth_token") if not token: return None user_id = cache.get(f"auth_token:{token}") if not user_id: return None return User.objects.filter(id=user_id).first()这段代码我已经在多个项目里用过,足够简单,也够用。如果要更标准,可以把这部分包成基于AuthenticationMiddleware的认证后端,但核心逻辑就是上面这些。
5.3 CSRF 和 CORS:两个新手容易混的问题
官方教程里的表单都要写{% csrf_token %},这是 Django 的 CSRF 防护。但如果你用第三方 API 工具(Postman、Apifox)测试 POST 接口,经常会报 CSRF 验证失败。开发阶段可以先豁免,生产环境建议保留。
当你做前后端分离时,登录接口从另一个域名或端口发出请求时,就会有跨域问题,这由 CORS 配置管辖。简单说:同源请求靠 Cookie,跨域请求需要在响应头中告诉浏览器允许哪些域名访问。Django 没有内置 CORS 处理,通常装django-cors-headers。配置如下:
INSTALLED_APPS = [ ... "corsheaders", ] MIDDLEWARE = [ "corsheaders.middleware.CorsMiddleware", ... ] CORS_ALLOWED_ORIGINS = [ "http://localhost:3000", ]我见过很多人把 CSRF 和 CORS 搞混:CSRF 防的是伪造请求,CORS 管的是浏览器同源策略导致的跨域权限。互相之间有联系,但不是一个东西。排查问题时先分清你遇到的到底是谁。
6. 用 WebSocket 实现后台数据推送:让页面不再等人刷新
6.1 为什么需要 WebSocket,轮询到底差在哪
传统 HTTP 请求是请求-响应模型:前端发起请求,后端返回结果。如果后台数据变化了想推给前端,通常只能让前端隔几秒轮询一次接口。这种方案在小规模场景能用,但有几方面局限性:实时性差(几秒延迟)、浪费资源(大部分轮询请求都是空回来的)、服务端压力大(频繁建连、频繁查询)。
WebSocket 解决的是全双工通信的问题:前端和服务器建立起一条长连接后,服务器可以随时主动推送消息给前端,前端也可以随时发消息给服务器。适合实时通知、在线状态、数据看板、聊天消息这类场景。
Django 原生不支持 WebSocket,因为它走的是 HTTP 的 WSGI 协议。要支持 WebSocket,得切换到异步——这就是 Django Channels 的用途。Channels 基于 ASGI,可以同时处理 HTTP 和 WebSocket。
6.2 用 Django Channels 实现后台推送
这里我以一个“后台数据变化后,前端实时收到通知”的场景为例。
第一步,安装 Channels:
pip install channels第二步,把项目从 WSGI 模式转换成 ASGI 模式。找到asgi.py文件,改成这样:
import os from django.core.asgi import get_asgi_application from channels.routing import ProtocolTypeRouter, URLRouter from django.urls import path from your_app.consumers import NotificationConsumer os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings") application = ProtocolTypeRouter({ "http": get_asgi_application(), "websocket": URLRouter([ path("ws/notifications/", NotificationConsumer.as_asgi()), ]), })第三步,编写消费者。消费者相当于 WebSocket 的视图,负责处理连接建立、收到消息、断开连接:
from channels.generic.websocket import AsyncJsonWebsocketConsumer class NotificationConsumer(AsyncJsonWebsocketConsumer): async def connect(self): await self.accept() # 可以把连接分组,方便后台定向推送 await self.channel_layer.group_add("notifications", self.channel_name) async def disconnect(self, close_code): await self.channel_layer.group_discard("notifications", self.channel_name) async def notify(self, event): # 后台调用 group_send 时会触发这个方法 await self.send_json({ "message": event["message"], })第四步,在后台任意位置推送消息:
from channels.layers import get_channel_layer from asgiref.sync import async_to_sync channel_layer = get_channel_layer() def push_notification(message): async_to_sync(channel_layer.group_send)( "notifications", { "type": "notify", "message": message, } )这样只要调用push_notification("有新订单"),前端就能实时收到消息。
6.3 前端接入与踩坑实录
前端用原生 JavaScript 接入很简单:
const socket = new WebSocket("ws://localhost:8000/ws/notifications/"); socket.onmessage = function(event) { const data = JSON.parse(event.data); console.log("收到推送:", data.message); };但我实际使用中踩过几个坑,值得记下来。
第一个坑是通道层配置。生产环境 Channels 需要 Redis 作为通道层,开发环境可以只用 InMemory 通道层:
CHANNEL_LAYERS = { "default": { "BACKEND": "channels.layers.InMemoryChannelLayer", }, }但 InMemory 只在单进程、单 worker 下有效。一旦你用多个 worker,推送消息很可能发不到你连接所在的那个进程。生产环境务必换 Redis,我最早上线时忘了换,结果推送消息时好时坏,排查了大半天。
第二个坑是async_to_sync。普通 Django 视图是同步的,调用 Channels 的异步方法要包一层async_to_sync。很多人第一次写的时候漏掉这个,会报SynchronousOnlyOperation错误。
第三个坑是部署时的 ASGI 服务器选择。开发阶段用uvicorn或者 Daphne 都可以,生产环境也需要对应配置反向代理支持 WebSocket 升级。Nginx 层需要设置Upgrade头,这个在常规 HTTP 配置里不会自带,忘了配置的话客户端永远连不上 wss。
第四个坑是连接鉴权。WebSocket 连接建立时如何知道用户身份?我常在connect()中通过查询字符串或 Cookie 拿到 token,再解析用户。需要注意 WebSocket 的握手阶段没有普通视图的request,但 Channels 提供了一个scope对象,里面带着 Cookie 等 HTTP 会话信息。简单示例如下:
from django.contrib.auth.models import AnonymousUser async def connect(self): cookies = self.scope.get("cookies", {}) token = cookies.get("auth_token") # 解析 token,找到 user,存到 self.scope["user"] 中 self.scope["user"] = await get_user_by_token(token)7. 常见问题与排查技巧:实战中踩过的那些坑
7.1 新手必踩的五个坑
官方教程之后的独立开发,最容易踩的坑我列一下,都是真实项目中多次见到的。
第一个是静态文件 404。本地开发时DEBUG=True的情况下静态文件通常没问题,但部署后DEBUG=False时,Django 不再帮你处理静态文件,需要collectstatic收集到指定目录并交给 Web 服务器托管。很多人部署后页面样式全丢,就是漏了这一步。
第二个是数据库迁移冲突。多人协作时经常出现,你本地改了模型,同事也改了同一个模型,两边各自生成迁移文件,合并时就会有冲突。不要怕,Django 有内置合并命令:
python manage.py makemigrations --merge第三种是时区问题。USE_TZ = True时,Django 存储和处理的都是带时区的时间,如果你在前端直接用datetime.now()跟模型里的时间比较,容易差 8 小时。统一使用django.utils.timezone.now()。
第四种是query.get()的异常处理。get()匹配不到数据会抛DoesNotExist,匹配到多条会抛MultipleObjectsReturned。如果你不确定数据唯一性,最好用 try-except 包住,或者用filter().first()。
第五种是表单验证的坑。官方教程里的form.is_valid()看起来简单,但实际项目里你往往需要自定义验证逻辑。一个常见误区是在clean()里直接改self.cleaned_data,但忘了cleaned_data可能还没有字段值。正确做法是逐个字段定义clean_字段名()方法。
7.2 排查问题的一个固定顺序
我调 Django 问题一般遵循这个顺序:
- 看浏览器 Network 面板,确认请求 URL、状态码、响应体。很多时候问题不在后端,而是前端没发对请求。
- 看后端日志。Django 开发服务器的终端输出会打印 SQL 和异常堆栈,这是定位问题的宝藏。
- 如果异常信息不明,用
python manage.py shell复现。可以手动执行模型查询和函数调用,比在视图里盲目打日志快得多。 - 模型、视图逻辑都没问题时,再怀疑配置。检查
settings.py里的INSTALLED_APPS、中间件顺序、URL 路由。
这套顺序帮我排掉过六成以上的问题。真正复杂的往往不是技术,而是数据问题——比如旧数据的脏数据导致新逻辑报错,这种只能靠写脚本清洗。
7.3 调试技巧:少走冷门弯路
print()大法虽然不高级,但配合python manage.py shell真的很实用。我经常把要验证的代码片段直接粘进 shell 里跑,省去了反复重启开发服务器的时间。
另外一个冷门技巧是django-debug-toolbar。它会在页面侧边显示 SQL 查询数量、耗时、模板加载情况,对性能优化很有帮助。装上之后你会发现一个页面居然查了几十次数据库——这就是 N+1 问题的证据,然后用select_related和prefetch_related修复。
关于日志,建议早点用标准库的logging而不是print。配置一个文件日志处理器,把异常写入 logs 文件,对排查线上问题是长期投资。Django 有个内置的django.request日志器,配置好以后 5xx 错误会自动记录请求路径和异常信息。
8. 扩展方向与我的个人体会
官方教程只是一个开始,这句话很多人说,但实践之后才能体会到它的分量。拍卖完投票应用之后,如果你想继续深入,我建议按这样的顺序来:先做一个带用户注册登录的简单内容发布系统,把用户体系、session、表单验证、soft delete 都用进去;然后做接口化改造,试着用 JsonResponse 返回数据,接入 token 认证;最后再碰 WebSocket 和异步任务,做实时通知或者后台一键推送。
我个人的实际体会是,Django 的坑多数不在框架本身,而在那些你没理解透的概念上——懒惰查询、时区、CSRF、CORS、ASGI 和 WSGI 的区别。每一个概念初看都很简单,但组合起来之后,问题会变得诡异。这篇记录里很多内容也是我自己踩坑之后才想明白的。希望你看完能少走一些弯路。
最后分享一个小技巧:官方教程的投票应用项目不要删,把它当成一个实验场。你学到的新东西,比如 token 登录、WebSocket、类视图,都可以先在这个小项目里试一遍。小项目不会因为搞挂而有负担,但你能完整地跑通整个链路,这个经验积累对下一个正式项目非常宝贵。
如果你是刚把官方教程做完,先别急着找更复杂的教程。把投票应用从“能跑”改到“像样”:加上用户注册、让用户可以投票后看到结果、给后台加上一些筛选功能。这个改造过程,比看十个新教程都更有价值。