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/PostgreSQL
Django 开发环境默认用 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 = True,Django 会在数据库里存 UTC 时间,在渲染时换算成本地时间。这里有个容易踩的坑:如果你在代码里用datetime.datetime.now()获取当前时间,得到的是 UTC 时间,不是北京时间。想拿本地时间,应该用django.utils.timezone.now(),或者干脆引入zoneinfo手动指定时区。
静态文件配置也是一个高频问题区域:
STATIC_URL = 'static/' STATICFILES_DIRS = [BASE_DIR / 'static'] STATIC_ROOT = BASE_DIR / 'staticfiles'STATIC_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_length=200, verbose_name='标题') content = models.TextField(verbose_name='内容') created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') def __str__(self): return self.title模型里字段类型的选择不能随意:短文本用CharField,长文本用TextField,日期用DateTimeField。如果一开始字段类型定错,后面数据量大了再改类型,迁移成本会非常高。
查询和删除是日常操作里最频繁的场景,我直接列一些实战代码:
# 查询单个对象 article = Article.objects.get(id=1) # 查询不存在会抛 DoesNotExist,多条会抛 MultipleObjectsReturned # 所以如果确定只有一条,用 get;不确定就用 filter().first() # 条件查询 articles = Article.objects.filter(title__contains='Django') # 排序 latest_articles = Article.objects.order_by('-created_at') # 删除单个对象 article.delete() # 批量删除符合条件的所有对象 Article.objects.filter(created_at__year=2022).delete() # 注意 delete() 返回的是一个元组 deleted_count, detail = Article.objects.filter(id__gt=10).delete() print(deleted_count)这里必须提醒一个关键点:delete()的级联行为。如果你有外键关联,比如 Article 和 Comment 是一对多关系,删除 Article 时默认会级联删除所有关联的 Comment。这是 Django 默认的on_delete=models.CASCADE行为。如果业务上不允许连带删除,可以在外键字段上换用on_delete=models.PROTECT,这样有子记录时删除父记录会报ProtectedError,从根源上避免误删。
还需要注意性能问题:filter().delete()是数据库层面的批量删除,不会触发模型里的delete()方法,所以如果你重写了该方法并期望它执行,比如写入日志、清理缓存,批量删除时是不会执行的。遇到这种业务场景,得手动遍历删除:
for obj in Article.objects.filter(created_at__year=2022): obj.delete()单删和批删的语义差异,是 ORM 里非常容易踩坑的细节。
3.3 登录态与接口安全:Cookie 与 Token 设置
全栈开发意味着你既要页面,也要接口。登录状态的保持就需要考虑 Cookie 和 Token 的配合。Django 自带的 Session 框架支持将 session 数据存到数据库、缓存、或者文件里,配合 Cookie 使用。
最简单的做法是利用 Django 内置的 session:
# 写入 def login_view(request): user = authenticate(username='xxx', password='xxx') 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 的跨域问题会让前端开发人员抓狂。这时候我会选择 JWT(JSON 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, httponly=True, samesite='Lax', secure=False, # 生产环境记得改成 True,走 HTTPS )SameSite属性也很重要:Lax允许同站请求携带 Cookie,但跨站 POST 请求不带;Strict则更严格。对于登录态保护来说,Lax是在安全性和易用性之间不错的平衡点。生产环境务必加secure=True,否则 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()), ]) ), })然后写一个最基础的 consumer:
from 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_data=json.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 的 backend:
CHANNEL_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=''或者null=True,这时候别偷懒填了个临时默认值就完事,要结合业务场景想清楚。
如果模型改得比较多,报迁移冲突时,不要一上来就migrate --fake。--fake是告诉 Django“别管数据库现状,假装迁移过”,这命令在需要对齐历史记录时确实有用,但滥用的话会让数据库表和迁移记录彻底失去同步,后面再迁移时各种报错,排查成本极高。
最稳妥的顺序是:
python manage.py makemigrations python manage.py migrate如果本地开发还没上线,遇到迁移冲突,宁可把数据库删了重来,也别硬着头皮用--fake扔到生产环境里。
4.3 静态文件 404:开发环境与生产环境的差异
这个坑我敢说每个用 Django 写过项目的人都遇到过。CSS、图片、JS 加载不出来,页面光秃秃的。分两种情况排查:
开发环境(DEBUG=True)下,确认文件放到了STATICFILES_DIRS指定的目录,模板里用了{% load static %},引用路径是{% static 'css/style.css' %}。注意static标签会自动拼接STATIC_URL,不要写成硬编码的/static/css/style.css。
生产环境(DEBUG=False)下,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-headers:
INSTALLED_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 detected | app 没有注册进 INSTALLED_APPS | 检查 app 是否已加入配置列表 |
| 迁移出现冲突记录 | 多人开发产生的迁移文件冲突 | makemigrations --merge合并 |
| 静态文件加载不出 | 文件路径错误或未执行 collectstatic | 检查 STATICFILES_DIRS 与 STATIC_ROOT,生产环境跑 collectstatic |
| CSRF 校验失败 | 表单未携带 token,或跨域携带方式错误 | 模板加 csrf_token,AJAX 带 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 的,还能顺便排查 N+1 查询问题——当你发现明明只查了 10 条数据,控制台却打印了 20 条 SQL 时,就该考虑上select_related或者prefetch_related了。
这个技巧看起来不起眼,但排查线上数据库查询问题时真的能救命。尤其全栈项目后期性能优化阶段,SQL 日志能直接告诉你哪里多查了、哪里没走索引,比对着代码猜效率高一个量级。
我做 Django 项目这几年最大的体会就是:配置从来不是一次写完就一劳永逸的,它会随着项目从开发到部署的演进不断调整。所以不用追求“一步到位”,把每个配置的用途吃透,遇到问题能迅速定位到 settings 里的那几行,就已经超过绝大多数半路出家的开发者了。希望这篇配置拆解和实战踩坑清单,能让你在启动下一个全栈项目时少走几段弯路。