1. 为什么我把赌注压在Django上全栈框架的选型逻辑1.1 全家桶不是笨重是一套完整的工作流Python做Web开发Flask和Django是两条绕不开的路线。我最早从Flask入门当时觉得Django太笨重一个项目初始化就生成十几个文件哪像Flask一个脚本就能跑起服务。直到接手一个真正的企业级后台系统——多用户权限、内容管理、数据报表、定时任务、实时通知全都要有——才意识到Django的全能恰恰是它最大的竞争力。Django官网把自己的理念概括为batteries included翻译过来就是电池都给你配好了。你用Flask搭一个带登录功能的站要自己装Flask-Login、Flask-SQLAlchemy、Flask-WTF、Flask-Migrate还要花时间保证这些第三方库之间的版本兼容。而Django把Admin后台、ORM、Auth认证、表单处理、模板引擎、迁移机制全部内置版本由Django官方统一维护升级一个包就能同步拿到所有安全补丁。这个差别在写个人博客时感受不明显但团队协作、企业交付、长期维护的时候差距是全方位的。我见过太多团队在Flask项目里被技术债追着跑换了负责人就想换ORM换了ORM又要改业务层再加上测试用例不齐全重构等于重写。Django的约定优于配置项目结构是框架定的新人接手后光看目录就知道路由写在哪儿、模型定义在哪个文件、模板放在哪个文件夹这种确定性在Web开发里非常值钱。1.2 什么项目适合Django什么项目别硬上Django先说结论后台管理系统、内容管理平台、数据可视化平台、电商系统的服务端、企业内部的运营工具这类前后端一体或者后端职责重的项目Django是当前Python生态里最稳的选择。尤其如果你要交付一个带Admin可维护后台的系统Django自带的Admin在初期搭建效率上是任何Flask方案都追不上的。你还没有写一行业务代码数据模型一同步增删改查界面就有了。但Django也不是万能钥匙。如果你只是做一个极轻量的API服务比如给小程序提供几个简单接口请求量不大、业务逻辑几乎没有你用Django确实会觉得杀鸡用牛刀。这时候Flask或FastAPI更合适。再比如你已经上了微服务架构每个服务只负责单一职责Django的全家桶优势发挥不出来反而是它的体积和启动速度成了负担。选型这件事没有非黑即白核心看两点第一项目生命周期长不长如果注定要长期迭代Django的生态红利会越来越明显第二团队成员对框架的熟练度一个团队如果都熟Flask硬切Django的沟通成本也很高。我的做法是核心业务平台用Django边缘服务用Flask或FastAPI各得其所。2. 环境搭建与项目骨架从Python安装到第一个app的完整记录2.1 Python版本与虚拟环境这一步偷懒后面全是坑Django的版本策略比较激进新特性只在小版本里迭代。截至我写这篇文章的时间Django 4.2和5.0是主流生产版本对Python版本有明确要求4.2支持Python 3.8到3.125.0要求Python 3.10以上。我个人的建议是别用Python 3.7以下的老版本也别追最新的Python 3.13测试版老老实实装3.10或3.11兼容性和性能都稳妥。很多新手第一步就栽在环境上。系统自带的Python版本很可能是老的直接全局安装Django会污染系统环境后面装别的项目依赖时各种冲突。所以虚拟环境是必须的。我一般用Python自带的venv不额外装virtualenv# 创建虚拟环境 python3 -m venv myenv # 激活Linux/macOS source myenv/bin/activate # 激活Windows myenv\Scripts\activate # 在虚拟环境中安装Django pip install django如果你用的是VSCode写Python记得把解释器指向虚拟环境里那个Python。在VSCode里按CtrlShiftP输入Python: Select Interpreter选择myenv/bin/python。这样终端和编辑器用的都是同一个环境省掉无数我明明装了为什么import报错的烦恼。pip下载慢的问题就一句话配置国内镜像源。在用户目录下的pip.confLinux/macOS或pip.iniWindows里写上[global] index-url https://pypi.tuna.tsinghua.edu.cn/simple实测下载速度能快一个数量级Django这种大包几秒就完事。2.2 startproject与startapp项目和应用到底啥关系很多新手搞不清project和app的区别。简单说一个project是一个完整的网站/项目一个app是project里的一个功能模块。比如你做一个博客平台整个平台是project而用户管理、文章管理、评论管理分别可以做成独立的app。这种拆分方式让代码职责清晰也方便后续复用。创建项目用django-admin命令# 在指定目录下创建项目 django-admin startproject mysite # 进入项目目录 cd mysite # 创建博客应用 python manage.py startapp blog创建完后目录结构是这样的mysite/ ├── manage.py # 项目管理入口所有命令都通过它执行 ├── mysite/ # 项目配置文件目录 │ ├── __init__.py │ ├── settings.py # 全局配置 │ ├── urls.py # 根路由 │ ├── asgi.py # ASGI入口WebSocket/异步 │ └── wsgi.py # WSGI入口传统同步 └── blog/ # 刚创建的app ├── __init__.py ├── admin.py # Admin后台配置 ├── apps.py # app配置信息 ├── models.py # 数据模型 ├── tests.py # 测试文件 └── views.py # 视图函数这里有个很容易忽略的步骤新创建的app并不会自动被项目认识你必须把它注册到settings.py的INSTALLED_APPS列表里。很多新手创建完app直接开始写models一运行migrate发现表没建回头一看app压根没注册。我习惯每创建一个app就立刻注册养成肌肉记忆。# mysite/settings.py INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, blog, # 新加的app注册在这里 ]2.3 settings.py里的关键配置项动手改之前先搞懂含义settings.py是Django项目的神经中枢改动任何一行之前都要知道它的含义。我重点说一下新手最容易踩的三个坑。第一个是SECRET_KEY。这个密钥用于加密session、生成密码哈希、防止CSRF伪造是项目的最高机密。默认生成的密钥是开发用的千万不要硬编码在代码仓库里尤其别推到GitHub公开仓库。正确做法是用环境变量import os SECRET_KEY os.environ.get(DJANGO_SECRET_KEY, development-insecure-key)第二个是DEBUG。开发阶段设True生产环境一定改成False。很多人上线忘了关直接把报错堆栈暴露给用户相当于把服务器底裤晾给别人看。DEBUGFalse还有个连带影响不再提供静态文件服务此时需要单独配置静态文件收集和部署这个后面讲部署时会详细说。第三个是DATABASES。默认配置是SQLite零依赖就能跑做学习和开发验证完全够用。但要上生产你最好换成PostgreSQL或MySQL。切换配置也很简单DATABASES { default: { ENGINE: django.db.backends.postgresql, NAME: mydb, USER: myuser, PASSWORD: mypassword, HOST: 127.0.0.1, PORT: 5432, } }我建议新手一开始就用PostgreSQL别在SQLite上写完再迁移。虽然Django的ORM帮你屏蔽了大部分数据库差异但有些字段类型、索引、查询行为的细节开发和生产环境不一致就可能在测试时一切正常、上线后随机翻车。3. Django ORM的发动机模型设计、查询与删除的底层逻辑3.1 模型设计字段选择和关系映射决定业务边界Django ORM把数据库表抽象成了Python类字段就是类的属性。比如一篇文章模型from django.db import models from django.contrib.auth.models import User class Article(models.Model): title models.CharField(max_length200) slug models.SlugField(uniqueTrue) content models.TextField() author models.ForeignKey(User, on_deletemodels.CASCADE, related_namearticles) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) status models.CharField( max_length10, choices[(draft, 草稿), (published, 发布)], defaultdraft, ) class Meta: ordering [-created_at] def __str__(self): return self.title字段类型的选择直接影响数据库层面的存储和索引效率。CharField必须指定max_length数据库里对应的是varcharTextField对应text适合大段内容。SlugField本质是带有unique约束的CharField适合URL友好的标识。ForeignKey的on_delete参数是最容易踩坑的地方。CASCADE表示关联的User删除时他写的所有文章也被删。PROTECT表示有文章未删除时用户删不掉数据库会抛ProtectedError。还有SET_NULL需要配合nullTrue使用用户删了文章还在author变成NULL。我建议从业务语义出发选文章数据是核心资产我一般用PROTECT不允许前置条件未满足时就销毁数据。定义好模型后记得执行两条命令# 生成迁移文件相当于数据库变更的说明书 python manage.py makemigrations # 应用到数据库真正执行建表/改表 python manage.py migratemakemigrations可以在不连接数据库的情况下生成迁移文件提交到代码仓库里让团队成员同步migrate则在本地数据库执行。我踩过的一个坑是多人协作时大家各自改了模型却不生成迁移文件最后合并代码时冲突让人头大。所以习惯上谁改模型谁负责提交迁移文件。3.2 QuerySet的惰性求值为什么查询没有立即执行Django ORM最容易被新手误解的点就是QuerySet的惰性求值。你以为写了一句查询代码数据库就立刻跑了一遍其实没有。我来演示一下articles Article.objects.filter(statuspublished) # 这一行不查数据库 print(articles[0]) # 取到具体对象时才执行查询 print(articles.count()) # count时报错没有count()方法不对count会执行查询精确来说QuerySet在迭代、切片、布尔判断、list()转换、调用count()、exists()等方法时才会真正执行SQL。这个设计的好处是方便链式调用比如articles Article.objects.filter(author__usernameadmin) articles articles.filter(statuspublished) articles articles.order_by(-created_at)三条链式条件拼在一起最终只生成一条SQL语句返回避免了多次访问数据库。但惰性也是一把双刃剑最大的坑是N1查询问题。假设你要在模板里列出所有文章和作者名# 视图里 articles Article.objects.filter(statuspublished)模板里循环时每取一篇文章的article.author.username就会执行一次对auth_user表的查询。100篇文章就是101次数据库查询性能立刻崩。解决办法是select_relatedarticles Article.objects.filter(statuspublished).select_related(author)select_related通过SQL的JOIN把关联表一次查出来就解决N1了。多对多关系用prefetch_related原理是先查主表再查关联表在内存里做合并。这个优化几乎是所有Django项目的必修课你只要看到页面响应变慢第一反应就先去查SQL日志看有没有循环查询。3.3 删除对象的多条路径delete()、QuerySet.delete()与软删除热搜词里有个django执行查询-删除对象说明大家对这个操作有疑问。Django里删除对象有几种方式各有讲究。单个对象删除用delete()方法article Article.objects.get(pk1) article.delete()这段代码返回一个元组(删除的总行数, 具体计数信息)。它会删除当前对象如果这个对象有外键关联的子对象就会触发级联删除。比如上面Article的作者外键没有配置子表但如果有一个Comment表外键指向Article并设置了on_deletemodels.CASCADE删除文章会很干净评论一起没了。这种级联是数据库层面的行为不是Django伪造的。批量删除用QuerySet的delete()Article.objects.filter(statusdraft).delete()这里有个隐藏陷阱批量删除不会调用模型的delete()方法也不会触发pre_delete和post_delete信号。所以如果你在模型的delete()里做了额外逻辑比如记录日志、清理关联文件批量删除时这些逻辑不会执行。需要在信号处理器里处理才可靠。再聊一个实际工程里常见的需求软删除。业务系统通常不希望数据被物理删除而是给记录打个标记让它从列表里消失但保留在数据库。实现方式常见的是给模型加个is_deleted字段class Article(models.Model): # ...其他字段 is_deleted models.BooleanField(defaultFalse) class Meta: # 全局默认查询先生效排除已删除的数据 # 需要在 Manager 中自定义这里略过 pass然后自定义一个Manager重写get_queryset方法过滤掉is_deletedTrue的记录。这样平时Article.objects.all()查不到已删除的数据但Article.all_objects.all()可以全量查询。软删除的实际操作比看起来复杂需要考虑唯一约束、关联数据完整性、回收站恢复等场景但它是生产系统的必修课。4. 请求响应链路URL、视图与模板如何形成完整闭环4.1 URLconfpath()与反向解析的套路Django收到一个HTTP请求后第一步是进入根urls.py通过urlpatterns列表从上到下匹配请求路径。核心配置示例# mysite/urls.py from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(blog/, include(blog.urls)), ]在blog/urls.py里定义业务路由from django.urls import path from . import views urlpatterns [ path(, views.article_list, namearticle_list), path(int:pk/, views.article_detail, namearticle_detail), path(int:pk/delete/, views.article_delete, namearticle_delete), ]int:pk是路径参数这里用了类型转换器只匹配整数。Django 2.0之后推荐用path()而不用正则的re_path()因为path()更简洁也足够满足大多数需求。除了int还有str不包含斜杠的字符串、slug字母数字中划线、uuid等转换器。为什么路由一定要起name因为模板和视图里到处要用。比如模板里的链接a href{% url article_detail pkarticle.id %}查看详情/a这个叫反向解析。好处是URL结构变化时只要路由里的path改了模板里的链接自动跟上不用一个个改硬编码的URL。如果多个app都有同名路由就要用命名空间# blog/urls.py app_name blog urlpatterns [...]模板里写{% url blog:article_detail pkarticle.id %}彻底避免冲突。我见过很多项目上线后把URL路径改了结果一堆外部链接和书签全失效反向解析的意识还是得从一开始就有。4.2 函数视图还是类视图各就各位总有人问我Django到底是写函数视图还是类视图。我的风格是按场景区分。简单页面操作比如返回一个模板渲染结果函数视图最直观from django.shortcuts import render from .models import Article def article_list(request): articles Article.objects.filter(statuspublished) return render(request, blog/article_list.html, {articles: articles})但如果要做ListView、DetailView这类CRUD页面类视图帮你省掉大量样板代码。比如文章详情页from django.views.generic import DetailView from .models import Article class ArticleDetailView(DetailView): model Article template_name blog/article_detail.html context_object_name article代码量少到令人发指。而且类视图自带访问权限控制、表单处理、成功跳转等扩展点适合快速构建标准页面。我用类视图处理通用CRUD用函数视图处理业务逻辑复杂的动作比如注册、支付回调、导出报表之类。两者混用完全没问题Django不会限制你选边站。一个重要提醒视图里写业务逻辑时不要把所有代码堆在视图函数里。视图只负责接收请求、调用业务、返回响应真正复杂的逻辑最好放在独立的service模块或模型方法里。否则你的视图会膨胀成一坨不好测试也不好维护的意大利面条。4.3 模板的继承与上下文数据传递Django模板引擎自带一套继承机制它的灵活程度让我在迁移到其他框架时总会想念。基模板base.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 title{% block title %}默认标题{% endblock %}/title /head body div classcontainer {% block content %} {% endblock %} /div /body /html子模板article_list.html{% extends base.html %} {% block title %}文章列表{% endblock %} {% block content %} {% for article in articles %} h2a href{% url blog:article_detail pkarticle.pk %}{{ article.title }}/a/h2 p{{ article.created_at|date:Y-m-d }}/p {% empty %} p暂无文章。/p {% endfor %} {% endblock %}模板更擅长做的事情是展示数据而不是算数据。很多新手在模板里写复杂判断和参数传递看着头大。我的原则是模板里只保留for循环、if判断、{% url %}反向解析、简单的过滤器任何需要计算或者访问字段关联的复杂逻辑都放到视图或模板上下文处理器里。上下文处理器值得一提。比如你希望所有页面都能显示当前用户信息不用每个视图都手动传一遍user。Django内置了django.contrib.auth.context_processors.auth它会把user注入到所有模板的上下文中。想加入全局数据也可以自己写上下文处理器注册到TEMPLATES的context_processors里。5. 登录与会话Cookie与Token在生产环境中的正确姿势5.1 Django Auth系统的正确用法与扩展Django内置的django.contrib.auth模块提供了一套完整的用户认证方案包括User模型、登录、注销、密码哈希、权限系统。新手最容易犯的错误是项目还没跑起来就想着我要不要自己去设计用户表。我的建议是从头开始就用AbstractUser来自定义用户模型。默认的User模型用username做登录标识字段是写死的。如果你一开始没自定义等上线后想加个phone字段、想改用邮箱登录迁移过程会非常痛苦。正确做法是项目一开始就做自定义from django.contrib.auth.models import AbstractUser from django.db import models class CustomUser(AbstractUser): phone models.CharField(max_length20, blankTrue) avatar models.URLField(blankTrue)然后在settings.py里注册AUTH_USER_MODEL users.CustomUser注意这一步一定要在第一次migrate之前配好否则数据库里已经有原版User表再切换自定义User模型就会各种冲突。这是Django项目里唯一一个开局就必须决定的配置。登录操作的核心是authenticate和loginfrom django.contrib.auth import authenticate, login def login_view(request): if request.method POST: username request.POST[username] password request.POST[password] user authenticate(request, usernameusername, passwordpassword) if user is not None: login(request, user) return redirect(home) else: # 登录失败提示 passauthenticate会检查用户凭证并返回User对象login负责把用户状态写入session。这一套流程官方封装得很稳不需要自己折腾会话管理。5.2 手动设置Cookie的细节max_age、httponly与SameSiteDjango默认会话是通过session实现的但有时我们需要手动操作Cookie比如保存用户偏好、追踪来源、存token。设置Cookie的方式是一行代码response HttpResponse(ok) response.set_cookie( token, valueyour_token_value, max_age3600 * 24 * 7, # 7天后过期单位是秒 httponlyTrue, # 禁止JavaScript读取防XSS窃取 secureTrue, # 仅HTTPS传输 samesiteLax, # 控制第三方请求携带Cookie )这里面每个参数背后都是血的教训。httponlyTrue是关键如果把token放在Cookie里又不设httponly前端一个document.cookie就能把它捞走站点的安全防线基本形同虚设。samesite属性用于防御CSRF攻击我推荐Lax作为默认——GET跨站请求可以带上Cookie但POST这类跨站请求会拦截兼顾用户体验和安全。还有一个细节删除Cookie很多人以为设置一个空值就行。正确的做法是response.delete_cookie(token)delete_cookie会同时修改Cookie的过期时间为过去的时间点让浏览器立刻丢弃它。手动设置Cookie虽然简单但建议只在没有更好的替代方案时使用。现代Web应用里用户标识更推荐用专门的会话或token机制。5.3 前后端分离下的Token认证方案DRF与JWT怎么选如果你做的是前后端分离项目前端是Vue或React后端的登录状态不能再依赖服务端session因为API没有Cookie和Session的概念。此时主流方案是Token认证。Django生态里最常用的是django-rest-frameworkDRF。DRF自带的TokenAuthentication实现思路很简单用户登录成功后生成一个全球唯一的Token字符串存到数据库的authtoken_token表里返回给前端。前端每次请求在Authorization头里带上Token 你的token值。优点是好实现缺点是Token明文存在数据库里泄露后难以自动过期适合内部系统。更通用的方案是JWTJSON Web Token通过djangorestframework-simplejwt库实现。JWT是把用户信息加密签名后生成一段自包含的token字符串服务端不需要存token通过密钥验签即可。我推荐用simplejwt因为它的实现成熟支持access token/refresh token分离能够控制有效期。配置好DRF后在settings.py里加权限控制REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: [ rest_framework_simplejwt.authentication.JWTAuthentication, rest_framework.authentication.SessionAuthentication, ], DEFAULT_PERMISSION_CLASSES: [ rest_framework.permissions.IsAuthenticated, ], }这里我加了SessionAuthentication保留后台管理API的兼容性。JWT的密钥复用SECRET_KEY所以环境变量管理好JWT就不会出大问题。聊点踩坑心得JWT一旦签发在过期前无法主动吊销所以用户改密码后旧token还是有效的。解决思路是设置较短的access token有效期比如15分钟到1小时用refresh token去续签将变动纳入可接受的窗口期。6. 实时推送实战WebSocket如何把后台数据实时送到前端6.1 从轮询到长连接实时通信的演进路线传统HTTP协议是请求-响应模式前端想要新数据只能去问服务器。如果页面需要实时展示后台的数据变化比如新订单提醒、消息通知、大盘数据刷新最简单的做法是前端定时轮询接口每秒或每几秒问一次。轮询实现简单但有两个硬伤一是延迟永远存在间隔期内数据变化了你不知道二是不论数据有没有变化服务器都收到一堆请求浪费资源和流量。WebSocket把通信模式反转了浏览器和服务器之间建立一条长连接连接建立后服务器可以主动把数据推给浏览器。这样延迟几乎为零而且不需要前端反复请求。Django 3.0之后引入了ASGI支持在此基础上配合Channels库可以在一个Django项目里同时跑HTTP和WebSocket。如果你只是需要在某个页面做实时推送Channels就是标准的答案。关于django websocket实现后台有数据前端推送我下面给出一套完整的实现流程。6.2 ASGI与Channels的核心概念Consumer、Channel Layer与Redis传统Django跑在WSGI上同步工作模式。要支持WebSocket这种长连接你需要ASGI异步服务器网关接口。Channels把WebSocket连接封装成了一个个Consumer对象你可以理解成处理一条连接的应用。安装步骤pip install channels channels-redis安装完成后把channels加到INSTALLED_APPS然后修改项目的asgi.pyimport os from django.core.asgi import get_asgi_application from channels.routing import ProtocolTypeRouter, URLRouter from channels.auth import AuthMiddlewareStack import blog.routing os.environ.setdefault(DJANGO_SETTINGS_MODULE, mysite.settings) application ProtocolTypeRouter({ http: get_asgi_application(), websocket: AuthMiddlewareStack( URLRouter( blog.routing.websocket_urlpatterns ) ), })AuthMiddlewareStack负责在WebSocket连接里也能像普通请求那样访问request.user这个对多用户实时推送非常关键。然后是Consumer的核心逻辑。Consumer是处理WebSocket消息的地方它包含几个生命周期方法connect处理连接建立、receive处理收到前端消息、disconnect处理断开连接。下面是一个接收后台数据并推送给前端的Consumer# blog/consumers.py import json from channels.generic.websocket import AsyncWebsocketConsumer class NotificationConsumer(AsyncWebsocketConsumer): async def connect(self): # 建立一个房间组把当前用户加入进去 self.user self.scope[user] self.group_name fuser_{self.user.id} 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): # 收到前端发来的消息可以做处理也可以不处理 text_data_json json.loads(text_data) message text_data_json[message] # 把消息回发给同组用户 await self.channel_layer.group_send( self.group_name, { type: send_notification, message: message, } ) async def send_notification(self, event): # 从channel layer收到事件向WebSocket客户端发消息 await self.send(text_datajson.dumps({ message: event[message], }))这里的channel_layer就是位于Redis的通信层它充当一个中间枢纽可以把消息广播给同一组的多个连接。异步Consumer用await关键字执行I/O操作不阻塞其它连接适合高并发场景。6.3 前后端实时推送的完整链路从Redis到浏览器WebSocket的路径需要单独路由所以在blog/routing.py里定义from django.urls import path from . import consumers websocket_urlpatterns [ path(ws/notifications/, consumers.NotificationConsumer.as_asgi()), ]前端JavaScript的WebSocket客户端长这样const socket new WebSocket(ws:// window.location.host /ws/notifications/); socket.onmessage function(e) { const data JSON.parse(e.data); // 把收到的消息渲染到页面 document.getElementById(notification-list).innerHTML li data.message /li; }; socket.onclose function(e) { console.error(WebSocket连接关闭); };后端业务代码里什么场景需要推送消息比如订单状态变化、新用户注册、某个异步任务完成。你可以在处理业务的地方调用channel_layer发送消息from channels.layers import get_channel_layer from asgiref.sync import async_to_sync def notify_user(user_id, message): channel_layer get_channel_layer() async_to_sync(channel_layer.group_send)( fuser_{user_id}, { type: send_notification, message: message, } )这里要注意一点async_to_sync是在同步代码里调异步channel_layer的桥接工具。如果整个函数本身就定义成async直接await即可不需要bridge。我踩过的坑是在同步view里调用channel_layer时不加async_to_sync运行时报错说group_send返回的是一个coroutine对象消息没发出去还很难排查。生产环境中WebSocket服务需要使用支持ASGI的服务器比如Daphne或Uvicorn后面第7章会讲部署选型。开发调试时python manage.py runserver已经内置了ASGI支持直接就能玩起来。7. 从开发到生产部署、性能优化与Django Unfold带来的体验升级7.1 生产服务器选型Gunicorn还是DaphneNginx该怎么配开发环境用runserver没问题但它是一个轻量级开发服务器性能、安全性都不适合生产环境。传统同步Django应用最经典的部署组合是Nginx Gunicorn Django。Nginx负责处理静态文件、反向代理Gunicorn运行Python代码。# 安装gunicorn pip install gunicorn # 启动命令4个worker进程根据服务器CPU核数调整 gunicorn mysite.wsgi:application --workers 4 --bind 127.0.0.1:8000如果你用了Channels、跑WebSocket就要换成ASGI服务器。Django官方推荐Daphne它本身就支持HTTP和WebSocket协议。pip install daphne daphne -b 127.0.0.1 -p 8000 mysite.asgi:application你也可以选择Uvicorn性能比Daphne更好但WebSocket特性兼容性要仔细测试。Nginx配置的核心是转发HTTP请求转发给Django静态文件直接由Nginx处理WebSocket请求需要升级协议头。下面是一段我常用的最小配置server { listen 80; server_name example.com; # 静态文件 location /static/ { alias /path/to/mysite/static/; } # WebSocket代理关键Upgrade头 location /ws/ { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; } # 其它请求转给Django location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }部署前还有两个必做步骤一是python manage.py collectstatic把分散在各app的静态文件集中收集到一个目录让Nginx统一serve二是DEBUG置False同时配置允许的域名ALLOWED_HOSTS比如[example.com]。不配置ALLOWED_HOSTS的话线上访问会直接给你抛一个DisallowedHost异常。7.2 索引、缓存与连接池把响应时间压下来的三板斧Django项目上线后数据库的响应速度往往最先成为瓶颈。第一板斧是加索引。在模型字段上设置db_indexTrue或者用Meta.indexes定义联合索引class Article(models.Model): # ... class Meta: indexes [ models.Index(fields[status, created_at]), ]第二板斧是使用Django缓存框架。Django支持把缓存放在内存、数据库、文件系统或Redis里最常见的是Redis。配置好后你可以对某个查询结果做缓存from django.core.cache import cache def get_article_list(): articles cache.get(article_list) if articles is None: articles Article.objects.filter(statuspublished).select_related(author) cache.set(article_list, articles, timeout300) return articles关键是timeout设置要符合业务场景。比如文章列表几分钟变化一次设置300秒用户信息变化频繁就别缓存或者缩短到30秒。缓存也要考虑失效更新问题最粗暴但也实用的方式是数据更新时cache.delete(article_list)。第三板斧是使用数据库连接池。Django默认每次请求创建连接、请求结束断开连接频繁的连接重建对高并发场景不友好。使用连接池有一个问题是官方文档没细讲的连接池里的连接可能因为数据库重启而失效需要给池子设置心跳检测或自动重连。我用的是dj-database-url配合连接池配置生产环境明显改善。这三板斧做完大多数中小型项目的响应时间都能从几百毫秒降到几十毫秒级别。更深的优化比如数据库慢查询定位、分库分表、消息队列异步化那就是另一个篇章了。7.3 Django Unfold让后台管理界面不再土很多开发者不太愿意用Django自带Admin理由是界面风格停留在上个时代。如果你也有这个感受强烈建议试试django-unfold。它是一个现代化UI组件库风格的Django Admin主题安装十分简单pip install django-unfold在INSTALLED_APPS里把unfold放在django.contrib.admin前面INSTALLED_APPS [ unfold, # 必须放在admin前面 django.contrib.admin, # ...其它app ]安装后重启项目Admin后台的界面就变成了带侧边导航、卡片式布局的现代风格。Unfold还支持暗黑模式、自定义主题颜色、列表页搜索表单的增强等特性。对于交付给客户的系统这个UI升级带来的第一印象提升非常明显。在admin.py里注册模型不复杂但可以做很多增强配置from django.contrib import admin from .models import Article admin.register(Article) class ArticleAdmin(admin.ModelAdmin): list_display (title, author, status, created_at) list_filter (status, created_at) search_fields (title, content) ordering (-created_at,)list_display控制列表页显示的列list_filter提供侧边栏筛选search_fields生成搜索框。这些Admin配置在生产系统中很有价值运营团队可以直接在后台管理数据不需要额外开发管理页面。配合Unfold的美化后台管理体验已经接近商业SaaS产品的水平。最后想补的一段话说点个人体会。Django入门的门槛不算低尤其是刚接触Web开发的新手面对project、app、settings、迁移、Admin、信号、类视图等一系列概念容易在头两周就劝退。我自己的经验是不要试图一次弄懂所有概念先跑通一个最小的完整链路——创建项目、建一个app、定义一个模型、写一个视图、渲染一个模板——然后再回头补每个环节的细节。你会发现Django的设计高度自洽很多看似复杂的机制其实是在帮你解决真实工程中的共性问题。后续如果再深入可以研究Django的中间件机制、信号系统、Celery异步任务、REST API设计、Docker容器化部署每个方向都能再写几篇长文。但先把这篇里的环境、ORM、视图、认证、WebSocket、部署这条主线走通你已经具备独立交付一个企业级Web项目的能力了。实操中最受益的一件事是随手把遇到的问题和解决方案记下来下次碰到相同报错五分钟就能定位。祝你在Django的路上少踩坑多产出。