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.12,5.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里按Ctrl+Shift+P,输入"Python: Select Interpreter",选择myenv/bin/python。这样终端和编辑器用的都是同一个环境,省掉无数"我明明装了为什么import报错"的烦恼。
pip下载慢的问题就一句话:配置国内镜像源。在用户目录下的pip.conf(Linux/macOS)或pip.ini(Windows)里写上:
[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。很多人上线忘了关,直接把报错堆栈暴露给用户,相当于把服务器底裤晾给别人看。DEBUG=False还有个连带影响:不再提供静态文件服务,此时需要单独配置静态文件收集和部署,这个后面讲部署时会详细说。
第三个是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_length=200) slug = models.SlugField(unique=True) content = models.TextField() author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='articles') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) status = models.CharField( max_length=10, choices=[('draft', '草稿'), ('published', '发布')], default='draft', ) class Meta: ordering = ['-created_at'] def __str__(self): return self.title字段类型的选择直接影响数据库层面的存储和索引效率。CharField必须指定max_length,数据库里对应的是varchar;TextField对应text,适合大段内容。SlugField本质是带有unique约束的CharField,适合URL友好的标识。
ForeignKey的on_delete参数是最容易踩坑的地方。CASCADE表示关联的User删除时,他写的所有文章也被删。PROTECT表示有文章未删除时,用户删不掉,数据库会抛ProtectedError。还有SET_NULL,需要配合null=True使用,用户删了文章还在,author变成NULL。我建议从业务语义出发选:文章数据是核心资产,我一般用PROTECT,不允许前置条件未满足时就销毁数据。
定义好模型后,记得执行两条命令:
# 生成迁移文件(相当于数据库变更的"说明书") python manage.py makemigrations # 应用到数据库(真正执行建表/改表) python manage.py migratemakemigrations可以在不连接数据库的情况下生成迁移文件,提交到代码仓库里让团队成员同步;migrate则在本地数据库执行。我踩过的一个坑是:多人协作时,大家各自改了模型却不生成迁移文件,最后合并代码时冲突让人头大。所以习惯上,谁改模型谁负责提交迁移文件。
3.2 QuerySet的惰性求值:为什么查询没有立即执行
Django ORM最容易被新手误解的点,就是QuerySet的惰性求值。你以为写了一句查询代码,数据库就立刻跑了一遍,其实没有。我来演示一下:
articles = Article.objects.filter(status='published') # 这一行不查数据库 print(articles[0]) # 取到具体对象时才执行查询 print(articles.count()) # count时报错:没有count()方法?不对,count会执行查询精确来说,QuerySet在迭代、切片、布尔判断、list()转换、调用count()、exists()等方法时才会真正执行SQL。这个设计的好处是方便链式调用,比如:
articles = Article.objects.filter(author__username='admin') articles = articles.filter(status='published') articles = articles.order_by('-created_at')三条链式条件拼在一起,最终只生成一条SQL语句返回,避免了多次访问数据库。但惰性也是一把双刃剑,最大的坑是N+1查询问题。假设你要在模板里列出所有文章和作者名:
# 视图里 articles = Article.objects.filter(status='published')模板里循环时,每取一篇文章的article.author.username,就会执行一次对auth_user表的查询。100篇文章就是101次数据库查询,性能立刻崩。解决办法是select_related:
articles = Article.objects.filter(status='published').select_related('author')select_related通过SQL的JOIN把关联表一次查出来,就解决N+1了。多对多关系用prefetch_related,原理是先查主表再查关联表,在内存里做合并。这个优化几乎是所有Django项目的必修课,你只要看到页面响应变慢,第一反应就先去查SQL日志,看有没有循环查询。
3.3 删除对象的多条路径:delete()、QuerySet.delete()与软删除
热搜词里有个"django执行查询-删除对象",说明大家对这个操作有疑问。Django里删除对象有几种方式,各有讲究。
单个对象删除用delete()方法:
article = Article.objects.get(pk=1) article.delete()这段代码返回一个元组(删除的总行数, 具体计数信息)。它会删除当前对象,如果这个对象有外键关联的子对象,就会触发级联删除。比如上面Article的作者外键没有配置子表,但如果有一个Comment表外键指向Article并设置了on_delete=models.CASCADE,删除文章会很"干净",评论一起没了。这种级联是数据库层面的行为,不是Django伪造的。
批量删除用QuerySet的delete():
Article.objects.filter(status='draft').delete()这里有个隐藏陷阱:批量删除不会调用模型的delete()方法,也不会触发pre_delete和post_delete信号。所以如果你在模型的delete()里做了额外逻辑(比如记录日志、清理关联文件),批量删除时这些逻辑不会执行。需要在信号处理器里处理才可靠。
再聊一个实际工程里常见的需求:软删除。业务系统通常不希望数据被物理删除,而是给记录打个标记,让它从列表里消失但保留在数据库。实现方式常见的是给模型加个is_deleted字段:
class Article(models.Model): # ...其他字段 is_deleted = models.BooleanField(default=False) class Meta: # 全局默认查询先生效,排除已删除的数据 # 需要在 Manager 中自定义,这里略过 pass然后自定义一个Manager,重写get_queryset方法过滤掉is_deleted=True的记录。这样平时Article.objects.all()查不到已删除的数据,但Article.all_objects.all()可以全量查询。软删除的实际操作比看起来复杂,需要考虑唯一约束、关联数据完整性、回收站恢复等场景,但它是生产系统的必修课。
4. 请求响应链路:URL、视图与模板如何形成完整闭环
4.1 URLconf:path()与反向解析的套路
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, name='article_list'), path('<int:pk>/', views.article_detail, name='article_detail'), path('<int:pk>/delete/', views.article_delete, name='article_delete'), ]<int:pk>是路径参数,这里用了类型转换器,只匹配整数。Django 2.0之后推荐用path()而不用正则的re_path(),因为path()更简洁也足够满足大多数需求。除了int,还有str(不包含斜杠的字符串)、slug(字母数字中划线)、uuid等转换器。
为什么路由一定要起name?因为模板和视图里到处要用。比如模板里的链接:
<a href="{% url 'article_detail' pk=article.id %}">查看详情</a>这个叫反向解析。好处是URL结构变化时,只要路由里的path改了,模板里的链接自动跟上,不用一个个改硬编码的URL。如果多个app都有同名路由,就要用命名空间:
# blog/urls.py app_name = 'blog' urlpatterns = [...]模板里写{% url 'blog:article_detail' pk=article.id %},彻底避免冲突。我见过很多项目上线后把URL路径改了,结果一堆外部链接和书签全失效,反向解析的意识还是得从一开始就有。
4.2 函数视图还是类视图:各就各位
总有人问我Django到底是写函数视图还是类视图。我的风格是按场景区分。简单页面操作,比如返回一个模板渲染结果,函数视图最直观:
from django.shortcuts import render from .models import Article def article_list(request): articles = Article.objects.filter(status='published') 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 lang="zh-CN"> <head> <meta charset="UTF-8"> <title>{% block title %}默认标题{% endblock %}</title> </head> <body> <div class="container"> {% block content %} {% endblock %} </div> </body> </html>子模板article_list.html:
{% extends 'base.html' %} {% block title %}文章列表{% endblock %} {% block content %} {% for article in articles %} <h2><a href="{% url 'blog:article_detail' pk=article.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_length=20, blank=True) avatar = models.URLField(blank=True)然后在settings.py里注册:
AUTH_USER_MODEL = 'users.CustomUser'注意:这一步一定要在第一次migrate之前配好,否则数据库里已经有原版User表,再切换自定义User模型就会各种冲突。这是Django项目里唯一一个"开局就必须决定"的配置。
登录操作的核心是authenticate和login:
from 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, username=username, password=password) if user is not None: login(request, user) return redirect('home') else: # 登录失败提示 passauthenticate会检查用户凭证并返回User对象,login负责把用户状态写入session。这一套流程官方封装得很稳,不需要自己折腾会话管理。
5.2 手动设置Cookie的细节:max_age、httponly与SameSite
Django默认会话是通过session实现的,但有时我们需要手动操作Cookie,比如保存用户偏好、追踪来源、存token。设置Cookie的方式是一行代码:
response = HttpResponse("ok") response.set_cookie( 'token', value='your_token_value', max_age=3600 * 24 * 7, # 7天后过期,单位是秒 httponly=True, # 禁止JavaScript读取,防XSS窃取 secure=True, # 仅HTTPS传输 samesite='Lax', # 控制第三方请求携带Cookie )这里面每个参数背后都是血的教训。httponly=True是关键,如果把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-framework(DRF)。
DRF自带的TokenAuthentication实现思路很简单:用户登录成功后,生成一个全球唯一的Token字符串,存到数据库的authtoken_token表里,返回给前端。前端每次请求在Authorization头里带上Token 你的token值。优点是好实现,缺点是Token明文存在数据库里,泄露后难以自动过期,适合内部系统。
更通用的方案是JWT(JSON 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.py:
import 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 = f'user_{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_data=json.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)( f'user_{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还是Daphne,Nginx该怎么配
开发环境用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_index=True,或者用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(status='published').select_related('author') cache.set('article_list', articles, timeout=300) 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的路上少踩坑,多产出。