6、MVC在Web开发中的应用:以Python的Django框架为例
聊到Web框架,Django是个绕不开的名字。我最早接触Django是在2015年,当时接了个内容管理系统的活,工期紧得要命。说实话,一开始看到Django的MTV模式,我还挺懵的——说好的MVC呢?怎么到了Python这里就改名叫MTV了?
后来做多了才明白,这其实就是换了个马甲。Django的MTV和经典的MVC,本质上是同一套思想的不同表达。今天我就带你把它彻底搞明白。
6.1 从MVC到MTV:Django为什么改名字?
先看一张图,我手绘的,帮你快速建立整体认知:
你看这张图,Django的MTV其实就是把MVC里的Controller换了个名字。在Django中,Model还是Model,Template对应View,而View则扮演了Controller的角色。说白了,就是换了个叫法,核心思想一点没变。
核心对应关系:
- MVC中的Model → Django的Model(数据层)
- MVC中的View → Django的Template(展示层)
- MVC中的Controller → Django的View(控制层)
我记得刚带团队的时候,有个新人问我:「老师,Django不是MTV吗?为什么面试官问我MVC我答不上来?」我告诉他,你只要把Django的View理解成Controller,把Template理解成View,就全通了。面试官考的不是名字,是你能不能把三层拆明白。
6.2 Model层:数据怎么管?
Model层说白了就是跟数据库打交道的地方。Django的ORM做得相当漂亮,你基本不用写SQL。
来看一个实际例子。我之前做过一个博客系统,文章模型大概长这样:
# models.py
from django.db import models
from django.utils import timezone
class Article(models.Model):
title = models.CharField(max_length=200, verbose_name="标题")
content = models.TextField(verbose_name="正文")
pub_date = models.DateTimeField(default=timezone.now, verbose_name="发布时间")
author = models.ForeignKey('Author', on_delete=models.CASCADE, verbose_name="作者")
tags = models.ManyToManyField('Tag', verbose_name="标签")
views = models.IntegerField(default=0, verbose_name="阅读量")
class Meta:
ordering = ['-pub_date']
verbose_name = "文章"
def __str__(self):
return self.title
def increment_views(self):
"""增加阅读量——业务逻辑放在Model里是个好习惯"""
self.views += 1
self.save(update_fields=['views'])
我的经验:把简单的业务方法写在Model里,比如上面的increment_views,能让代码更干净。但复杂的业务逻辑别往里塞,否则Model会变成「上帝类」。我曾经接手过一个项目,一个Model文件写了2000多行……那滋味,你懂的。
6.3 View层:业务逻辑的调度中心
Django的View就是MVC里的Controller。它接收请求,调用Model拿数据,再决定用哪个Template渲染。
我习惯把View分成两类:函数视图(FBV)和类视图(CBV)。新手先用FBV,上手快。等熟练了,CBV能省不少重复代码。
先看FBV的写法:
# views.py
from django.shortcuts import render, get_object_or_404
from .models import Article
def article_detail(request, article_id):
"""文章详情页"""
# 1. 拿数据(Model层)
article = get_object_or_404(Article, id=article_id)
article.increment_views() # 调用Model里的业务方法
# 2. 准备上下文
context = {
'article': article,
'related_articles': Article.objects.filter(
tags__in=article.tags.all()
).exclude(id=article.id)[:5]
}
# 3. 渲染模板(Template层)
return render(request, 'blog/article_detail.html', context)
你看,三步走:拿数据 → 准备上下文 → 渲染模板。这就是MVC的典型流程。
再来看CBV的写法,我个人更推荐这种方式:
# views.py
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'
def get_context_data(self, **kwargs):
# 调用父类方法获取已有上下文
context = super().get_context_data(**kwargs)
# 添加额外数据
article = self.get_object()
context['related_articles'] = Article.objects.filter(
tags__in=article.tags.all()
).exclude(id=article.id)[:5]
return context
def get_object(self, queryset=None):
article = super().get_object(queryset)
article.increment_views()
return article
注意:CBV虽然方便,但别滥用。如果你的视图逻辑特别复杂,硬套CBV反而会让代码更难读。我见过有人为了用CBV,在一个方法里塞了300行代码……嗯,那还不如用FBV来得清爽。
6.4 Template层:展示的最后一公里
Template就是MVC里的View,负责把数据渲染成HTML。Django的模板引擎很强大,但也要注意别把逻辑写进去。
一个典型的模板文件:
<!-- templates/blog/article_detail.html -->
{% extends "base.html" %}
{% block title %}{{ article.title }} - 我的博客{% endblock %}
{% block content %}
<article>
<h1>{{ article.title }}</h1>
<p class="meta">
作者:{{ article.author.name }} |
发布时间:{{ article.pub_date|date:"Y-m-d H:i" }} |
阅读量:{{ article.views }}
</p>
<div class="content">
{{ article.content|safe }}
</div>
<div class="tags">
标签:
{% for tag in article.tags.all %}
<a href="{% url 'blog:tag_articles' tag.id %}">{{ tag.name }}</a>
{% endfor %}
</div>
</article>
<h3>相关文章</h3>
<ul>
{% for related in related_articles %}
<li><a href="{% url 'blog:article_detail' related.id %}">{{ related.title }}</a></li>
{% empty %}
<li>暂无相关文章</li>
{% endfor %}
</ul>
{% endblock %}
我的建议:模板里只做三件事——显示数据、循环、条件判断。别在模板里写复杂的计算,更别调数据库。我曾经看到有人在模板里写{% with articles=some_queryset %}……那等于把Controller的活扔给了View层,架构就乱了。
6.5 URLconf:把一切串起来
URLconf就是MVC里的路由表。它告诉Django:用户访问这个URL,应该调用哪个View。
# urls.py
from django.urls import path
from . import views
urlpatterns = [
# FBV方式
path('article/<int:article_id>/', views.article_detail, name='article_detail'),
# CBV方式(推荐)
path('article/<int:pk>/', views.ArticleDetailView.as_view(), name='article_detail_cbv'),
]
这里有个小细节:<int:article_id>这种写法是Django 2.0之后才有的。老版本用的是正则表达式。如果你维护老项目,可能会看到url(r'^article/(?P<article_id>\d+)/$', ...)这种写法。两种都能用,但我建议新项目用新语法,更直观。
6.6 完整请求流程:一个请求的生命周期
咱们把整个流程串起来走一遍。假设用户访问了/article/3/:
- WSGI服务器(比如Gunicorn)收到HTTP请求,交给Django
- Django中间件处理请求(比如CSRF校验、Session加载)
- URLconf匹配到
/article/3/,调用ArticleDetailView - View从数据库取出id=3的文章,调用
increment_views()增加阅读量 - View查询相关文章,打包成context字典
- Template接收context,渲染成HTML字符串
- Django把HTML包装成HTTP响应,返回给浏览器
整个过程清晰明了。每一层各司其职,互不干扰。这就是MVC模式最大的好处——关注点分离。
总结一下Django MTV的核心原则:
- Model只管数据,别管怎么展示
- Template只管展示,别管业务逻辑
- View只管调度,别直接操作数据库(通过Model间接操作)
说实话,我见过太多初学者把这三层混在一起写。一个视图函数里既写SQL查询,又拼HTML字符串,还处理表单验证……那代码简直没法维护。记住一句话:每一层只做自己的事。坚持这个原则,你的项目才能经得起时间的考验。
好了,Django的MTV模式就讲到这里。下一章咱们聊聊Flask——一个更轻量的框架,看看它在MVC上又玩出了什么花样。
公众号:蓝海资料掘金营,微信deep3321