很多人问我Django 学到什么程度才算“真正入门”。我的标准很简单能把视图层的两种写法讲明白能把自己的项目从开发服务器搬到生产环境里稳定跑起来。这篇是系列的第五篇正好踩在这两个坎上——FBV 与 CBV 是视图层的两条路NginxuWSGI 则是上线绕不开的标准组合。这篇适合两类读者一是跟着系列学到模板、表单之后想搞清楚视图层到底该怎么组织的初学者二是已经在用 Django 写东西但一直没想明白 CBV 内部机制、部署时也总出各种问题的开发者。看完之后你会彻底搞懂 FBV 和 CBV 的本质差异也能亲手在服务器上跑起一套 NginxuWSGI 的 Django 生产环境。代码和配置我都会贴出来命令可以直接抄。1. 视图层的两条路FBV 与 CBV到底怎么选1.1 先搞清楚请求是怎么走到视图里的讲两种视图之前必须先把 Django 处理 HTTP 请求的链路捋一遍。浏览器发来一个请求Django 先根据 URLconfurls.py里的路径匹配规则找到对应的视图函数或视图类视图拿到请求对象后做业务处理最后返回一个 HTTP 响应对象。这个“请求进来、响应出去”的过程就是视图层的核心职责。FBV 的全称是 Function-Based View也就是函数视图——把每个视图写成一个普通的 Python 函数。CBV 的全称是 Class-Based View类视图——把每个视图封装成 Python 类用类里的方法对应不同的 HTTP 请求方式。这两种写法解决的是同一个问题但思路完全不同。函数视图简单直白一个函数就是一个接口你需要在函数内部手动判断请求方法GET、POST、PUT 等再分派给不同的逻辑分支。类视图则把“请求方法到处理函数的映射”变成了框架的默认行为GET 请求自动走到 get 方法POST 请求自动走到 post 方法你只需要在类里定义对应的方法就行。新手最常见的困惑是既然 FBV 这么简单为什么还要学 CBV这就好比问“既然手动挡能开为什么还要自动挡”——日常代步手动挡确实足够了但当你面对复杂路况、需要频繁切换档位的时候自动挡的封装价值就体现出来了。CBV 的核心竞争力是继承与复用后面我会用实际代码对比给你看。1.2 同一个功能两种写法的直观对比我拿“展示文章列表”这个最典型的场景来对比。先看 FBV 写法# urls.py from django.urls import path from . import views urlpatterns [ path(articles/, views.article_list, namearticle_list), ] # views.py from django.shortcuts import render from django.core.paginator import Paginator from .models import Article def article_list(request): article_list Article.objects.filter(publishedTrue).order_by(-pub_time) paginator Paginator(article_list, 10) page_number request.GET.get(page) page_obj paginator.get_page(page_number) return render(request, blog/article_list.html, {page_obj: page_obj})再看看 CBV 写法# urls.py from django.urls import path from . import views urlpatterns [ path(articles/, views.ArticleListView.as_view(), namearticle_list), ] # 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 10 def get_queryset(self): return Article.objects.filter(publishedTrue).order_by(-pub_time)两种写法实现的效果完全一样但 CBV 的代码量明显更少而且分页这个功能直接被paginate_by 10一个类属性搞定了。FBV 里你还得手动创建 Paginator 对象、手动取页码、手动调get_page——当然这些步骤也没多复杂但项目里如果有一百个列表页FBV 就得重复写一百次分页逻辑CBV 只需要在每个类里加一行paginate_by。这不是说 FBV 一无是处。我的经验是逻辑极简、只有一两个分支的接口比如“返回一个 JSON 给前端”FBV 依然是首选代码意图一目了然。但凡是涉及列表展示、对象详情、增删改表单这类 CRUD 密集的操作CBV 的继承优势是压倒性的。提示不要为了用 CBV 而用 CBV。TemplateView、ListView这些通用视图解决了 80% 的常规场景但业务逻辑一旦复杂到“一个页面要聚合五张表的数据”我会果断切回 FBV 或重写 CBV 的get_context_data方法。代码的初心是可读性不是炫技。2. CBV 的并发原理与复用机制拆开看才敢放心用2.1 as_view() 背后到底做了什么用了这么长时间 CBV我一开始也犯过迷糊为什么路由里注册的是views.ArticleListView.as_view()而不是直接写views.ArticleListView原因在于 Django 的 URLconf 要求最终提供给它的必须是一个可调用对象callable这个对象接收request参数并且返回一个response。而类本身是不可直接调用的——你需要先实例化再在实例上调用方法。as_view()这个类方法就起到了“桥梁”作用。as_view()的简化逻辑是这样的它返回一个闭包函数view这个函数内部做了三件事——第一每次请求进来时都创建一个新的视图类实例第二把request、位置参数、关键字参数绑定到这个实例上第三调用实例的dispatch方法由dispatch负责后续的分发。很多初学者对“每次请求都创建新实例”这一条特别不放心总觉得开销很大。但其实这正是 Django 设计上的巧妙之处每个请求都拿到一个全新的实例不同请求之间完全隔离不用操心数据串号问题。如果你试图把用户信息存在类属性上比如在__init__里self.user_cache []那每个请求都会产生独立的一份不会互相污染。反而是那些不加思考就把可变数据放到类属性里的写法会引发难以定位的并发 bug。2.2 dispatch 方法的分发机制理解dispatch是理解 CBV 的关键一步。这个方法名听起来拗口它的行为其实就跟外卖平台的前台一样收到一份请求看一眼请求的方法GET、POST、PUT、DELETE……然后把请求转给对应的“后厨”——也就是类里的get、post、put、delete方法。Django 源码里dispatch的核心逻辑非常短但信息量很大先判断request.method.lower()是否在当前视图类声明的合法方法列表里如果合法就用getattr把同名的处理方法取出来调用如果请求方式不在列表里就返回http_method_not_allowed——一个默认的 405 响应。这意味着你只要在类里写一个get方法它就天然只接受 GET 请求POST 请求进来会自动收到 405不需要你手动判断。理解了dispatch之后你在 CBV 里写权限校验就多了一个方案重写dispatch方法在调用父类dispatch之前做统一处理。我经常在这种地方加登录判断、操作日志、甚至请求耗时统计效果非常好class MyBaseView(View): def dispatch(self, request, *args, **kwargs): # 统一处理所有继承这个类的视图都会先经过这里 if not request.user.is_authenticated: return HttpResponseRedirect(/login/) return super().dispatch(request, *args, **kwargs)这样写的好处是不用把登录校验逻辑复制粘贴到每个方法里改一处全线生效维护成本直线下降。2.3 Mixin 组合CBV 真正的杀招如果说dispatch是 CBV 的枢纽那么 Mixin 就是 CBV 组合能力的核心。Mixin 本质上是一个提供额外方法或属性的类它不单独使用而是被多重继承混合到具体的视图类中。我用一个实际例子说明。假设你的博客需要几个受保护的页面文章发布页、草稿箱、用户个人中心。这三个页面都需要“用户已登录”这个前置条件而且都涉及查询当前用户的数据。用 Mixin 组合起来代码会非常整洁from django.contrib.auth.mixins import LoginRequiredMixin from django.views.generic import ListView, TemplateView from .models import Article class UserScopedMixin(object): 统一把当前用户传给模板并限定数据范围 def get_queryset(self): return super().get_queryset().filter(authorself.request.user) def get_context_data(self, **kwargs): context super().get_context_data(**kwargs) context[current_user] self.request.user return context class MyArticlesView(LoginRequiredMixin, UserScopedMixin, ListView): model Article template_name blog/my_articles.html context_object_name articles class DashboardView(LoginRequiredMixin, TemplateView): template_name account/dashboard.html这里LoginRequiredMixin负责登录校验UserScopedMixin负责把数据限定到当前用户两个 Mixin 叠加在一个 ListView 上各司其职。这种“横切逻辑”用 Mixin 分离之后项目里新增任何一个受保护的页面只需要选择性地继承这些 Mixin 就行。在工程里用 Mixin 多了之后我总结出几条实操规矩Mixin 命名尽量以Mixin结尾一目了然Mixin 内部不要单独继承View否则容易破坏 MRO方法解析顺序多继承的时候把 Mixin 放在左边通用视图放在右边这样可以保证 Mixin 的dispatch、get_queryset等钩子优先被调用。踩过一次 Mixin 顺序反了导致get_queryset没有被执行的坑之后我现在写任何多继承都会先检查 MRO。3. Nginx uWSGI从本地 runserver 到生产环境的完整流程3.1 为什么生产环境不能直接用 runserver本地开发的时候python manage.py runserver用着很舒服——改代码自动重启、自带静态文件服务、报错页面堆栈一清二楚。但这个开发服务器是单进程、单线程的性能极其有限而且官方文档明确说了它不做安全加固不适合生产环境使用。生产环境的标准做法是拆成两层Nginx 做“大门”负责接收用户的所有 HTTP 请求、处理静态文件、负载均衡uWSGI 做“厨房”负责把 Nginx 转发过来的请求翻译成 Django 能理解的 WSGI 调用执行完业务逻辑再把结果返回给 Nginx。为什么不直接用 Nginx 访问 Django因为 Nginx 本身不理解 Python 的 WSGI 协议需要一个适配层。uWSGI 就是这样一个应用服务器它不仅仅是简单的进程管理器还自带一套高效的通信协议和 Nginx 配合作业时性能非常可观。整个链路在脑子里过一遍就是浏览器 → Nginx:80 端口 → uWSGI:8000 端口 → Django 的 WSGI 应用 → 返回。每一层各司其职出了问题分层排查思路会非常清晰。3.2 服务器环境准备与项目上传部署前先把服务器基础环境准备好。我通常在 Ubuntu 系的系统上操作流程是更新系统包、安装 Python 开发包与虚拟环境工具、安装 Nginx、安装 uWSGI。依赖安装完把本地项目上传到服务器。这边有个原则不要传虚拟环境目录本身只要传项目的代码文件然后在服务器上重新创建虚拟环境。所以我习惯在本地维护一份requirements.txt每次部署都在服务器上重新执行一遍pip install -r requirements.txt这样能保证依赖版本一致也能避免把本地虚拟环境里的一堆无关依赖带上去。数据库方面如果项目用的是 SQLite这个阶段还能凑合跑一跑但如果并发上来强烈建议切到 PostgreSQL。SQLite 在读写锁的限制下多 worker 并发时会出现database is locked的报错这在生产环境里是个大坑。具体切换方法也很简单安装好 PostgreSQL创建数据库和用户然后在settings.py里把DATABASES配置从 SQLite 换成 PostgreSQL 的连接字符串即可。3.3 uWSGI 配置文件逐行拆解uWSGI 的配置我强烈建议写成uwsgi.ini文件而不是把一长串命令行参数堆在启动命令里。配置文件一旦写好就基本不用再改了而且用 systemd 管理的时候指向配置文件的方式也最干净。下面是一份适用于绝大多数 Django 项目的 uWSGI 配置我一行一行给你解释[uwsgi] chdir /srv/www/myproject module myproject.wsgi:application master true processes 4 threads 2 socket 127.0.0.1:8000 chmod-socket 664 vacuum true die-on-term true max-requests 5000 pidfile /run/uwsgi/myproject.pid logto /var/log/uwsgi/myproject.logchdir切换到项目根目录相当于在启动前执行cd /srv/www/myproject。这个路径必须准确因为 Django 的WSGI_APPLICATION配置是相对于它来解析的。module指定加载哪个 WSGI 应用。Django 项目默认生成一个myproject/wsgi.py文件里面定义了application对象所以这一行的格式是“项目配置目录名.wsgi:application”。master true启动一个 master 进程来管理下面的 worker 进程。master 进程本身不处理业务负责 worker 的 spawn、exit、内存回收。没有 master 模式uWSGI 的很多高级特性都用不了强烈建议开启。processes 4和threads 24 个 worker 进程每个进程 2 个线程总并发能力约等于 8 个并发请求。这个数值不是拍脑袋定的要考虑服务器 CPU 核心数。通用做法是processes CPU核心数线程数视任务类型调整——推荐设置线程数在 2-4 之间避免线程切换开销过大。socket 127.0.0.1:8000监听本机 8000 端口。生产环境不要让 uWSGI 直接监听公网只监听内网地址安全很多。这里用的是 uWSGI 自己的协议不是 HTTP 协议原因是后面 Nginx 配合使用uwsgi_pass走的是这套原生的高质量协议。vacuum true进程退出时自动清理 socket 文件、pid 文件避免残留。die-on-term true收到系统终止信号时优雅退出。这个不设置的话systemd 停服务时可能一直等不到 uWSGI 的回应表现就是卡在停止状态。max-requests 5000每个 worker 处理完 5000 个请求后自动重启这是防止内存缓慢泄漏常用的兜底策略。logto把日志写到固定文件里。排查问题的时候没有日志简直寸步难行这行务必加上。如果你在调试阶段希望看到错误堆栈在终端直接输出可以暂时把logto注释掉让日志打到终端上跑通之后再重新加回日志文件。3.4 Nginx 配置把静态文件和反向代理分开处理Nginx 配置是整个部署流程里最容易犯迷糊的地方。我会把配置分成两个 location 来写一个负责静态文件一个负责动态请求转发。静态文件先说清楚一个关键概念Django 项目里的static/目录存放的是开发阶段由各 app 提供的静态文件而STATIC_ROOT是生产环境通过python manage.py collectstatic汇总之后的位置。Nginx 直接服务STATIC_ROOT对应目录一旦路径配对错误页面会呈现出一个典型的症状HTML 能打开但所有 CSS、JS、图片全部 404。下面是一份能直接套用的 Nginx server 配置server { listen 80; server_name your_domain.com; client_max_body_size 20m; location /static/ { alias /srv/www/myproject/staticfiles/; } location /media/ { alias /srv/www/myproject/media/; } location / { include uwsgi_params; uwsgi_pass 127.0.0.1:8000; uwsgi_param UWSGI_SCHEME $scheme; } }第一个location /static/当 URL 路径以/static/开头时Nginx 直接从本地文件系统的/srv/www/myproject/staticfiles/目录读取文件返回不经过 Django。alias的意思是“URL 中/static/之后的部分拼在 alias 目录后面去找文件”比如请求/static/css/app.cssNginx 就去读/srv/www/myproject/staticfiles/css/app.css。第二个location /media/用户上传的文件存放在这里同样交给 Nginx 直接服务。这两个目录必须和 Djangosettings.py里的STATIC_URL、MEDIA_URL、STATIC_ROOT、MEDIA_ROOT保持一致。第三个location /匹配剩下所有请求直接转发给 uWSGI。include uwsgi_params把 uWSGI 和 Nginx 通信需要的标准参数加载进来uwsgi_pass指定 uWSGI 监听的地址。关于alias和root的差异我重点提醒一下。root会把location后面的完整路径追加到指定目录而alias会把location匹配的部分替换掉。很多人把这两者混用导致静态文件 404是最常见的低级错误之一。用我上面的写法location /static/配alias就不会出错。3.5 Django 侧的上线前检查清单部署到生产环境之前Django 自身的配置也需要做一轮调整。最核心的有这么几项DEBUG必须改成False否则一旦线上报错错误堆栈会直接暴露给用户这不仅是体验问题更是安全隐患。ALLOWED_HOSTS必须配置成你实际的域名或服务器 IP否则 Django 会拒绝所有请求。SECRET_KEY不要写在代码库里用环境变量方式注入。collectstatic这一步绝对不能漏。你把DEBUG设为False之后Django 就不再自动托管静态文件了必须先跑到项目目录里执行python manage.py collectstatic --noinput把全部静态文件汇总到STATIC_ROOT指定的目录Nginx 才有东西可服务。我见过太多人线上页面样式全丢都是漏了这一步。上线之后如果页面涉及跨域或者需要接收来自某些域名的 POST 请求还要检查CSRF_TRUSTED_ORIGINS配置。Django 4.0 之后这个配置项对跨域提交的 CSRF 校验非常重要配置不当会导致跨域表单提交全部报 403。整个部署流程跑一遍我的建议顺序是先在服务器上创建虚拟环境并安装依赖 → 改settings.py并执行collectstatic→ 用命令行直接启动 uWSGI 测试能不能正常响应 → 配置 systemd 管理 uWSGI → 再配置 Nginx 并nginx -t测试配置正确性 → 最后 reload Nginx。每一步都验证通过再进下一步不要一次把所有东西全改完然后一起重启出了问题很难定位是哪个环节挂了。4. 部署踩坑实录那些日志不会直接告诉你的东西4.1 静态文件全部 404先查这五个地方部署完成之后页面 HTML 能打开但样式全丢这是所有 Django 初学者都会遇到的经典问题。我排查这个问题有一组固定流程先确认collectstatic是否执行过、STATIC_ROOT是否正确再确认STATIC_URL是否以/static/结尾然后确认 Nginx 的location /static/的alias路径和STATIC_ROOT是否完全一致接着检查服务器目录权限Nginx worker 进程是否对目录有读取权限最后打开浏览器看网络请求看 404 的请求路径是否符合STATIC_URL 具体文件路径。这五个点里最容易让人疏忽的是“权限”。很多云服务器默认的 web 目录权限很严格collectstatic生成的文件归属是部署用户而 Nginx 的 worker 进程是以nginx用户的身份运行的如果目录的访问权限没有开放给 nginx 用户就会造成 404。解决办法就是检查目录和文件的权限位或者把静态目录的属主改成 Nginx 的运行用户。注意不要为了省事直接把静态目录权限设成777。生产环境这种权限设置等于开门揖盗正确做法是给目录设置合适的属主和组并保证组内有可读可执行权限。4.2 502 Bad Gateway 的三种常见原因Nginx 返回 502说明 Nginx 已经成功把请求转发到了后端的 127.0.0.1:8000但 uWSGI 没有正常响应。最常见的原因有三种一是 uWSGI 服务根本没起来或者起来了但因为配置错误崩掉了二是socket的端口对不上Nginx 里uwsgi_pass配的端口和 uWSGI 监听端口不一致三是 uWSGI 的 worker 进程卡死或与 master 失联。排查方法很直接先systemctl status uwsgi看服务状态再打开 uWSGI 的日志文件看有没有异常堆栈。如果日志里出现ImportError之类的东西大概率是虚拟环境没配对启动时候用的 Python 解释器不对。如果日志里提示“bind socket 失败”说明端口被占用用ss -tlnp查一下端口情况即可。还有一个隐藏很深的 502 原因uWSGI 用socket 127.0.0.1:8000这个配置时如果vacuum没开几个版本之间残留的 socket 文件可能互相干扰。解决办法就是确保配置里有vacuum true。4.3 403 与 CSRF 校验先分清是 Nginx 还是 Django 的锅403 这个状态码在不同环节代表完全不同的原因我最常踩的一个坑是登录后页面能正常 GET但一提交 POST 表单就 403。这时先看响应内容如果响应里带了CSRF字样的错误页面那就是 Django 的 CSRF 校验没通过。原因不外乎三种模板里没有加{% csrf_token %}跨域请求没配置CSRF_TRUSTED_ORIGINS或者浏览器与服务器的域名不一致导致 referer 校验失败。如果响应页面根本没有 Django 的报错信息、纯粹是 Nginx 的 403 Forbidden那问题多半出在 Nginx 层比如server_name配置不正确、location匹配规则有问题、或目录没有index文件。遇到这类问题第一件事不是猜而是去读 Nginx 的 error.log 和 access.log日志里给出的信息远比表面现象可靠。4.4 常用问题速查表现象可能原因快速排查命令 / 操作静态文件 404未执行 collectstatic / alias 路径错误 / 目录权限不足ls STATIC_ROOT、nginx -t、ll /srv/www/myproject502 Bad GatewayuWSGI 未启动 / 端口不一致 / worker 崩溃systemctl status uwsgi、tail -f uwsgi.log403 CSRF模板缺 token / 跨域白名单缺失 / referer 不一致检查模板、检查 settings页面能开但 POST 超时worker 数不足 / 数据库连接池耗尽htop看 CPU、ss -tlnp看连接数重启后无法访问uWSGI PID 文件残留 / Nginx 未 reloadkill -9清理残留进程、nginx -s reload这张表是我自己的排查笔记精简出来的直接按行自上而下排查90% 的问题都能快速定位。最后再补一句部署环境里最忌讳“改了配置不重启”uWSGI 和 Nginx 的配置改动分别需要systemctl restart uwsgi和nginx -s reload才能真正生效只改文件不重启一切看起来都“正常”但就是不生效。我个人在实际操作中的体会是FBV 和 CBV 没有高下之分它们是一枚硬币的两面学会根据场景做选择比盲目崇拜任何一种写法都重要。至于部署第一次弄总是手忙脚乱的多调试几次之后你就会发现NginxuWSGI 这套组合其实非常稳定只要配置文件写对了跑上几个月不重启都没问题。按照这篇文章的顺序把settings.py的配置、uWSGI 的 ini 文件、Nginx 的 server 块逐个落实你的项目就能稳稳地挂在线上而不是停留在本地 runserver 的阶段了。