Django 是 Python 全栈开发里绕不开的那根“定海神针”。很多人学完 Flask 或者写完几个脚本接口之后想做一个真正能落地的全栈项目最后都会回到 Django 上来自带 Admin 后台、ORM、模板引擎、路由系统一套东西能从前端页面管到后端接口甚至连数据库迁移都给你包圆了。这篇内容我围绕 Django 的基本配置和项目初始化展开把从创建项目到跑通第一个页面的完整过程、settings.py 里每个关键配置的作用、以及全栈开发里最常碰到的 ORM 查询删除、Cookie/Token 设置、WebSocket 实时推送这些高频场景全部拆开讲一遍。适合刚接触 Django 想直接上手搭项目的新手也适合需要一个快速可复用的配置清单的初级全栈开发者。我自己从 Django 2.x 一路用到 5.x最大的感受是Django 的难点从来不在语法而在配置的理解。只要把 settings.py 和项目骨架搞透了后面写业务代码就是水到渠成的事。1. 开工之前环境准备与工程骨架1.1 虚拟环境这一步千万别省Django 项目我基本不会在系统 Python 环境里直接开干。原因很简单不同项目的依赖版本经常打架尤其是个别第三方库和 Django 版本之间的兼容性问题在系统环境里装一次就知道疼了。用虚拟环境隔离依赖是我每次开头第一件事。# 创建虚拟环境 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # macOS / Linux: source venv/bin/activate # 安装 Django pip install django # 验证版本 python -m django --version这一步不需要复杂解释但有个细节值得注意有些人喜欢用 conda 或者 virtualenvwrapper都可以关键是保证pip list里是干净的。我见过太多“装了半天报错 ModuleNotFoundError: No module named django”的案例最后问题基本都出在没激活虚拟环境或者激活了但 IDE 的终端没继承环境变量。如果你用的是 PyCharm直接在 Settings 里把 Project Interpreter 指到 venv 路径比每次手动激活要省心得多。1.2 用 django-admin startproject 搭骨架时的细节Django 提供了脚手架命令但脚手架不等于理解。先跑命令再逐个解释目录结构django-admin startproject myproject .注意我加了末尾的.这会让项目文件直接生成在当前目录而不是再嵌套一层myproject/myproject。这个习惯在后面部署或者想用 docker 时尤其方便目录层级少一层路径配置少踩一个坑。生成完之后的目录长这样manage.py myproject/ __init__.py settings.py urls.py asgi.py wsgi.py刚开始的时候settings.py是核心中的核心它几乎决定了项目在开发环境里跑不跑得起来。urls.py是全局路由入口所有 URL 分发都要经过这里。wsgi.py和asgi.py则是部署相关的入口前者对应传统的同步服务Gunicorn 用这个后者对应异步场景后面讲 WebSocket 的时候就要动它。1.3 第一个配置检查命令manage.py check很多新手启动项目的方法就是python manage.py runserver但我在写任何配置文件之后、启动服务之前都会先跑一条命令python manage.py check这条命令会做一次系统性的配置检查比如 settings.py 里语法错误、URL 配置冲突、应用注册问题都能快速报出来。实测下来它的执行速度很快秒级完成而 runserver 启动后如果配置有问题经常是报一堆看不懂的异常然后再退出。养成先 check 再 runserver 的习惯能省下大量排查时间。2. Django 核心配置逐项拆解从 settings 到数据库2.1 INSTALLED_APPS 与 app 注册逻辑打开 settings.py第一眼就是大段配置。很多人容易懵的地方在于Django 的项目与 app 是分离的。项目是容器app 是功能模块。你在项目里可以创建多个 app每个 app 负责一块业务比如用户、文章、留言板。关键点是创建出来的 app 不会自动生效必须手动加进 INSTALLED_APPS。INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, # 你自己的 app 加在下面 blog, users, ]默认的 7 个内置应用我简单说一下各自的用途免得你删了不该删的admin自带的后台管理界面。auth用户认证系统负责登录、权限。contenttypes为模型提供通用关系支持auth 的权限离不开它。sessions会话框架处理用户 session 的存储。messages临时消息提示Admin 后台经常会用到。staticfiles静态文件管理后面处理 CSS/JS 就靠它。新创建的 app 添加的位置也有讲究建议加在默认应用的后面别插到中间去否则有些第三方包初始化时依赖的 app 顺序会出问题。2.2 数据库配置从 SQLite 切到 MySQL/PostgreSQLDjango 开发环境默认用 SQLite好处是零配置文件即数据库。但对于全栈项目一旦涉及并发写入多、数据量大、部署上云这些场景SQLite 基本撑不住。我一般会在一开始就用 MySQL 或者 PostgreSQL避免项目写到一半再切换时被类型差异坑到。DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: myproject_db, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } }这里有个实用建议连接 MySQL 时字符集选项强烈建议显式指定utf8mb4。否则存 Emoji 表情或者生僻字会报Incorrect string value错误。你也许会觉得这是小事但等到线上用户评论带个表情导致接口报错那种尴尬只有经历过的人才懂。PostgreSQL 的话把ENGINE换成django.db.backends.postgresql就行。两个都行我更倾向 PostgreSQL 一些尤其在查询复杂度和 JSON 字段处理上PostgreSQL 的 JSONField 比 MySQL 的 JSON 类型顺手太多。数据库迁移也要记住一对命令组合python manage.py makemigrations python manage.py migrate前者根据模型变更生成迁移文件后者把迁移真正写进数据库。顺序不要反少跑哪一步都会导致模型和数据表不同步。2.3 语言、时区、静态文件这些“不起眼”的配置settings.py 里有几个配置非常容易被忽略但影响体验最直接LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai USE_TZ TrueLANGUAGE_CODE改成zh-hans之后Admin 后台界面会变成中文这是最直观的变化。TIME_ZONE设置时区配合USE_TZ TrueDjango 会在数据库里存 UTC 时间在渲染时换算成本地时间。这里有个容易踩的坑如果你在代码里用datetime.datetime.now()获取当前时间得到的是 UTC 时间不是北京时间。想拿本地时间应该用django.utils.timezone.now()或者干脆引入zoneinfo手动指定时区。静态文件配置也是一个高频问题区域STATIC_URL static/ STATICFILES_DIRS [BASE_DIR / static] STATIC_ROOT BASE_DIR / staticfilesSTATIC_URL是浏览器访问静态文件时的 URL 前缀。STATICFILES_DIRS是开发环境里你放静态资源的目录可以有多个。STATIC_ROOT是执行collectstatic时把所有静态文件收集起来的目标目录部署时 nginx 指向它。这个配置看起来简单但很多新手的静态文件 404 问题都出在把文件放错位置、或者没建static目录。记住开发环境下Django 是直接从STATICFILES_DIRS里的目录找文件的生产环境下静态文件由 nginx 托管Django 不直接管。3. 全栈开发关键环节创建 app、ORM 操作与接口认证3.1 创建 app 的正确姿势startapp 与目录结构进入全栈开发的正题后第一步通常是创建业务模块python manage.py startapp blog创建完之后的目录结构是这样的blog/ __init__.py admin.py apps.py models.py views.py tests.py urls.py # 这个文件通常需要自己创建 migrations/ __init__.py很多人会习惯性地把视图函数都堆在views.py里项目小的时候没问题一旦业务复杂起来这个文件就成了一个无限膨胀的垃圾场。我的习惯是在业务模块内部按功能再拆分子模块比如views/目录里放blog_views.py、comment_views.py然后通过views/__init__.py统一导出。这样路由写起来简洁以后找人改代码也快。别忘了把 app 注册进INSTALLED_APPS否则你在migrate时能看到表格迁移成功但路由和模型就是找不到报错信息还很误导人。3.2 ORM 模型设计与查询、删除对象实战Django 的 ORM 是全栈开发效率的加速器。我们定义一个简单模型from django.db import models class Article(models.Model): title models.CharField(max_length200, verbose_name标题) content models.TextField(verbose_name内容) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) def __str__(self): return self.title模型里字段类型的选择不能随意短文本用CharField长文本用TextField日期用DateTimeField。如果一开始字段类型定错后面数据量大了再改类型迁移成本会非常高。查询和删除是日常操作里最频繁的场景我直接列一些实战代码# 查询单个对象 article Article.objects.get(id1) # 查询不存在会抛 DoesNotExist多条会抛 MultipleObjectsReturned # 所以如果确定只有一条用 get不确定就用 filter().first() # 条件查询 articles Article.objects.filter(title__containsDjango) # 排序 latest_articles Article.objects.order_by(-created_at) # 删除单个对象 article.delete() # 批量删除符合条件的所有对象 Article.objects.filter(created_at__year2022).delete() # 注意 delete() 返回的是一个元组 deleted_count, detail Article.objects.filter(id__gt10).delete() print(deleted_count)这里必须提醒一个关键点delete()的级联行为。如果你有外键关联比如 Article 和 Comment 是一对多关系删除 Article 时默认会级联删除所有关联的 Comment。这是 Django 默认的on_deletemodels.CASCADE行为。如果业务上不允许连带删除可以在外键字段上换用on_deletemodels.PROTECT这样有子记录时删除父记录会报ProtectedError从根源上避免误删。还需要注意性能问题filter().delete()是数据库层面的批量删除不会触发模型里的delete()方法所以如果你重写了该方法并期望它执行比如写入日志、清理缓存批量删除时是不会执行的。遇到这种业务场景得手动遍历删除for obj in Article.objects.filter(created_at__year2022): obj.delete()单删和批删的语义差异是 ORM 里非常容易踩坑的细节。3.3 登录态与接口安全Cookie 与 Token 设置全栈开发意味着你既要页面也要接口。登录状态的保持就需要考虑 Cookie 和 Token 的配合。Django 自带的 Session 框架支持将 session 数据存到数据库、缓存、或者文件里配合 Cookie 使用。最简单的做法是利用 Django 内置的 session# 写入 def login_view(request): user authenticate(usernamexxx, passwordxxx) if user: request.session[user_id] user.id return JsonResponse({code: 0}) # 读取 user_id request.session.get(user_id) # 退出登录 def logout_view(request): request.session.flush()但如果你做前后端分离前端是 Vue 或 React走 AJAX 请求接口Session 的方案就不太方便了因为 Cookie 的跨域问题会让前端开发人员抓狂。这时候我会选择 JWTJSON Web Token方案Django 生态里最常用的是djangorestframework-simplejwt# settings.py REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: ( rest_framework_simplejwt.authentication.JWTAuthentication, ), } # 获取 token 的接口 from rest_framework_simplejwt.views import TokenObtainPairView urlpatterns [ path(api/token/, TokenObtainPairView.as_view()), ]Token 的存储位置也是个值得讲究的问题。不建议把 JWT 放在本地存储localStorage里因为 XSS 攻击能直接把 token 偷走。我常用的做法是将 token 放在 HttpOnly Cookie 里JS 脚本读不到这个 Cookie前端通过fetch自动携带 Cookie 完成认证。服务端设置 HttpOnly Cookie 很简单response JsonResponse({code: 0}) response.set_cookie( access_token, token, httponlyTrue, samesiteLax, secureFalse, # 生产环境记得改成 True走 HTTPS )SameSite属性也很重要Lax允许同站请求携带 Cookie但跨站 POST 请求不带Strict则更严格。对于登录态保护来说Lax是在安全性和易用性之间不错的平衡点。生产环境务必加secureTrue否则 Cookie 在 HTTPS 页面和 HTTP 之间传来传去非常容易被中间人截获。3.4 实时数据推送在 Django 中集成 WebSocket热词里出现了“python django websocket实现后台有数据前端推送”这说明很多人在 Django 项目里遇到了实时通信需求。比如后台任务完成了一条数据处理前端页面需要马上收到通知轮询虽然简单但效率太低WebSocket 才是正解。Django 3.0 之后引入了 ASGI 规范支持异步但默认的runserver并不支持 WebSocket 协议。要在 Django 项目里用 WebSocket主流方案是引入channels库pip install channels安装之后要把channels加进INSTALLED_APPS并且把asgi.py配置成指向 Channels 的应用# asgi.py import os from django.core.asgi import get_asgi_application from channels.routing import ProtocolTypeRouter, URLRouter from channels.auth import AuthMiddlewareStack from django.urls import path os.environ.setdefault(DJANGO_SETTINGS_MODULE, myproject.settings) application ProtocolTypeRouter({ http: get_asgi_application(), websocket: AuthMiddlewareStack( URLRouter([ path(ws/notify/, YourConsumer.as_asgi()), ]) ), })然后写一个最基础的 consumerfrom channels.generic.websocket import AsyncWebsocketConsumer import json class NotifyConsumer(AsyncWebsocketConsumer): async def connect(self): self.group_name notify_group await self.channel_layer.group_add(self.group_name, self.channel_name) await self.accept() async def disconnect(self, code): await self.channel_layer.group_discard(self.group_name, self.channel_name) async def send_message(self, event): message event[message] await self.send(text_datajson.dumps({message: message}))后台要主动推送消息给前端时从任意视图函数里调用from channels.layers import get_channel_layer from asgiref.sync import async_to_sync def push_notify(request): channel_layer get_channel_layer() async_to_sync(channel_layer.group_send)( notify_group, { type: send_message, message: 后台有新数据啦, } ) return JsonResponse({status: ok})前端的连接代码也很简单const socket new WebSocket(ws://127.0.0.1:8000/ws/notify/); socket.onmessage function(event) { const data JSON.parse(event.data); console.log(data.message); // 更新页面 };这里需要特别说明跑 WebSocket 服务时不能再用 Django 默认的runserver因为默认服务器对 WebSocket 支持不完整。正确姿势是用daphne或者uvicorn启动pip install daphne daphne myproject.asgi:application只用runserver的话能连上但很容易掉线很多新手卡在这里怀疑自己代码写错其实只是服务器没选对。实时推送这块还有一个隐藏坑channel_layer默认是内存里的所以如果你用了多进程启动不同进程之间消息发不互通。一旦部署成多 worker就必须引入 Redis 作为 channel layer 的 backendCHANNEL_LAYERS { default: { BACKEND: channels_redis.core.RedisChannelLayer, CONFIG: { hosts: [(127.0.0.1, 6379)], }, }, }这是在项目上线前就必须预见到的架构问题。4. 实战中的高频坑常见问题与排查技巧4.1 端口占用与开发服务器启动失败python manage.py runserver报Error: That port is already in use这大概是出现频率最高的启动报错了。解决思路很简单先看什么进程占用了 8000 端口然后换端口跑。Windows 下用netstat -ano | findstr 8000 taskkill /PID 你的进程ID /FmacOS / Linux 下用lsof -i :8000 kill -9 进程ID如果真的只是临时换个端口直接python manage.py runserver 8080就行Django 支持指定端口跑不用改配置文件。另外提醒一点千万别一边开着runserver的自动重载一边手动重启另一个实例这会直接导致端口被两个进程抢占报错信息还会特别难懂。4.2 迁移命令怎么用才能不丢数据数据库操作是整个开发流程里最需要谨慎的部分。makemigrations只生成迁移文件不落库migrate才真正执行变更。比如你给Article模型加了一个字段运行makemigrations之后Django 会问你要为新字段提供一个默认值。如果你选的字段类型是CharField还强制要求设定default或者nullTrue这时候别偷懒填了个临时默认值就完事要结合业务场景想清楚。如果模型改得比较多报迁移冲突时不要一上来就migrate --fake。--fake是告诉 Django“别管数据库现状假装迁移过”这命令在需要对齐历史记录时确实有用但滥用的话会让数据库表和迁移记录彻底失去同步后面再迁移时各种报错排查成本极高。最稳妥的顺序是python manage.py makemigrations python manage.py migrate如果本地开发还没上线遇到迁移冲突宁可把数据库删了重来也别硬着头皮用--fake扔到生产环境里。4.3 静态文件 404开发环境与生产环境的差异这个坑我敢说每个用 Django 写过项目的人都遇到过。CSS、图片、JS 加载不出来页面光秃秃的。分两种情况排查开发环境DEBUGTrue下确认文件放到了STATICFILES_DIRS指定的目录模板里用了{% load static %}引用路径是{% static css/style.css %}。注意static标签会自动拼接STATIC_URL不要写成硬编码的/static/css/style.css。生产环境DEBUGFalse下Django 不会再主动服务静态文件需要手动执行python manage.py collectstatic把散落在各 app 的静态文件全部收集到STATIC_ROOT目录然后交由 nginx 之类的反向代理服务器处理。如果你直接拿runserver跑生产配置通常会看到 CSS 全部 404这不是代码错而是服务器角色没分清楚。4.4 CSRF 校验失败与跨域问题写全栈项目时Django 的表单提交默认会校验 CSRF Token前端如果不带这个 token提交表单就直接报CSRF token missing or incorrect。解决方案取决于你的页面怎么渲染如果你的页面是 Django 模板渲染的表单里加{% csrf_token %}就行。如果你用 Vue 或 React 做前后端分离接口走 AJAX可以在获取页面时把 token 放到 Cookie 里然后前端请求时带上X-CSRFToken请求头。至于跨域问题Django 默认不开启跨域资源共享。如果你的前端跑在 3000 端口、后端跑在 8000 端口浏览器会因为跨域直接拦掉接口响应。常规做法是安装django-cors-headersINSTALLED_APPS [ corsheaders, ... ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, ... ] CORS_ALLOWED_ORIGINS [ http://localhost:3000, ]CorsMiddleware 的放置顺序很关键官方文档明确要求放在 CommonMiddleware 之前并且要放在能处理响应的中间件前面。顺序不对的话跨域响应头不会正确附加到响应里前端依然会被浏览器拦截。4.5 排查经验速查表我整理了一份工作中最常用的排查对照表直接收藏照着查就行症状可能原因处理方案runserver 报端口占用另一个服务进程占用了 8000用 lsof/netstat 查看进程并 kill或换端口启动启动报 ModuleNotFoundError虚拟环境未激活或依赖未装确认当前终端处于 venv 环境重新 pip install django数据库迁移提示 No changes detectedapp 没有注册进 INSTALLED_APPS检查 app 是否已加入配置列表迁移出现冲突记录多人开发产生的迁移文件冲突makemigrations --merge合并静态文件加载不出文件路径错误或未执行 collectstatic检查 STATICFILES_DIRS 与 STATIC_ROOT生产环境跑 collectstaticCSRF 校验失败表单未携带 token或跨域携带方式错误模板加 csrf_tokenAJAX 带 X-CSRFToken 请求头WebSocket 频繁断开使用了 runserver 而非 daphne/uvicorn改用 daphne 启动 ASGI 应用时间字段差了 8 小时USE_TZ 为 True 且使用了 datetime.now()改用 django.utils.timezone.now()4.6 调试模式下的最后一个压箱底技巧最后分享一个我几乎每天都在用的调试技巧配置LOGGING时把 SQL 打出来。当接口返回的数据不对劲但又不知道是代码问题还是数据问题看一眼 ORM 实际生成的 SQL 就全明白了LOGGING { version: 1, handlers: { console: { class: logging.StreamHandler, }, }, loggers: { django.db.backends: { level: DEBUG, handlers: [console], }, }, }配置好之后每次跑 ORM 查询终端都会打印出实际执行的 SQL。这不仅帮你确认 Django 内部是怎么翻译 ORM 的还能顺便排查 N1 查询问题——当你发现明明只查了 10 条数据控制台却打印了 20 条 SQL 时就该考虑上select_related或者prefetch_related了。这个技巧看起来不起眼但排查线上数据库查询问题时真的能救命。尤其全栈项目后期性能优化阶段SQL 日志能直接告诉你哪里多查了、哪里没走索引比对着代码猜效率高一个量级。我做 Django 项目这几年最大的体会就是配置从来不是一次写完就一劳永逸的它会随着项目从开发到部署的演进不断调整。所以不用追求“一步到位”把每个配置的用途吃透遇到问题能迅速定位到 settings 里的那几行就已经超过绝大多数半路出家的开发者了。希望这篇配置拆解和实战踩坑清单能让你在启动下一个全栈项目时少走几段弯路。