把Django官方教程从头到尾走完一遍这个计划我酝酿了很长时间。之前也看过不少教学视频跟着敲过好几个所谓的“实战项目”但知识始终是散的——能改别人的代码自己却写不出东西项目能跑起来但说不清楚为什么能跑。这次我下定决心老老实实把官方文档那个投票应用从头撸到尾七个部分一步步过。说实话走完以后收获远超预期URL调度、ORM、模板继承、测试、静态文件、Admin后台定制整套骨架都藏在官方这个看似简单的应用里。这篇博文就是这次实战的完整记录包括我踩过的坑、排查问题的思路以及后来又自己加的两个小功能的经验给准备啃官方文档的新朋友一份参考。1. 为什么我建议完整走一遍官方教程1.1 教程表面是“投票系统”底层是Django的心智模型很多人觉得官方教程太简单做了个投票功能很没成就感。但如果你把教程当“框架使用说明书”去看视角就完全不同了。官方教程的投票应用从头到尾覆盖了Django Web开发的核心链路第一部分创建项目、创建应用、编写第一个视图第二部分数据库配置、模型定义、激活模型、Admin后台初识第三部分视图与模板、404错误、模板语法第四部分表单处理、通用视图ListView / DetailView第五部分自动化测试第六部分静态文件管理第七部分自定义Admin后台这套结构对应的正是一套Web应用从开发到上线要考虑的全部问题。你跟着撸完等于把“用户请求怎么进、数据怎么存、页面怎么渲染、怎么测、怎么部署静态资源”这条主线完整过了一遍。之后再看任何第三方教程你都能快速定位它讲的是这条主线上的哪个环节知识不再是零散的碎片。1.2 从零到“能跑”环境准备环节也有讲究我一开始的环境比较乱系统里既有Python 2时代的残留工具又装了Python 3还遇到过Django版本冲突的问题。这里强烈建议先建虚拟环境python3 -m venv venv source venv/bin/activate pip install Django之后再执行django-admin startproject mysite就不会找错解释器。Windows用户注意source venv/bin/activate要换成venv\Scripts\activate。这个细节不处理好后面可能出现“明明pip list里有Django但django-admin就是找不到”的诡异情况。创建好项目骨架后我会立刻创建一个应用python manage.py startapp polls项目与应用的区别值得想明白一个project是“站点配置容器”一个app是“具体业务模块”。这个区分在项目变大后尤其重要把不同功能拆进不同app目录结构会清晰很多。2. 模型层是理解Django的最佳起点2.1 从“建表”到“ORM思维”模型的真正含义官方教程里的经典模型是Question和Choice这里体现的是ORM的核心关系一篇文章对应多条评论一个投票问题对应多个选项。from django.db import models class Question(models.Model): question_text models.CharField(max_length200) pub_date models.DateTimeField(date published) def __str__(self): return self.question_text class Choice(models.Model): question models.ForeignKey(Question, on_deletemodels.CASCADE) choice_text models.CharField(max_length200) votes models.IntegerField(default0) def __str__(self): return self.choice_textForeignKey后面的on_deletemodels.CASCADE非常重要它定义了父对象被删除时子对象怎么处理。CASCADE表示级联删除父问题删了它下面的所有选项也一起删。另一个常用选项是SET_NULL要求外键字段允许为空适合“删除分类但不删除文章”的业务场景。我还想强调__str__方法。教程里写它只是为了在Admin后台和shell里能友好显示对象看起来不起眼实际开发中作用很大。没有它调试时看到的一排Question: Question object (1)会让人头大。2.2 查询、过滤与删除对象教程没写透的实用操作官方教程用了python manage.py shell演示增删改查但篇幅有限很多关键操作一笔带过。这里补充几个我实战中反复用到的高频操作。查询单个对象时get与filter的差别# 查不到会抛DoesNotExist多条会抛MultipleObjectsReturned q Question.objects.get(pk1) # 查不到返回空QuerySet多条返回多条不会报错 qs Question.objects.filter(question_text__startswithWhat)刚接触ORM的人容易在这两个方法上栽跟头。get返回的是一个模型实例适合按照唯一键取对象filter返回的是QuerySet适合做列表筛选。删除对象的正确姿势# 删除单个对象 q Question.objects.get(pk1) q.delete() # 删除多个对象返回元组第一个元素是删掉的总条数 deleted_count, _ Question.objects.filter(pub_date__year2023).delete() # 级联删除删掉问题后它的Choice也会被自动删除这里有一个很实用的技巧如果要快速处理“陈旧数据”直接对查询集调用delete()往往比写循环高效得多它会生成一条批量删除SQL而不是挨个执行。但同时要格外小心级联删除误删了关联数据无法一键恢复。我建议操作前先在shell里count()确认数据量。2.3 迁移机制数据库版本控制的逆向思维刚开始我对迁移机制的理解是“自动同步数据库”直到在项目里改过几次模型字段后才体会到它真正的设计意图。模型改了数据库结构也要跟着变这时执行python manage.py makemigrations polls python manage.py migratemakemigrations会生成一个迁移文件这个文件本质上是“数据库结构变更说明书”它记录了你从上一个版本到当前版本做了哪些改动。重点在于这个文件是纳入版本控制的团队成员从Git拉代码后只要执行migrate数据库就能和你保持一致。我踩过的坑是改模型字段后没看迁移文件名就顺手全删了。后来队友执行migrate时报错对不上历史。正确做法是让makemigrations生成的迁移文件按顺序保留不要随意手动删除。3. 视图、URL和模板请求的完整生命周期3.1 URL调度的两个易忽略细节官方教程有一步很关键把polls应用的URL接到项目主URL下。# mysite/urls.py from django.urls import include, path urlpatterns [ path(polls/, include(polls.urls)), ]include()的作用是把子应用的URL配置“挂载”到主项目里让每个app保持独立。我觉得这里有两个细节值得展开一是路径转换器。path(polls/int:question_id/, views.detail)里的int:question_id不只是占位符它自带类型转换和校验。如果传了非数字字符Django直接返回404不需要自己在视图里写正则判断。除了int还有str、slug、uuid、path几种转换器设计API的时候很实用。二是reverse()与重定向。模板里用{% url polls:detail question.id %}视图里做跳转时建议用reverse()而不是手写URL字符串。因为项目变大后URL可能调整手写路径很容易漏改导致404reverse()会根据URL配置自动生成当前正确的地址这是隐藏很深的省心技巧。3.2 视图函数与通用视图怎么选官方教程从函数视图讲到ListView和DetailView的改造这个进阶过程很有代表性。刚开始写视图就是一个函数里面对QuerySet做各种处理然后render返回模板def index(request): latest_question_list Question.objects.order_by(-pub_date)[:5] context {latest_question_list: latest_question_list} return render(request, polls/index.html, context)改成通用视图之后代码量大幅减少from django.views import generic class IndexView(generic.ListView): template_name polls/index.html context_object_name latest_question_list def get_queryset(self): return Question.objects.order_by(-pub_date)[:5]但通用视图有一个“潜规则”容易让初学者困惑它默认会在app_name/models/modelname_list.html找模板默认传入的context变量名是object_list。如果你不看文档直接套模板里变量名对不上页面渲染出来是空的。我的经验是列表页适合用ListView详情页适合用DetailView但一旦页面定制程度高、表单处理多还是函数视图更直觉。不要为了炫技强行上通用视图代码可读性优先。3.3 模板系统extends与include的协作教程里虽然只是简单渲染了几个页面但模板继承的思想非常重要。!-- polls/base.html -- !DOCTYPE html html langzh-hans head title{% block title %}投票系统{% endblock %}/title /head body {% block content %}{% endblock %} /body /html子模板只需要写{% extends polls/base.html %} {% block content %} h1{{ question.question_text }}/h1 {% endblock %}这样的好处是全站导航栏、页脚、CSS引用只需维护一份。后来我给系统加了“最新投票”侧栏直接在base.html里加一套模板片段所有页面同步生效。模板里加载静态文件也要注意使用{% load static %}再加{% static polls/style.css %}这样Django能自动拼对静态文件路径部署后也能通过配置切换CDN。4. 进阶扩展把教程项目改造成“能用”的系统4.1 Admin后台优化用django-unfold提升颜值与效率官方教程里的Admin后台是Django原生样式功能全但界面比较原始。在实际交付时客户对后台颜值是有要求的所以我试了几个第三方后台主题留到最后的是 django-unfold 。它跟原生Admin完全兼容只是换了一套现代风格的模板和组件。安装配置很简单pip install django-unfold然后在INSTALLED_APPS里注意顺序把unfold放在django.contrib.admin前面INSTALLED_APPS [ unfold, django.contrib.admin, # ... ]登录页和后台列表页样式立刻变化。配合自定义配置还能调整侧边栏品牌名、侧栏菜单展开等。不过这里我建议不要为了炫酷引入太多第三方依赖轻量主题改起来容易重之又重反而给后期升级留隐患。顺带说一下Admin列表页的优化。原生Admin默认展示的是__str__返回值信息量很低。给注册的模型加几个属性效果完全不同class QuestionAdmin(admin.ModelAdmin): list_display [question_text, pub_date] list_filter [pub_date] search_fields [question_text]4.2 实时推送给投票页面加WebSocket官方教程里数据更新后需要手动刷新页面才看得到但在真实场景里“后台有数据变化前端自动推送更新”是高频需求。这个就要用WebSocket。Django原生对WebSocket的支持比较弱一般会配合Django Channels来做。Django Channels的原理可以简单理解成让Django能同时处理HTTP和WebSocket两种协议把长连接请求交给异步channel layer而不是走经典的请求-响应循环。它在项目里引入了一个ASGI入口。配置步骤大致如下pip install channels# mysite/asgi.py import os from django.core.asgi import get_asgi_application from channels.routing import ProtocolTypeRouter, URLRouter os.environ.setdefault(DJANGO_SETTINGS_MODULE, mysite.settings) application ProtocolTypeRouter({ http: get_asgi_application(), websocket: URLRouter([ path(ws/poll_feed/, consumers.PollConsumer.as_asgi()), ]), })consumer里的核心逻辑就是一个简单的异步推送import json from channels.generic.websocket import AsyncWebsocketConsumer class PollConsumer(AsyncWebsocketConsumer): async def connect(self): await self.accept() async def receive(self, text_data): data json.loads(text_data) # 模拟广播最新投票数量给前端 await self.send(text_datajson.dumps({ question_id: data[question_id], votes: data[votes], }))前端用浏览器原生WebSocket对象就能收const ws new WebSocket(ws://localhost:8000/ws/poll_feed/); ws.onmessage (event) { const data JSON.parse(event.data); document.getElementById(vote-count).textContent data.votes; };生产环境部署时除了Django自身还需要在反向代理层开启WebSocket转发配置并确保连接不走的普通HTTP超时限制。这块我第一次搭的时候忽略了结果开发环境正常、上线后连接频繁断开排查了很久才明白是代理层没配对。4.3 Cookie与Token认证状态管理的实用落点官方教程里主要用了Session来记住匿名用户是否投过票这个机制本质上是服务端把状态存在Session表里浏览器只保存一个SessionID的Cookie。这个模式在后端管理系统中够用但做前后端分离或需要给第三方开放API时Token方式更常见。Django里设置Cookie原生也很简单response HttpResponse(ok) response.set_cookie(username, dev, max_age86400)但直接设Cookie容易引发安全风险。实战中更稳妥的做法是用成熟的认证方案比如Django REST framework配合JWT。JWT的核心是服务端不保存Session用户登录后拿到的Token自带有效期和签名信息。客户端每次请求把Token放到Authorization请求头里服务端验签即可。教程项目虽然用不到这么复杂但理解这个区别很有价值。Socket、Session、Cookie、Token是四层不同的东西很多人混在一起说。我的理解是Cookie只是浏览器存储数据的载体Session是服务端存储用户状态的容器Token则是无状态认证用的凭证。5. 实战中的常见障碍与排查技巧5.1 迁移失败改模型改崩了怎么办我遇到最典型的报错是django.db.migrations.exceptions.InconsistentMigrationHistory出现原因通常是历史迁移被手动改过或者数据库里表结构已经变了。处理思路是不要慌先确认哪些迁移已经执行过。python manage.py showmigrations polls如果某个迁移显示[X]表示已执行说明数据库状态和迁移文件对不上。最简单的修复方案是回退到上一个迁移把它对应的表删掉再重新迁移而不是暴力清空数据库。当然如果你还在学习阶段直接删掉开发库重建也不是不行但一定要意识到生产环境绝对要保留数据迁移文件是唯一可靠的版本记录。5.2 静态文件加载不出来开发与部署的差异开发环境访问http://127.0.0.1:8000/static/polls/style.css404这个坑几乎人人必踩。原因在于DEBUG模式下Django会自动提供静态文件服务但要求模板里用的是{% static %}标签而且应用里的静态文件要放在polls/static/polls/这个嵌套目录结构里。部署上线时情况又有变化。需要先执行收集静态文件python manage.py collectstatic然后把STATIC_ROOT指向的目录交给Nginx或对象存储来托管Django本身不再直接服务静态文件。这个转换过程有很多新手卡在“本地正常、上线白板”其实核心就是静态文件服务职责转移的问题。5.3 用shell调试数据写测试之外的快速验证手段官方教程第五部分讲了测试我强烈建议把测试用例写完但日常调试里最高效的工具还是python manage.py shell。python manage.py shellfrom polls.models import Choice, Question # 统计最近30天创建的问题 Question.objects.filter(pub_date__gtetimezone.now() - timedelta(days30)).count()这种交互式操作对数据库做小范围增删改查非常顺手查完可以直接exit()。我还经常配合shell批量生成测试数据写段循环造几百条记录比手工在Admin后台一条条录入快得多。5.4 官方教程之外还应该补什么教程本身是骨架它没讲清楚但你迟早会遇到的知识点还有不少用户认证与权限、文件上传、表单集、数据库索引优化、日志配置、环境变量管理、Docker部署。这些是另一个学习路径。我的建议是先把官方教程练透再去接触Django REST framework从写API的角度反推框架设计理解会更深。最后再分享一个小技巧我这次实战最大的体会是不要追求“跑通”要追求“改得动”。教程代码看似简单但我每学完一个部分就尝试在源码上做一点没有标准答案的改动比如给投票系统加一个“热门问题”排序、限制投票时间、给choice加图片字段。改着改着就会发现说明书上的代码和自己的业务需求之间隔着的恰恰是你对Django设计哲学的熟悉程度。等你能在报名前心里默写出整个请求流程——URL找到视图、视图取数、模板渲染、响应返回——就说明你已经真正跨过了Django新手村。