第一次真正让 Django 进入我的工作流,是接一个在线预约管理系统。需求不复杂:后台录入服务项目、前台展示排期、用户登录后查看自己的预约记录。但看到要求里写着"同时有几千条数据要能快速筛选管理"和"管理后台要做得像个正经系统",我就知道 Flask 那种自由发挥的风格要吃亏了。Django 自带 Admin、自带头认证、自带 ORM,这些"自带"的东西在初期看不出优势,等需求堆上来就知道省了多少事。
这篇文章不是 Django 语法手册,而是一条从无到有把 Django 项目跑起来的实战路径。我会按我自己做项目的顺序,把一个完整的 Django 应用从初始化、模型设计、查询与删除、视图联动、认证与 token,到 WebSocket 实时推送、最终部署的关键点串起来。无论你是刚接触 Django 的新手,还是想补全某个环节(比如 cookies 设置 token、channels 实时推送)的老手,都可以直接参考对应章节。
1. 为什么说 Django 是"一个人撑起全栈"的最佳选择
1.1 Django 到底给了你什么
Django 常被称作"全家桶"框架,这不只是营销话术。它把 Web 开发中高频重复的模块全部内置了:
- ORM:不需要写原生 SQL 就能完成表结构设计、增删改查,还能在 MySQL 和 SQLite 之间无缝切换。
- Admin 后台:Django 的杀手锏功能。只要你定义了模型,后台会自动生成一套可登录、可筛选、可排序的管理界面,甚至在数据量不大时完全能当业务后台用。
- 认证系统:内置 User 模型、Session、Permission 和 Group。做登录、权限控制几乎开箱即用。
- URL 路由与模板引擎:自带模板继承与过滤机制,配合 Bootstrap 或 Vue 做前端渲染都行。
- 中间件与信号:从 CSRF 防护到请求钩子,Django 为你铺好了扩展点。
1.2 和 Flask、FastAPI 放在一起,怎么选
很多新手会在 Django、Flask、FastAPI 之间纠结。我的判断标准很简单:
| 框架 | 适合场景 | 短板 |
|---|---|---|
| Flask | 小型 API、微服务、完全掌控一切 | 大量业务逻辑要自己搭,容易失控 |
| FastAPI | 高并发 API、异步接口、实时数据服务 | 没有内置模板和 Admin,纯 API 取向 |
| Django | 完整业务系统、后台管理、数据密集型、快速迭代 | 重,同步模型处理高并发需额外调优 |
如果项目需要"一个后台管数据 + 一个前台展示业务 + 一个用户体系",Django 是最优先选择。毕竟 Admin 和 ORM 是经过十几年沉淀的,稳定性和开发效率都在那摆着。
1.3 版本选择:别追新,求稳
目前 Django 官方主打 4.x 与 5.x。个人建议是选择 LTS 长期支持版本(比如 Django 4.2 LTS 或 5.x 的 LTS,按你建项目时的稳定版本来定)。不要因为想"用新特性"就用 dev 版本,生产环境出问题没人替你兜底。
2. 从零创建 Django 项目与 App:骨架搭建实操
2.1 环境准备与项目初始化
先在项目目录里建一个虚拟环境,避免系统 Python 的包污染:
mkdir django_project && cd django_project python3 -m venv venv source venv/bin/activate # Windows 是 venv\Scripts\activate pip install django然后创建项目。注意django-admin startproject和python manage.py startapp是两回事——startproject生成的是整个站点配置,startapp生成的是一个业务模块。一个项目里可以有多个 App,每个 App 负责独立的一块业务。
django-admin startproject config . python manage.py startapp appointments我把项目名写成config而不是默认的django_project,是因为一个项目中配置文件、业务 App、静态文件夹混在一起很长,起一个简短的配置目录名,后面改起来顺手。执行完后的目录结构大概是这样:
config/ settings.py urls.py wsgi.py / asgi.py appointments/ models.py views.py admin.py migrations/ manage.py2.2 settings.py 里必须调整的几个点
新手初始化项目后第一件事就是python manage.py runserver,结果看到英文界面的欢迎页。但真正开始写业务前,先打开settings.py做三件事:
第一,注册 App。在INSTALLED_APPS列表里加入你刚才创建的 App 名称。很多人忘记这一步,导致后面python manage.py makemigrations时提示"没有检测到模型变化",排查半天才发现 App 没注册。
第二,调整时区和语言。
LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai'注意TIME_ZONE要配合数据库里的时间字段使用。如果建表以后才改时区,旧数据里的时间一般不会自动转换,需要额外处理。
第三,配置数据库。如果只是开发,默认的 SQLite 完全够用:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', } }但生产我建议换 MySQL 或 PostgreSQL,并且一定要把密码和主机地址通过环境变量读取,不要硬编码在文件里。后面部署时你就知道这个习惯多救命。
2.3 快速让 Admin 界面好看一点:django-unfold
Django 自带的 Admin 功能没问题,但 2024 年了还在用那套老界面,客户看了总觉得是上个时代的产物。想在不重写后台的前提下提升观感,可以直接装django-unfold:
pip install django-unfold在INSTALLED_APPS中把unfold放在django.contrib.admin之前:
INSTALLED_APPS = [ "unfold", "django.contrib.admin", # ... ]然后给管理后台的 models 加@admin.register装饰器时,样式会自动生效。我这套小型管理后台用django-unfold后,侧边栏、卡片、表单整体观感直接翻了一倍,几乎零成本。
2.4 修改默认后台 URL
config/urls.py里默认是/admin,如果你不想让后台路径暴露得太明显,可以改成一个难猜的路径:
from django.contrib import admin from django.urls import path urlpatterns = [ path('manage-panel/', admin.site.urls), ]顺便把 Django 默认的注册视图也开发出来。第一步跑通后,先别急着写代码,先在 Admin 里创建一个超管账号:
python manage.py createsuperuser python manage.py runserver 0.0.0.0:8000访问http://127.0.0.1:8000/manage-panel/,如果能登录,说明整个骨架已经活了。
3. ORM 模型设计与增删改查的心法
3.1 从"建表"到"迁移":理解 makemigrations 与 migrate
ORM 最大的优势是你用 Python 类描述数据,Django 替你翻译成数据库表。models.py里定义一个预约模型:
from django.db import models class ServiceItem(models.Model): name = models.CharField('服务名称', max_length=100) price = models.DecimalField('价格', max_digits=10, decimal_places=2) duration = models.PositiveIntegerField('时长(分钟)', default=30) is_active = models.BooleanField('是否上架', default=True) created_at = models.DateTimeField('创建时间', auto_now_add=True) class Meta: ordering = ['-created_at'] def __str__(self): return self.name定义完只是第一步,真正让数据库有这张表,必须执行两条命令:
python manage.py makemigrations python manage.py migratemakemigrations生成迁移文件,migrate把迁移文件翻译成 SQL 执行到数据库。这个过程新手经常懵,我把它们类比成"修改方案"和"施工队进场":makemigrations只是把所有改动记录在案,migrate才真正动数据库。所以每次都先跑前者,确认迁移文件内容无误后,再跑后者。
3.2 查询的艺术:all/filter/get/exclude 不只是语法
Django ORM 的查询是惰性执行的,链式调用的每一步只是构建 QuerySet,只有真正需要数据时才执行 SQL。这个特性在排查性能问题时很关键。
基础查询示例:
# 获取所有上架的服务 services = ServiceItem.objects.filter(is_active=True) # 按价格排序取前3 top3 = ServiceItem.objects.filter(is_active=True).order_by('-price')[:3] # 排除某些类型 available = ServiceItem.objects.exclude(name__contains='测试') # 精准获取单条 item = ServiceItem.objects.get(id=1)get有个脾气:结果必须唯一,查不到或查到多条直接抛 DoesNotExist 和 MultipleObjectsReturned。如果你只是"取一条,没有也无所谓",建议用.first()返回None,不会触发异常。
使用filter时,字段名后加__contains、__gte、__lte、__in是 Django ORM 的核心细节:
# 价格大于100且时长小于60分钟 items = ServiceItem.objects.filter(price__gt=100, duration__lt=60) # 名称包含的关键词 items = ServiceItem.objects.filter(name__icontains='洗') # id 在某个列表中 items = ServiceItem.objects.filter(id__in=[1, 2, 3])字段名__后面接的是查询表达式,这比写原生 SQL 直观多了。真要排查数据问题,我都是直接在python manage.py shell里逐行敲,比轮询看日志快得多。
3.3 删除对象:单条删除、批量删除与级联陷阱
删除在 Django ORM 里是个"看着简单、坑也不少"的操作。单个实例直接调用.delete():
item = ServiceItem.objects.get(id=1) item.delete()返回值是一个(total_deleted, detail_dict)元组。如果你只是在控制台里操作,看到1代表删除成功;如果看到大于 1,说明级联删除了关联数据。这点特别重要——Django 默认的 FK(外键)行为是on_delete=models.CASCADE,即删除主表数据会连带删除所有关联的外键数据。
class Appointment(models.Model): service_item = models.ForeignKey(ServiceItem, on_delete=models.CASCADE, related_name='appointments')如果我删掉一个ServiceItem,所有关联的Appointment也会一并消失。这通常不是业务上想要的效果。保险的做法是改成:
service_item = models.ForeignKey(ServiceItem, on_delete=models.PROTECT)PROTECT会让 Django 在你尝试删除仍有关联数据的ServiceItem时主动抛出 ProtectedError,提醒你先处理子表数据。只有确定"子数据必须随主数据一起消失"时才用 CASCADE。
批量删除则是直接对 QuerySet 调用.delete():
ServiceItem.objects.filter(is_active=False).delete()注意,批量删除不会触发模型里的delete()重写方法。如果你有pre_save/post_save信号或自定义清理逻辑,批量删除会绕过它们。要精确控制删除逻辑时,最好还是遍历实例逐个删,或手动执行清理脚本。
3.4 查询性能:select_related 与 prefetch_related 的差别
项目中如果模型有关联关系,查询很容易出现 N+1 问题。第一次查主表 10 条记录,每条记录访问外键字段时又产生一次数据库查询,总共 11 次 SQL。本人第一次做列表页时没注意这个,本地测试毫无感觉,数据量到三五千条时页面明显变慢,后台直接显示几百条 Query 日志。
解决办法是对外键和一对一关系使用select_related,对多对多和反向外键关系使用prefetch_related:
# 查询预约列表时,顺便把关联的服务项目一起查出来 appointments = Appointment.objects.select_related('service_item').all()这条语句会 JOIN 查询一次性拿回主表和关联表数据,后续访问appointment.service_item.name不再触发额外 SQL。数据量大、关联层级深时,这两种方法带来的性能提升是数量级的。
4. 视图、路由与模板:把数据变成页面
4.1 路由设计:不只是path()这么简单
Django 的 URL 通路由urls.py统一指派。写大型项目时,主urls.py应该只做入口分发,具体的 App 路由在各自目录下定义,再通过include引入:
from django.contrib import admin from django.urls import path, include urlpatterns = [ path('manage-panel/', admin.site.urls), path('api/', include('appointments.urls')), path('', include('appointments.urls')), ]App 内的urls.py可以按业务把 URL 分到不同模块:
from django.urls import path from . import views urlpatterns = [ path('services/', views.service_list, name='service_list'), path('services/<int:pk>/', views.service_detail, name='service_detail'), path('appointments/create/', views.create_appointment, name='create_appointment'), ]这里<int:pk>是 Django 的路径转换器,会把 URL 中的123自动转成 int 类型传给视图函数。类似的还有<str:slug>和<uuid:uuid>。路径转换器在 Django 2.0 之后就全面替代了旧的url()正则写法,看着清爽,也省心。
4.2 视图:函数视图与类视图怎么选
视图层可以使用函数视图(FBV)或类视图(CBV)。新手项目,我建议先用函数视图,因为它逻辑直观,想在哪里加 if 加 for 都不会被类方法挡路。
from django.shortcuts import render, get_object_or_404 from .models import ServiceItem def service_list(request): services = ServiceItem.objects.filter(is_active=True) return render(request, 'appointments/service_list.html', {'services': services}) def service_detail(request, pk): service = get_object_or_404(ServiceItem, pk=pk, is_active=True) return render(request, 'appointments/service_detail.html', {'service': service})当你熟悉之后,再换取 CBV 会体会到它处理泛型逻辑的便利。比如列表页、详情页、创建页,这些几乎全是重复代码,直接用ListView、DetailView、CreateView几行代码搞定:
from django.views.generic import ListView from .models import ServiceItem class ServiceListView(ListView): model = ServiceItem template_name = 'appointments/service_list.html' context_object_name = 'services' def get_queryset(self): return ServiceItem.objects.filter(is_active=True)一个原则:页面只有"读 + 展示"逻辑时用 CBV 最爽,页面有复杂的自定义业务动作时回到 FBV 更自由。两者混用在同一个项目里完全没问题。
4.3 模板继承:一步到位避免改 50 个页面
Django 的模板系统最核心的能力是继承。我通常建一个base.html,把导航栏、页头、页脚、CSS/JS 引入全部放进去,然后用{% block %}留出变化区:
<!DOCTYPE html> <html lang="zh-hans"> <head> <meta charset="UTF-8"> <title>{% block title %}默认标题{% endblock %}</title> </head> <body> <nav>{% include 'appointments/navbar.html' %}</nav> <main> {% block content %}{% endblock %} </main> </body> </html>子模板只需要写:
{% extends 'base.html' %} {% block title %}服务列表{% endblock %} {% block content %} <div class="row"> {% for service in services %} <div class="card"> <h3>{{ service.name }}</h3> <p>价格:{{ service.price }}</p> </div> {% empty %} <p>暂无可预约服务</p> {% endfor %} </div> {% endblock %}{% empty %}是 Django 模板的隐藏福利,当列表为空时显示指定内容,比在视图里先判断再传递上下文要省事。
4.4 Cookie 设置 Token:登录后如何正确种下凭证
Django 默认用 Session 认证,但 API 端或前后端分离项目里更常用 JWT 或自定义 Token。很多新手问"cookie 到底什么时候该设置,怎么设置令牌"。这里结合 Django 内置的set_cookie做一套完整实践:
前端发起登录请求后,后端验证通过,生成 Token(可以用 Django 内置的加密签名,也可以引入djangorestframework-simplejwt生成 JWT)。然后把 Token 写入 HttpResponse 的 cookie:
from django.http import JsonResponse def login_view(request): # 假设这里验证了账号密码,生成了 token_str token_str = generate_token_for_user(user) response = JsonResponse({'code': 200, 'message': '登录成功'}) response.set_cookie( 'auth_token', token_str, max_age=60 * 60 * 24 * 7, # 7天有效期 httponly=True, # 前端 JS 无法读取,防 XSS samesite='Lax', # CSRF 保护策略 secure=False, # 生产环境改 True,启用 HTTPS 传输 path='/' ) return response参数细节值得讲透:
max_age控制有效期,单位是秒。不设置的话 cookie 会被浏览器标记为会话 cookie,关闭浏览器就失效。httponly=True非常关键。它禁止 JavaScript 读取 Document.cookie,是防止恶意脚本偷取 Token 的第一道防线。samesite='Lax'控制跨站请求携带 cookie 的策略。同站正常请求不受影响,跨站 POST 时不携带 cookie,这对 CSRF 有防护作用。secure=True生产环境必须开,它保证 cookie 只在 HTTPS 连接下传输。
登录页面同时也要设计"退出登录"逻辑。要让 cookie 失效,最好的方式是覆盖同名 cookie 并把max_age设为 0:
response.delete_cookie('auth_token')注意delete_cookie需要和当初设置 cookie 时的path参数一致,否则可能删不掉。
如果是纯 API 模式,推荐直接上djangorestframework-simplejwt。它的优势在于 Token 默认有过期时间,不需要服务端存储会话状态,适合做无状态 API:
from rest_framework_simplejwt.tokens import RefreshToken def get_tokens_for_user(user): refresh = RefreshToken.for_user(user) return { 'refresh': str(refresh), 'access': str(refresh.access_token), }前端拿到 access token 后放在Authorization: Bearer <token>头里调用 API。若用 Swagger 调试,这个头就是标准操作。
4.5 从 Django 到前后端分离:什么时候用 DRF
如果页面需要给小程序、App 或多端提供数据,不能在模板里渲染。有两个选择:一是写普通 Django JSONResponse 视图,二是引入 Django REST Framework(DRF)。强烈建议数据接口上 DRF,哪怕只是返回三个字段,因为你很快就会发现需要序列化外键、嵌套关系、限流和权限控制。DRF 的ModelViewSet和ModelSerializer几乎能零代码生成标准 CRUD API。
5. WebSocket 实时推送:后台有数据,前端立即知道
5.1 为什么 Django 传统同步机制做不到"实时"
Django 默认用 WSGI 协议,它的模型是"请求-响应":客户端发起 HTTP 请求,服务端处理完后响应,连接就断开了。如果后台新增了一条数据,想让处于同一个页面的所有用户看到,轮询是笨办法,WebSocket 才是正解:客户端与服务端保持长连接,服务端可以随时主动往客户端的连接里推数据。
Django 的解决方案是引入channels,它让 Django 从 WSGI 扩展为 ASGI 模式,既支持传统同步视图,也支持异步 WebSocket。
5.2 channels 的安装与配置
首先安装必要的包:
pip install channels channels-redis然后在settings.py中注册channels,并指定 ASGI 应用:
INSTALLED_APPS = [ 'daphne', # ASGI 服务器,替换 runserver 默认服务器 'channels', # ...其他应用 ] ASGI_APPLICATION = 'config.asgi.application'同时配置 channel layers,这是 WebSocket 消息转发的"中间枢纽"。生产环境用 Redis:
CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { "hosts": [('127.0.0.1', 6379)], }, }, }开发环境如果没有 Redis,也可以使用 InMemory 层:
CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels.layers.InMemoryChannelLayer' } }但重启服务后通道信息会丢失,只建议调试时用。
5.3 编写 Consumer:处理连接、收消息、推送消息
在 App 里新增consumers.py。下面是一个"预约状态更新后实时通知前端"的例子:
import json from channels.generic.websocket import AsyncWebsocketConsumer class AppointmentConsumer(AsyncWebsocketConsumer): async def connect(self): self.group_name = 'appointment_notify' # 加入消息组 await self.channel_layer.group_add( self.group_name, self.channel_name ) await self.accept() async def disconnect(self, close_code): await self.channel_layer.group_discard( self.group_name, self.channel_name ) # 处理前端发来的消息(如果需要) async def receive(self, text_data): data = json.loads(text_data) # 可以在这里处理前端传来的指令 await self.send(text_data=json.dumps({ 'message': '服务器已收到消息' })) # 后台通过 group_send 会触发这个方法,把数据推给前端 async def appointment_updated(self, event): await self.send(text_data=json.dumps({ 'type': 'appointment_update', 'data': event['data'] }))这里的关键是消息组的概念。多个 WebSocket 连接可以加入同一个 group,后台只要往 group 里发一条消息,组内所有活跃连接都能收到。非常适合做"所有用户同步看到后台数据变更"。
5.4 同步视图里如何推送:async_to_sync 是关键
WebSocket Consumer 是异步的,但普通 Django 视图是同步的。在视图里如何触发推送?用async_to_sync包装:
from asgiref.sync import async_to_sync from channels.layers import get_channel_layer def update_appointment(request, pk): # ...业务逻辑,比如修改状态为已完成 appointment.status = 'completed' appointment.save() channel_layer = get_channel_layer() async_to_sync(channel_layer.group_send)( 'appointment_notify', { 'type': 'appointment_updated', 'data': { 'id': appointment.id, 'status': appointment.status, } } ) return JsonResponse({'code': 200})注意type字段的值是appointment_updated,它会自动映射到 Consumer 里的appointment_updated方法。这是 channels 消息路由的命名约定,形如xx.updated就会调用名为xx_updated的 async 方法。
5.5 前端 JavaScript 如何接收
前端不需要复杂操作,原生 WebSocket 就够了:
const ws = new WebSocket('ws://yourdomain.com/ws/appointments/'); ws.onmessage = function(e) { const data = JSON.parse(e.data); if (data.type === 'appointment_update') { // 例如更新页面上的状态标签 document.querySelector(`#appointment-${data.data.id}`).innerText = data.data.status; } }; ws.onclose = function() { // 断线后重连策略 setTimeout(() => { connectWebSocket(); }, 3000); };要在 Django 路由里注册 WebSocket 的 URL,需要建立routing.py:
from django.urls import re_path from . import consumers websocket_urlpatterns = [ re_path(r'ws/appointments/$', consumers.AppointmentConsumer.as_asgi()), ]然后在项目主asgi.py中用ProtocolTypeRouter分类路由:
import os from django.core.asgi import get_asgi_application from channels.routing import ProtocolTypeRouter, URLRouter from channels.auth import AuthMiddlewareStack import appointments.routing os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') application = ProtocolTypeRouter({ 'http': get_asgi_application(), 'websocket': AuthMiddlewareStack( URLRouter( appointments.routing.websocket_urlpatterns ) ), })整体链路是:后台保存数据 → 视图调用 group_send → Redis channel layer 转发 → Consumer 的 appointment_updated 方法执行 → 前端 onmessage 收到数据 → 页面局部更新。
5.6 channels 实际踩过的坑
channels 配置正常后,本地跑起来很容易遇到几个诡异现象:
- 连不上 WebSocket,报 404。多半是
routing.py的路径没有匹配上。re_path里的正则必须以^开头、$结尾,否则会匹配到不该匹配的路径。 - group 消息所有人收到多份。这通常是 Redis 复用旧连接导致的。检查
ASGI_APPLICATION是否配置正确,重启 Redis 和 Django 进程往往是解决办法。 - 开发服务器行为异常。
runserver默认走 WSGI,不处理 WebSocket。要么用daphne启动:
daphne config.asgi:application要么在settings.py中把daphne放入INSTALLED_APPS顶部,Django 就会用 Daphne 替换内置 runserver 的服务器,让本地开发同时支持 HTTP 和 WebSocket。
数据量大时如何加速:常见 ORM 优化清单
除了前面提到的select_related和prefetch_related,还有几个高频性能提升手段值得记录。
用.values()或.values_list()减少内存占用。如果你只需要查出一批字段做只读展示,不要再加载完整的模型实例。例如只取 ID 和名称:
pairs = ServiceItem.objects.values_list('id', 'name')构建一个[ (1, '清洗'), (2, '保养'), ... ]的元组列表。比all()加载完整对象占用的内存少得多,在后台导出等场景特别明显。
对经常参与查询的字段加数据库索引:
class Appointment(models.Model): status = models.CharField(max_length=20, db_index=True)加了索引后,filter(status='completed')在数据量大时会快很多。代价是写入时要更新索引,但业务场景下读多写少,值得。
用annotate在数据库中完成聚合,而不是拿到 Python 层循环:
from django.db.models import Count statistics = Appointment.objects.values('status').annotate( count=Count('id') )得到的每条数据形如{'status': 'completed', 'count': 12}。整个统计过程只产生一条 SQL,如果自己循环统计数据,性能会差一个数量级。
在查询中加only('id', 'name')指定需要的字段,配合.defer('description')延迟加载大字段,对列表页和导出场景优化也很有用。记住一点:ORM 不是银弹,凡是能下推给数据库的操作,就不要放在 Python 中做。
6. 部署上线:从 DEBUG=True 到生产环境的踩坑清单
6.1 环境变量与敏感信息隔离
上线第一步是把settings.py里的密钥抽成环境变量。用一个.env文件存储变量,并用python-decouple读取:
pip install python-decouplefrom decouple import config SECRET_KEY = config('SECRET_KEY') DEBUG = config('DEBUG', default=False, cast=bool) ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='').split(',') DATABASE_URL = config('DATABASE_URL', default='sqlite:///db.sqlite3')把.env加进.gitignore,防止泄露。哪怕只是个人项目,我也建议一开始就保持这个习惯,等真要多人协作时你会发现这个决定太正确了。
6.2 DEBUG=False 与静态文件
DEBUG=True时 Django 会自动从静态文件目录读取资源;DEBUG=False时不会,你必须手动收集静态文件并交给 Nginx 等静态服务器处理。
先执行:
python manage.py collectstatic然后把STATIC_ROOT指向收集目录:
STATIC_ROOT = BASE_DIR / 'staticfiles'Nginx 的配置里,把/static/路径指向staticfiles目录即可:
location /static/ { alias /path/to/django_project/staticfiles/; }这步部署时最容易出的问题是"后台页面样式全没了",原因基本是没跑collectstatic或 Nginx 的 alias 路径不对。
6.3 用 Gunicorn + Daphne 混合服务
Django 项目同时包含 HTTP API 与 WebSocket,部署时常采用 Gunicorn 服务 HTTP、Daphne 或 Uvicorn 服务 ASGI/WebSocket。很多云平台现在支持多端口,可以在容器里跑两个进程。最简单稳定的组合:
# HTTP 请求,用 Gunicorn 跑 WSGI gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 3 # WebSocket 流量,用 Daphne 跑 ASGI daphne config.asgi:application --bind 0.0.0.0:8001然后在 Nginx 里做分发:
upstream django_http { server 127.0.0.1:8000; } upstream django_ws { server 127.0.0.1:8001; } server { listen 443 ssl; server_name yourdomain.com; location / { proxy_pass http://django_http; } location /ws/ { proxy_pass http://django_ws; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } }WebSocket 转发时,Upgrade和Connection头是缺一不可的。少了这两个头,WebSocket 连接握手就一直失败。
6.4 集中式日志与错误告警
生产环境DEBUG=False后,任何 500 错误只会显示通用页面,日志默认输出到服务器标准输出。我习惯配一个简单的LOGGING:
LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'handlers': { 'console': { 'class': 'logging.StreamHandler', }, }, 'root': { 'handlers': ['console'], 'level': 'WARNING', }, }然后把 stdout 接入到 ELK 或云日志服务。别等出问题时才想起来居然没有日志可查。
6.5 上线前的安全检查清单
本人每次部署前会对着下面这个清单自查:
DEBUG必须是False。SECRET_KEY使用环境变量注入,不包含在任何代码仓库中。ALLOWED_HOSTS只包含域名,不用*。- 数据库连接使用强密码且只允许内网访问。
/admin路径尽量改掉,并启用两因素认证。- HTTPS 强制跳转,HTTP 端口 80 全部 301 到 443。
- 上传图片的媒体目录不能放在项目源码里,单独挂载。
每一条都是踩过别人或自己踩过的坑总结出来的。尤其是ALLOWED_HOSTS设成*,一旦有恶意请求传入,日志会被刷爆,CSRF 防护也会变得脆弱。
结尾:几句实在话
Django 是一个"越深入越省心"的框架。你用原生 SQL 可能半小时写出来的查询,它在迁移、序列化、后台管理三个层面替你省下的时间,绝对远超学习成本。但也正因为"自带太多",新手往往会在框架规则上栽跟头——最常见的坑无非三个:忘记了makemigrations、漏掉了INSTALLED_APPS、把 DELETE 的级联行为想得太简单。这篇文章里的章节顺序,就是我当时踩坑、看源码、查文档后沉淀下来的完整路径。
我个人的实际操作体会是:项目一开始就要把 ORM 模型的related_name和on_delete定义清楚,想清楚删除主数据时关联数据到底该怎样,再开始写视图。别急着追求上了多少新技术栈,先把 Django 自身的 URL、ORM、Admin、认证榨干,等到确实需要面向实时场景时再来引入 channels 和 WebSocket。如果你正卡在哪一步,照着对应章节跑一遍,大概率能直接过关。