☰
Django视图选型与生产部署:FBV/CBV取舍及Nginx+uWSGI实战
2026/10/11 0:53:54 网站建设 项目流程

很多人写Django视图,第一反应是“能用就行”。FBV随手写个函数、return一句render,项目跑起来也算顺顺利利。可一旦业务复杂起来,同一个列表页要分页、要筛选、要权限控制,你再从头手写一遍逻辑,写到第三遍就忍不住想骂人了。这时候CBV的好处才真正体现出来。

反过来,也有人一上来就啃CBV,被ListView、DetailView、ModelForm、mixins这些概念砸得头晕,连get_queryset、get_context_data都还没搞清楚,就在类里瞎override,结果视图逻辑跑偏都不知道去哪查。最后退回FBV,老实写自己的if-else,反而清爽了。

我的态度很明确:FBV和CBV没有绝对的优劣,它们是两种思考模型。函数视图是面向过程的控制流,类视图是面向对象的职责切分。第五篇就把这两套东西掰开揉碎讲清楚,再往后半程推进,讲真正上线时Nginx+uWSGI怎么配合,怎么把这套开发好的Django项目从runserver里搬出来,部署到生产环境里稳稳跑起来。这篇适合已经能独立写出Django项目的朋友,也适合刚学完基础、准备把第一个项目真正发布上线的开发者。

1. FBV与CBV的取舍:不只是写法差异

1.1 FBV的核心优势:逻辑直接、调试直观

FBV就是函数视图,本质上就是一个接收HttpRequest对象、返回HttpResponse对象的普通函数。它最大的优点是什么?代码透明。你看到什么,就是什么。

比如一个最简单的登录视图:

# views.py from django.contrib.auth import authenticate, login from django.shortcuts import render, redirect def login_view(request): if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') user = authenticate(request, username=username, password=password) if user is not None: login(request, user) return redirect('dashboard') else: error = '用户名或密码错误' return render(request, 'accounts/login.html', {'error': error}) return render(request, 'accounts/login.html')

这个函数从请求进来到响应出去,每一条分支都写得清清楚楚,没有任何“隐藏逻辑”。出了问题,你在函数里加一行print,或者用调试器打断点,直接就能看到是哪个if分支走错了。副本少、流程直、心智负担低,这是FBV最大的价值。

我实际工作中,凡是那种“一个页面只干一件事”的场景,比如单页表单提交、简单的动态跳转、需要精细控制响应内容的小接口,我都直接用FBV。不是因为不会写CBV,而是因为FBV在简单场景下的信噪比实在太高了。

1.2 CBV的核心优势:复用性强、结构规范

CBV(Class-Based Views)的底层逻辑是把视图的各个处理环节拆分成独立的可继承方法,然后由Django在请求进来时按照固定的生命周期去调用它们。

拿ListView举例,一个典型的分页列表页,用CBV写起来是这样:

# views.py from django.views.generic import ListView from .models import Article class ArticleListView(ListView): model = Article template_name = 'blog/article_list.html' context_object_name = 'articles' paginate_by = 20 def get_queryset(self): # 只展示已发布文章,按创建时间倒序 return Article.objects.filter(status='published').order_by('-created_at')

表面上看,代码量比FBV少很多,但真正的价值不在“少写几行”,而在Django帮你兜住了复杂场景。分页逻辑不用你自己算page、has_next还是has_previous,模板里直接用Django内置的Page对象;queryset的过滤有独立的get_queryset方法;上下文默认帮你带上object_list和paginator。你只需要关注“这个列表要查什么数据、显示哪个模板”,剩下的流程都被封装好了。

类视图的可复用性优势也更明显。比如你有一个文章列表和一个问答列表,它们都有分页、都有筛选,只是数据模型不同,那你就可以抽一个公共的基类,把分页和筛选逻辑放在基类里,子类只改model。这在FBV里需要复制粘贴或者自己封装函数,代码组织起来远不如类继承优雅。

CBV还有一套完整的方法分派机制,这是它的核心原理:

# URL配置里这样写 path('articles/', ArticleListView.as_view()),

**as_view()类方法干了什么?**它把类实例化后绑定到闭包函数view上,当请求到达时,view函数根据request.method调用类上对应的同名方法——GET请求走get()方法、POST请求走post()方法。这些方法又各自调用get_queryset()、get_context_data()等钩子方法,形成一套完整的处理流程。

1.3 什么时候必须用CBV,什么时候一定不要用

结合这些年的项目经验,我给一个务实的判断标准:

判断维度推荐FBV推荐CBV
视图逻辑复杂度逻辑特殊、分支多、难以抽象典型CRUD、列表、详情、表单
代码复用需求一次性视图、各视图间差异大多个视图共用流程,只是数据源不同
调试难度函数直接可断点,流程直观涉及父类调用,需要理解MRO
学习成本低,会写函数就会写中高,需要理解基类、mixin、继承
与DRF配合可用@api_view装饰器高度契合,ViewSet天然配合
团队规范适合快速迭代、原型阶段适合长期维护、多人协作的中大型项目

**什么时候一定不要用CBV?**我印象很深的一个场景:某个视图要根据用户角色、时间、产品类型做多维度权限判断,不同角色返回完全不同的页面结构。我把这逻辑硬塞进DetailView的get()方法里,最后为了适配通用流程,override了一堆方法,代码比FBV还长,别人根本看不懂。后来重构直接改成FBV,加几个helper函数,逻辑一下就通透了。

**混合使用完全没有问题。**Django官方也不要求你二选一。我的习惯是:视图函数就像一块乐高底板,能拼就拼——列表页、详情页、表单处理页这类明确有固定流程的场景用CBV,其他哪怕稍微偏离通用流程的场景就用FBV。一个项目里两种风格并存,只要注释写清楚,team里的人都认可,反而比强行统一风格更高效。

2. 生产部署选型:为什么是Nginx+uWSGI

2.1 开发环境的runserver为什么不能直接上生产

很多新手学Django的时候一直在用python manage.py runserver,觉得项目跑得好好的,为什么部署上线还要搞Nginx+uWSGI这么麻烦?

原因很简单:runserver不是一个为生产环境设计的服务器。它的定位是开发调试工具,默认是单进程、单线程、同步处理请求,实测并发能力很差。当同时有几十上百个请求打进来时,runserver会触发warning提示你“不要在生产环境使用这个服务器”。更重要的是,runserver在开发时自动加载静态文件、自动reload代码,这会带来严重的性能开销,也不安全。

生产环境的Django需要一个真正的WSGI服务器来承接请求,还要一个高性能的Web服务器来处理静态资源和反向代理。这正是Nginx+uWSGI组合的价值所在。

2.2 uWSGI在整条链路中扮演什么角色

先理清一个概念:uWSGI是一个WSGI服务器。WSGI(Web Server Gateway Interface)是Python Web应用和Web服务器之间的接口协议。Django项目本身不监听端口,它只是一个遵循WSGI协议的应用程序对象(项目里的wsgi.py文件暴露出来的application)。

uWSGI做的事,是在底层加载并运行这个application,把来自前端的HTTP请求转成WSGI环境让你写好application去处理,再把结果转回HTTP响应返回。

uWSGI有几个核心优势:

  • 多进程模型:通过processes参数启动多个worker进程,每个worker进程都加载一个完整的Django应用实例,能同时处理多个请求,突破了单进程的性能瓶颈。
  • 线程支持:在worker内部再用threads参数开多个线程,线程间共享进程内存,适合处理I/O密集型的请求。
  • 进程管理:master进程负责监控worker状态,worker挂了自动拉起,还支持优雅重启、热加载配置。
  • 与Python生态无缝集成:天然支持虚拟环境、支持多种Python版本、支持异步插件,部署配置灵活。

一条典型的生产链路是这样运作的:

用户浏览器发起HTTP请求 → Nginx接收请求 → Nginx把动态请求通过uWSGI协议转发给uWSGI → uWSGI调用Django application处理请求 → 返回HTTP响应 → Nginx把响应返回给浏览器。

Nginx在“最前面”承接所有用户的请求,但它不懂Python,也不直接运行Python代码。它把需要Python处理的动态请求交给后端的uWSGI,同时自己承担静态文件服务、安全防护、负载均衡等Nginx擅长的活儿。

2.3 Nginx的五大职责

Nginx在整个部署架构里至少承担五个关键职责:

一是处理静态文件。Django项目里的CSS、JS、图片等静态资源不需要经过Python处理,直接由Nginx读取磁盘文件返回,性能远超Django自己处理静态文件。Nginx处理静态文件的高效是其天然优势,能扛住高并发。

二是反向代理。用户的请求先进Nginx,再由Nginx决定转发给哪个后端服务。这让Nginx可以在多个Django应用实例之间做负载均衡,也能在同一台服务器上通过不同域名或端口代理多个不同的Web应用。

三是SSL终结。HTTPS证书配置在Nginx上,SSL握手、加密解密这些吃CPU的活儿由Nginx完成,后端uWSGI走内网HTTP,不再承受加解密开销。

四是请求缓冲与超时管理。Nginx可以缓冲客户端上传的数据,防止后端被慢速请求拖死;也可以设置代理超时时间,避免后端处理过慢时前端无限等待。

五是安全过滤。可以通过deny、allow限制IP访问,通过limit_conn限制并发连接数,通过配置防护常见的恶意请求。虽然不能替代WAF,但基础的防护能省心不少。

3. 完整部署实操:从安装到配置

3.1 环境准备与依赖安装

先准备好环境。我用的是一台Ubuntu 22.04服务器,Python 3.10,Django 4.2。如果你的系统版本不同,命令略有差异,但整体思路一样。

第一步,创建虚拟环境并安装依赖:

# 在项目目录外创建虚拟环境 python3 -m venv venv source venv/bin/activate # 安装依赖 pip install django==4.2 uwsgi

推荐把依赖列表记录在requirements.txt里,方便以后重建环境:

pip freeze > requirements.txt

第二步,安装Nginx:

sudo apt update sudo apt install nginx

安装完Nginx会自动启动,默认监听80端口。你可以先访问服务器的IP地址,看到Nginx的欢迎页说明安装成功。

第三步,确认Django项目的settings.py已经为生产做好了准备:

# settings.py DEBUG = False ALLOWED_HOSTS = ['www.example.com', 'example.com'] STATIC_ROOT = BASE_DIR / 'staticfiles'

DEBUG=False后,Django不再处理静态文件请求,必须配置好STATIC_ROOT,后面执行collectstatic收集。ALLOWED_HOSTS一定要配置,否则任何请求都会被拦截,这在生产环境是重要的安全校验。

在项目目录下执行:

python manage.py collectstatic --noinput

3.2 uWSGI配置文件详解

我用过命令行启动uWSGI,也用过ini文件配置,最终强烈推荐用ini或conf文件来管理配置。命令行参数一长串,重启服务器后很难维护,配置文件则一目了然。

在项目根目录创建uwsgi.ini:

[uwsgi] # 项目目录 chdir = /srv/www/myproject # Django的wsgi模块 module = myproject.wsgi:application # 通过socket与Nginx通信 socket = /srv/www/myproject/myproject.sock # 虚拟环境路径 home = /srv/www/venv # 进程与线程 master = true processes = 4 threads = 2 # 以nginx用户运行,避免权限问题 uid = www-data gid = www-data # 退出时自动清理临时文件 vacuum = true # 日志 logto = /var/log/uwsgi/myproject.log

这里几个参数重点解释一下:

  • socket:uWSGI与Nginx之间通信用的Unix socket文件路径,相比TCP端口方式更安全、性能更好。你也可以写成socket = 127.0.0.1:8001用TCP方式,不过Unix socket更推荐。
  • processes:worker进程数,一般设置为CPU核心数的2倍左右。我在4核服务器上就设为4。进程数不是越大越好,每个进程都要加载一份Django应用,内存占用会明显上升。
  • threads:每个worker内的线程数。当你的视图里有数据库查询、外部API调用这类I/O等待,线程能提高并发利用率。设得过高反而会造成CPU上下文切换开销。
  • master=true:启动master进程来管理所有worker,这是生产环境的标配。worker挂掉会被master自动重启。
  • vacuum=true:uWSGI退出时自动删除socket文件,避免下次启动时因为上次残留的socket文件导致bind失败。
  • uid/gid:切换运行用户。把worker进程从root降到www-data运行,这是重要的安全实践。生产环境下永远不要让应用以root身份运行。

启动uWSGI:

uwsgi --ini uwsgi.ini

想确认是否正常启动,看日志文件,里面有spawned uWSGI worker字样就说明worker成功起来了。

3.3 Nginx配置详解

接下来配置Nginx站点。在/etc/nginx/sites-available/下创建一个配置文件,名字和项目名对齐,方便管理:

server { listen 80; server_name www.example.com example.com; # 请求体大小限制,按需调整 client_max_body_size 50M; # 静态文件 location /static/ { alias /srv/www/myproject/staticfiles/; } # 上传媒体文件 location /media/ { alias /srv/www/myproject/media/; } # 动态请求转发给uWSGI location / { include uwsgi_params; uwsgi_pass unix:/srv/www/myproject/myproject.sock; uwsgi_read_timeout 60s; uwsgi_send_timeout 60s; uwsgi_connect_timeout 10s; } }

include uwsgi_params是Nginx与uWSGI通信的关键一步。这行代码把uWSGI协议需要的请求头参数(如HTTP头、请求体、请求方法等)填充进来,随后uWSGI才能正确解析出WSGI环境变量。这个文件位于Nginx的配置目录下,通常不需要手动修改。

然后创建软链接、测试配置并重载:

sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx

nginx -t这一步一定要做,它会检查整个Nginx配置语法是否正确。配置有错就直接reload,很可能把服务搞挂,先测一遍能省很多麻烦。

3.4 开机自启:用systemd托管uWSGI

uWSGI不能手动启动完就不管了,服务器重启后进程不会自动启动。生产环境用systemd托管服务是标准做法。

创建/etc/systemd/system/uwsgi.service:

[Unit] Description=uWSGI instance to serve myproject After=network.target [Service] User=www-data Group=www-data WorkingDirectory=/srv/www/myproject ExecStart=/srv/www/venv/bin/uwsgi --ini uwsgi.ini Restart=always RestartSec=5 [Install] WantedBy=multi-user.target

设置开机自启并启动服务:

sudo systemctl daemon-reload sudo systemctl enable uwsgi sudo systemctl start uwsgi

这里有个细节要注意:uwsgi.ini里指定的uid和gid,与systemd服务里指定的User、Group,要保持一致。我在一次部署中systemd用www-data运行,但ini里没有指定uid,默认全是root起的worker,结果静态文件权限是够的,但后面日志切割时碰到一堆权限问题。后来统一为www-data,经济又省心。

3.5 静态文件与多站点部署的经验

生产部署阶段最容易踩的坑就集中在静态文件上。第一,STATIC_ROOT是collectstatic的输出目录,不是你自己放源代码静态文件的地方。你的CSS、JS源文件应该放在各app的static/目录下,collectstatic会把它们全部汇总到STATIC_ROOT指向的目录。第二,如果你修改了静态文件,必须重新执行collectstatic,否则Nginx读的还是旧文件。我通常会写一个重启脚本,把collectstatic和重启uWSGI放在一起:

#!/bin/bash source /srv/www/venv/bin/activate cd /srv/www/myproject python manage.py collectstatic --noinput sudo systemctl restart uwsgi sudo systemctl reload nginx

同一台服务器上部署多个Django项目也很常见。每多一个项目,就多一个uWSGI实例(不同socket文件)和一个Nginx站点配置(不同server_name或不同端口),Nginx的sites-available和sites-enabled天生就支持这种多站点的组织方式。只要各自的本地开发环境用不同端口访问,虚拟机的Nginx反向代理就能根据域名把请求分发到对应站点。

4. 部署中的常见问题与排查实录

4.1 502 Bad Gateway:第一反应查uWSGI

502是部署Django+Nginx最常遇到的问题,几乎每个人都遇到过。它表示Nginx无法访问到后端的uWSGI服务,或者uWSGI没有正常工作。

排查顺序我一般是这样:

第一步,确认uWSGI是否在运行:

ps aux | grep uwsgi

如果没有进程,说明uWSGI挂了。看日志:

tail -f /var/log/uwsgi/myproject.log

第二步,确认socket文件权限。uWSGI以www-data用户创建socket文件后,Nginx也要有权限读这个socket。检查socket文件的所有者:

ls -l /srv/www/myproject/myproject.sock

如果socket文件属于root而Nginx是www-data,就会502。解决办法是确保uWSGI进程以www-data运行,或者chown www-data:www-data socket文件。

第三步,测试uWSGI本身是否正常工作。最直接的办法是用curl --unix-socket myproject.sock http://localhost/直接访问socket。如果Django能正确响应,说明uWSGI没问题,问题就出在Nginx配置上。

4.2 静态文件404或样式丢失

静态文件404的常见原因:

  • 没执行collectstatic,STATIC_ROOT目录是空的。
  • Nginx配置里alias路径写错,或者是root和alias混淆导致实际寻找的路径不对。
  • Nginx没有读取静态目录的权限。检查目录所属用户,chown -R www-data:www-data staticfiles。

一个屡见不鲜的困局是:DEBUG=True时Django能自动服务静态文件,一切正常;一旦DEBUG=False,Django就完全不处理静态文件了,如果Nginx配置没跟上,页面就变得“干干净净”。所以从开发切到生产时,第一步永远是collectstatic,再配Nginx的static location。

4.3 配置了HTTPS但证书不生效/替换后无效

这种情况我在网上看到过大量提问,自己实操中也遇到过。最常见的其实是缓存与重载逻辑的问题:

  • 修改Nginx配置后,忘了nginx -t然后systemctl reload nginx,或者只重启了服务但浏览器里缓存了旧的证书信息。
  • 证书文件路径写错了,或证书链不完整。Nginx会在启动时读取证书文件,如果证书链断在中间环节,浏览器就会报证书无效。
  • 需要注意Termination点的配置:SSL配置应该在监听443的server块里配置ssl_certificate和ssl_certificate_key,并把80端口的请求重定向到https。替换证书后,nginx -t测试通过后必须reload或restart才能生效,这个动作经常被忽略。

4.4 并发压力测试下性能不足与超时

uWSGI的processes/threads配置直接决定了Django的并发承载能力。如果上线后发现请求响应很慢,先看两个指标:CPU核数和内存大小,然后根据负载调整worker数量。

一个可参考的初始方案:

CPU核数内存推荐配置
2核2Gprocesses=2, threads=2
4核4Gprocesses=4, threads=2
8核8Gprocesses=6, threads=4

这个配置不是定死的,生产环境要用压测工具(如ab或wrk)实测调整。压测时先关注uWSGI的日志里有没有worker timeout或write error,这类错误往往说明worker处理不过来,需要增加进程数或提高harakiri超时时间。

Nginx侧的超时也要重视。反向代理默认的uwsgi_read_timeout是60s,但如果你的某个接口执行超过60s(比如导出一个大数据量的报表),Nginx会主动断开连接,前端收到504。解决办法是区分接口类型,千人千面的超时配置。动态接口60s没问题,但下载、导入导出类接口要单独设更长的超时时间。

Nginx自身也有连接数限制的概念。有人问“Nginx最大并发连接数老是用超”怎么办。受限于系统文件描述符上限,需要同时调整worker_processes、worker_connections和系统层面的ulimit。简单来说,Nginx允许的最大并发连接数大约是worker_processes × worker_connections,这个数还要低于系统对进程fd的限制,否则Nginx会报“too many open files”。排查时用ulimit -n看看进程文件描述符上限,再对应调server块里的worker_rlimit_nofile。

4.5 域名、反向代理与端口配置的常见困惑

本地开发时,你可能会用自定义域名指向127.0.0.1或虚拟机IP,然后在Nginx里按域名配置多个server块,让不同域名访问同一台机器上的不同应用。这套玩法放到生产环境其实一模一样,只是IP从127.0.0.1换成公网IP。

我见过很多新手在反向代理时出现混淆:Django内部的request.get_host()会被Nginx传递的Host头影响。如果你的Nginx用server_name接收了用户请求,转发给uWSGI时默认会保留原始Host头,Django也依赖这个头来校验ALLOWED_HOSTS。这时候如果某个域名没写进ALLOWED_HOSTS,Django会返回400。所以多域名部署时,所有真实访问的域名都要加进ALLOWED_HOSTS,不能偷懒只写一个IP。

结尾的几句心里话

这套Nginx+uWSGI的链路,我从第一次部署时被502折磨了两个小时,到现在基本能十分钟定位问题,中间踩过的坑都是实打实的教训。如果你问我哪一步最重要,我会说先把socket权限和日志链路搞清楚再谈别的。很多问题看起来千奇百怪,最后都能溯源到日志里的一行报错。部署前把Nginx的error.log和uWSGI的log路径都确认好,出了问题第一时间看日志,大部分谜题都能解开。

第五篇写到这里,FBV与CBV的取舍和Nginx+uWSGI生产部署这套实战链路都讲透了。下一篇我大概率会写Django的性能调优,包括数据库查询优化、缓存怎么接、异步任务怎么落地,这些都是项目上线之后真正会让你睡不着觉的问题,到时候再接着聊。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询