很多人写Django视图第一反应是“能用就行”。FBV随手写个函数、return一句render项目跑起来也算顺顺利利。可一旦业务复杂起来同一个列表页要分页、要筛选、要权限控制你再从头手写一遍逻辑写到第三遍就忍不住想骂人了。这时候CBV的好处才真正体现出来。反过来也有人一上来就啃CBV被ListView、DetailView、ModelForm、mixins这些概念砸得头晕连get_queryset、get_context_data都还没搞清楚就在类里瞎override结果视图逻辑跑偏都不知道去哪查。最后退回FBV老实写自己的if-else反而清爽了。我的态度很明确FBV和CBV没有绝对的优劣它们是两种思考模型。函数视图是面向过程的控制流类视图是面向对象的职责切分。第五篇就把这两套东西掰开揉碎讲清楚再往后半程推进讲真正上线时NginxuWSGI怎么配合怎么把这套开发好的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, usernameusername, passwordpassword) 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的核心优势复用性强、结构规范CBVClass-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(statuspublished).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. 生产部署选型为什么是NginxuWSGI2.1 开发环境的runserver为什么不能直接上生产很多新手学Django的时候一直在用python manage.py runserver觉得项目跑得好好的为什么部署上线还要搞NginxuWSGI这么麻烦原因很简单runserver不是一个为生产环境设计的服务器。它的定位是开发调试工具默认是单进程、单线程、同步处理请求实测并发能力很差。当同时有几十上百个请求打进来时runserver会触发warning提示你“不要在生产环境使用这个服务器”。更重要的是runserver在开发时自动加载静态文件、自动reload代码这会带来严重的性能开销也不安全。生产环境的Django需要一个真正的WSGI服务器来承接请求还要一个高性能的Web服务器来处理静态资源和反向代理。这正是NginxuWSGI组合的价值所在。2.2 uWSGI在整条链路中扮演什么角色先理清一个概念uWSGI是一个WSGI服务器。WSGIWeb 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.10Django 4.2。如果你的系统版本不同命令略有差异但整体思路一样。第一步创建虚拟环境并安装依赖# 在项目目录外创建虚拟环境 python3 -m venv venv source venv/bin/activate # 安装依赖 pip install django4.2 uwsgi推荐把依赖列表记录在requirements.txt里方便以后重建环境pip freeze requirements.txt第二步安装Nginxsudo 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 / staticfilesDEBUGFalse后Django不再处理静态文件请求必须配置好STATIC_ROOT后面执行collectstatic收集。ALLOWED_HOSTS一定要配置否则任何请求都会被拦截这在生产环境是重要的安全校验。在项目目录下执行python manage.py collectstatic --noinput3.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这里几个参数重点解释一下socketuWSGI与Nginx之间通信用的Unix socket文件路径相比TCP端口方式更安全、性能更好。你也可以写成socket 127.0.0.1:8001用TCP方式不过Unix socket更推荐。processesworker进程数一般设置为CPU核心数的2倍左右。我在4核服务器上就设为4。进程数不是越大越好每个进程都要加载一份Django应用内存占用会明显上升。threads每个worker内的线程数。当你的视图里有数据库查询、外部API调用这类I/O等待线程能提高并发利用率。设得过高反而会造成CPU上下文切换开销。mastertrue启动master进程来管理所有worker这是生产环境的标配。worker挂掉会被master自动重启。vacuumtrueuWSGI退出时自动删除socket文件避免下次启动时因为上次残留的socket文件导致bind失败。uid/gid切换运行用户。把worker进程从root降到www-data运行这是重要的安全实践。生产环境下永远不要让应用以root身份运行。启动uWSGIuwsgi --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 nginxnginx -t这一步一定要做它会检查整个Nginx配置语法是否正确。配置有错就直接reload很可能把服务搞挂先测一遍能省很多麻烦。3.4 开机自启用systemd托管uWSGIuWSGI不能手动启动完就不管了服务器重启后进程不会自动启动。生产环境用systemd托管服务是标准做法。创建/etc/systemd/system/uwsgi.service[Unit] DescriptionuWSGI instance to serve myproject Afternetwork.target [Service] Userwww-data Groupwww-data WorkingDirectory/srv/www/myproject ExecStart/srv/www/venv/bin/uwsgi --ini uwsgi.ini Restartalways RestartSec5 [Install] WantedBymulti-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第一反应查uWSGI502是部署DjangoNginx最常遇到的问题几乎每个人都遇到过。它表示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的常见原因没执行collectstaticSTATIC_ROOT目录是空的。Nginx配置里alias路径写错或者是root和alias混淆导致实际寻找的路径不对。Nginx没有读取静态目录的权限。检查目录所属用户chown -R www-data:www-data staticfiles。一个屡见不鲜的困局是DEBUGTrue时Django能自动服务静态文件一切正常一旦DEBUGFalseDjango就完全不处理静态文件了如果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核2Gprocesses2, threads24核4Gprocesses4, threads28核8Gprocesses6, threads4这个配置不是定死的生产环境要用压测工具如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_HOSTSDjango会返回400。所以多域名部署时所有真实访问的域名都要加进ALLOWED_HOSTS不能偷懒只写一个IP。结尾的几句心里话这套NginxuWSGI的链路我从第一次部署时被502折磨了两个小时到现在基本能十分钟定位问题中间踩过的坑都是实打实的教训。如果你问我哪一步最重要我会说先把socket权限和日志链路搞清楚再谈别的。很多问题看起来千奇百怪最后都能溯源到日志里的一行报错。部署前把Nginx的error.log和uWSGI的log路径都确认好出了问题第一时间看日志大部分谜题都能解开。第五篇写到这里FBV与CBV的取舍和NginxuWSGI生产部署这套实战链路都讲透了。下一篇我大概率会写Django的性能调优包括数据库查询优化、缓存怎么接、异步任务怎么落地这些都是项目上线之后真正会让你睡不着觉的问题到时候再接着聊。