6、MVC在Web开发中的应用:以Python的Django框架为例

聊到Web框架,Django是个绕不开的名字。我最早接触Django是在2015年,当时接了个内容管理系统的活,工期紧得要命。说实话,一开始看到Django的MTV模式,我还挺懵的——说好的MVC呢?怎么到了Python这里就改名叫MTV了?

后来做多了才明白,这其实就是换了个马甲。Django的MTV和经典的MVC,本质上是同一套思想的不同表达。今天我就带你把它彻底搞明白。

6.1 从MVC到MTV:Django为什么改名字?

先看一张图,我手绘的,帮你快速建立整体认知:

Django MTV 架构核心流程 🌐 用户浏览器 发送HTTP请求 📋 URLconf 路由分发 👁️ View 业务逻辑处理 调用Model/查询DB 🗄️ Model 数据存取/ORM DB 数据库 🎨 Template 渲染HTML 返回HTTP响应 MVC对应关系:Model ↔ Model | View ↔ Template | Controller ↔ View 数据层 路由/展示层 业务逻辑层

你看这张图,Django的MTV其实就是把MVC里的Controller换了个名字。在Django中,Model还是ModelTemplate对应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/

  1. WSGI服务器(比如Gunicorn)收到HTTP请求,交给Django
  2. Django中间件处理请求(比如CSRF校验、Session加载)
  3. URLconf匹配到/article/3/,调用ArticleDetailView
  4. View从数据库取出id=3的文章,调用increment_views()增加阅读量
  5. View查询相关文章,打包成context字典
  6. Template接收context,渲染成HTML字符串
  7. Django把HTML包装成HTTP响应,返回给浏览器

整个过程清晰明了。每一层各司其职,互不干扰。这就是MVC模式最大的好处——关注点分离

总结一下Django MTV的核心原则:

  • Model只管数据,别管怎么展示
  • Template只管展示,别管业务逻辑
  • View只管调度,别直接操作数据库(通过Model间接操作)

说实话,我见过太多初学者把这三层混在一起写。一个视图函数里既写SQL查询,又拼HTML字符串,还处理表单验证……那代码简直没法维护。记住一句话:每一层只做自己的事。坚持这个原则,你的项目才能经得起时间的考验。

好了,Django的MTV模式就讲到这里。下一章咱们聊聊Flask——一个更轻量的框架,看看它在MVC上又玩出了什么花样。


公众号:蓝海资料掘金营,微信deep3321