Skip to content

第2章:Django MTV架构

2.1 Django MTV架构模式

什么是MTV架构

Django采用MTV(Model-Template-View)架构模式,这是对传统MVC(Model-View-Controller)模式的变体。MTV模式将应用程序分为三个主要组件:

  • Model(模型):负责数据存储和业务逻辑
  • Template(模板):负责数据展示和用户界面
  • View(视图):负责处理用户请求和业务逻辑控制

本篇文章了解概念即可,我们后面会详细讲解每个组件的应用。

MTV vs MVC对比

组件Django MTV传统MVC说明
数据层ModelModel数据模型和业务逻辑
表示层TemplateView用户界面和数据展示
控制层ViewController请求处理和逻辑控制

Django的"View"更像是传统MVC中的"Controller",而"Template"对应传统MVC中的"View"。 用从鲜鱼到鱼干制作过程比喻来说明,Model是鱼,View是鱼到鱼干的加工过程,Template是鱼干的包装。Model是业务本质,决定了系统产生什么样的业务数据。

MTV协作流程

让我们通过一个完整的请求-响应流程来理解MTV如何协作:

1. 用户访问文章列表页面

用户请求: GET /articles/

URL路由: urls.py 匹配路径

视图处理: views.py 中的 article_list 函数

模型查询: models.py 中的 Article.objects.filter()

模板渲染: templates/blog/article_list.html

HTTP响应: 返回渲染后的HTML页面

2. 完整的URL配置

python
# urls.py
from django.urls import path
from . import views

app_name = 'blog'

urlpatterns = [
    # 函数视图
    path('', views.article_list, name='article_list'),
    path('article/<slug:slug>/', views.article_detail, name='article_detail'),
    path('category/<slug:slug>/', views.category_detail, name='category_detail'),
    
    # 类视图
    # path('', views.ArticleListView.as_view(), name='article_list'),
    # path('article/<slug:slug>/', views.ArticleDetailView.as_view(), name='article_detail'),
]

3. 数据流向图

┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│   Browser   │    │   Django    │    │  Database   │
│             │    │             │    │             │
└─────────────┘    └─────────────┘    └─────────────┘
       │                   │                   │
       │ 1. HTTP Request   │                   │
       ├──────────────────►│                   │
       │                   │ 2. Query Data     │
       │                   ├──────────────────►│
       │                   │ 3. Return Data    │
       │                   │◄──────────────────┤
       │                   │ 4. Render Template│
       │                   │                   │
       │ 5. HTTP Response  │                   │
       │◄──────────────────┤                   │

Model(模型)详解

Model是数据的唯一、权威的信息源。它包含存储数据的基本字段和行为。

基本模型示例

python
# models.py
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User

class Category(models.Model):
    """文章分类模型"""
    name = models.CharField(max_length=100, verbose_name="分类名称")
    slug = models.SlugField(unique=True, verbose_name="URL别名")
    description = models.TextField(blank=True, verbose_name="分类描述")
    created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
    
    class Meta:
        verbose_name = "文章分类"
        verbose_name_plural = "文章分类"
        ordering = ['name']
    
    def __str__(self):
        return self.name

class Article(models.Model):
    """文章模型"""
    STATUS_CHOICES = [
        ('draft', '草稿'),
        ('published', '已发布'),
        ('archived', '已归档'),
    ]
    
    title = models.CharField(max_length=200, verbose_name="标题")
    slug = models.SlugField(unique_for_date='publish_date', verbose_name="URL别名")
    content = models.TextField(verbose_name="内容")
  
    
    # 关联字段
    author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="作者")
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, verbose_name="分类")
    
    # 状态和时间字段
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft', verbose_name="状态")
    publish_date = models.DateTimeField(default=timezone.now, verbose_name="发布时间")
    created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
    updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
    
    # 统计字段
    view_count = models.PositiveIntegerField(default=0, verbose_name="浏览次数")
    
    class Meta:
        verbose_name = "文章"
        verbose_name_plural = "文章"
        ordering = ['-publish_date']
        indexes = [
            models.Index(fields=['status', 'publish_date']),
            models.Index(fields=['category', 'status']),
        ]
    
    def __str__(self):
        return self.title
    
    def get_absolute_url(self):
        """获取文章详情页URL"""
        from django.urls import reverse
        return reverse('article_detail', kwargs={'slug': self.slug})
    
    def is_published(self):
        """检查文章是否已发布"""
        return self.status == 'published' and self.publish_date <= timezone.now()

模型的关键特性

  1. 字段类型:CharField、TextField、DateTimeField等
  2. 字段选项:max_length、blank、null、default等
  3. 关系字段:ForeignKey、ManyToManyField、OneToOneField
  4. Meta类:定义模型的元数据
  5. 模型方法:自定义业务逻辑方法

View(视图)详解

View接收Web请求并返回Web响应。它包含处理请求的业务逻辑。

函数视图示例

python
# views.py
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse, Http404
from django.core.paginator import Paginator
from django.db.models import Q, F
from django.utils import timezone
from .models import Article, Category

def article_list(request):
    """文章列表视图"""
    # 获取查询参数
    search_query = request.GET.get('search', '')
    category_slug = request.GET.get('category', '')
    
    # 基础查询集
    articles = Article.objects.filter(
        status='published',
        publish_date__lte=timezone.now()
    ).select_related('author', 'category')
    
    # 搜索过滤
    if search_query:
        articles = articles.filter(
            Q(title__icontains=search_query) |
            Q(content__icontains=search_query)
        )
    
    # 分类过滤
    if category_slug:
        articles = articles.filter(category__slug=category_slug)
    
    # 分页
    paginator = Paginator(articles, 10)  # 每页10篇文章
    page_number = request.GET.get('page')
    page_obj = paginator.get_page(page_number)
    
    # 获取所有分类
    categories = Category.objects.all()
    
    context = {
        'articles': page_obj,
        'categories': categories,
        'search_query': search_query,
        'category_slug': category_slug,
        'is_paginated': page_obj.has_other_pages(),
        'page_obj': page_obj,
    }
    
    return render(request, 'blog/article_list.html', context)

def article_detail(request, slug):
    """文章详情视图"""
    article = get_object_or_404(
        Article,
        slug=slug,
        status='published',
        publish_date__lte=timezone.now()
    )
    
    # 增加浏览次数
    Article.objects.filter(pk=article.pk).update(view_count=F('view_count') + 1)
    
    # 获取相关文章
    related_articles = Article.objects.filter(
        category=article.category,
        status='published'
    ).exclude(pk=article.pk)[:5]
    
    context = {
        'article': article,
        'related_articles': related_articles,
    }
    
    return render(request, 'blog/article_detail.html', context)

def category_detail(request, slug):
    """分类详情视图"""
    category = get_object_or_404(Category, slug=slug)
    
    articles = Article.objects.filter(
        category=category,
        status='published',
        publish_date__lte=timezone.now()
    ).select_related('author')
    
    # 分页
    paginator = Paginator(articles, 10)
    page_number = request.GET.get('page')
    page_obj = paginator.get_page(page_number)
    
    context = {
        'category': category,
        'articles': page_obj,
        'is_paginated': page_obj.has_other_pages(),
        'page_obj': page_obj,
    }
    
    return render(request, 'blog/category_detail.html', context)

类视图示例

# views.py (类视图版本)
from django.views.generic import ListView, DetailView
from django.db.models import Q, F
from django.utils import timezone

class ArticleListView(ListView):
    """文章列表类视图"""
    model = Article
    template_name = 'blog/article_list.html'
    context_object_name = 'articles'
    paginate_by = 10
    
    def get_queryset(self):
        queryset = Article.objects.filter(
            status='published',
            publish_date__lte=timezone.now()
        ).select_related('author', 'category')
        
        # 搜索功能
        search_query = self.request.GET.get('search')
        if search_query:
            queryset = queryset.filter(
                Q(title__icontains=search_query) |
                Q(content__icontains=search_query)
            )
        
        # 分类过滤
        category_slug = self.request.GET.get('category')
        if category_slug:
            queryset = queryset.filter(category__slug=category_slug)
        
        return queryset
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['categories'] = Category.objects.all()
        context['search_query'] = self.request.GET.get('search', '')
        context['category_slug'] = self.request.GET.get('category', '')
        return context

class ArticleDetailView(DetailView):
    """文章详情类视图"""
    model = Article
    template_name = 'blog/article_detail.html'
    context_object_name = 'article'
    slug_field = 'slug'
    slug_url_kwarg = 'slug'
    
    def get_queryset(self):
        return Article.objects.filter(
            status='published',
            publish_date__lte=timezone.now()
        )
    
    def get_object(self, queryset=None):
        obj = super().get_object(queryset)
        # 增加浏览次数
        Article.objects.filter(pk=obj.pk).update(view_count=F('view_count') + 1)
        return obj
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        # 获取相关文章
        context['related_articles'] = Article.objects.filter(
            category=self.object.category,
            status='published'
        ).exclude(pk=self.object.pk)[:5]
        return context

访问URL示例

http://127.0.0.1:8000/articles/ - 所有文章
http://127.0.0.1:8000/articles/?search=django - 搜索"django"
http://127.0.0.1:8000/articles/?category=python - Python分类文章
http://127.0.0.1:8000/articles/?page=2 - 第二页

Template(模板)详解

Template负责定义数据如何呈现给用户。Django使用自己的模板语言(DTL)。

基本模板示例

<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}我的博客{% endblock %}</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <div class="container">
            <a class="navbar-brand" href="{% url 'home' %}">我的博客</a>
            <div class="navbar-nav">
                <a class="nav-link" href="{% url 'article_list' %}">文章</a>
                <a class="nav-link" href="{% url 'category_list' %}">分类</a>
            </div>
        </div>
    </nav>
    
    <main class="container mt-4">
        {% block content %}
        {% endblock %}
    </main>
    
    <footer class="bg-light text-center py-3 mt-5">
        <p>&copy; 2025 我的博客. All rights reserved.</p>
    </footer>
    
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
html
<!-- templates/blog/article_list.html -->
{% extends 'base.html' %}
{% load static %}

{% block title %}文章列表 - {{ block.super }}{% endblock %}

{% block content %}
<div class="row">
    <div class="col-md-8">
        <h2>最新文章</h2>
        {% for article in articles %}
        <article class="card mb-4">
            <div class="card-body">
                <h3 class="card-title">
                    <a href="{{ article.get_absolute_url }}" class="text-decoration-none">
                        {{ article.title }}
                    </a>
                </h3>
                <p class="card-text text-muted">
                    <small>
                        由 {{ article.author.username }} 发布于 {{ article.publish_date|date:"Y年m月d日" }}
                        | 分类:{{ article.category.name }}
                        | 浏览:{{ article.view_count }} 次
                    </small>
                </p>
                <p class="card-text">{{ article.excerpt|default:article.content|truncatewords:30 }}</p>
                <a href="{{ article.get_absolute_url }}" class="btn btn-primary">阅读全文</a>
            </div>
        </article>
        {% empty %}
        <div class="alert alert-info">
            <p>暂无文章发布。</p>
        </div>
        {% endfor %}
        
        <!-- 分页 -->
        {% if is_paginated %}
        <nav aria-label="文章分页">
            <ul class="pagination justify-content-center">
                {% if page_obj.has_previous %}
                <li class="page-item">
                    <a class="page-link" href="?page={{ page_obj.previous_page_number }}">上一页</a>
                </li>
                {% endif %}
                
                <li class="page-item active">
                    <span class="page-link">第 {{ page_obj.number }} 页,共 {{ page_obj.paginator.num_pages }} 页</span>
                </li>
                
                {% if page_obj.has_next %}
                <li class="page-item">
                    <a class="page-link" href="?page={{ page_obj.next_page_number }}">下一页</a>
                </li>
                {% endif %}
            </ul>
        </nav>
        {% endif %}
    </div>
    
    <div class="col-md-4">
        <div class="card">
            <div class="card-header">
                <h5>文章分类</h5>
            </div>
            <div class="card-body">
                {% for category in categories %}
                <a href="{% url 'category_detail' category.slug %}" class="badge bg-secondary text-decoration-none me-2 mb-2">
                    {{ category.name }}
                </a>
                {% endfor %}
            </div>
        </div>
    </div>
</div>
{% endblock %}

article_list在模板中的使用

<!-- 生成文章列表链接 -->
<a href="{% url 'article_list' %}">所有文章</a>

<!-- 带参数的链接 -->
<a href="{% url 'article_list' %}?search=django&category=python">
    搜索Django相关的Python文章
</a>

<!-- 分页链接 -->
<a href="{% url 'article_list' %}?page=2">下一页</a>

模板语言特性

  1. 变量
  2. 标签{% tag %}
  3. 过滤器0
  4. 模板继承{% extends %}{% block %}
  5. 包含模板{% include %}

MTV最佳实践

1. 模型设计原则

python
# 好的实践
class Article(models.Model):
    # 使用描述性的字段名
    title = models.CharField(max_length=200, verbose_name="标题")
    
    # 添加适当的约束
    slug = models.SlugField(unique_for_date='publish_date')
    
    # 使用合适的字段类型
    status = models.CharField(max_length=20, choices=STATUS_CHOICES)
    
    # 添加索引提高查询性能
    class Meta:
        indexes = [
            models.Index(fields=['status', 'publish_date']),
        ]
    
    # 提供有用的方法
    def get_absolute_url(self):
        return reverse('article_detail', kwargs={'slug': self.slug})

2. 视图设计原则

python
# 保持视图简洁
def article_list(request):
    # 单一职责:只处理列表展示
    articles = Article.objects.published()  # 使用模型管理器
    return render(request, 'blog/article_list.html', {'articles': articles})

# 使用类视图处理复杂逻辑
class ArticleCreateView(CreateView):
    model = Article
    form_class = ArticleForm
    template_name = 'blog/article_form.html'
    
    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)

3. 模板设计原则

html
<!-- 使用模板继承减少重复 -->
{% extends 'base.html' %}

<!-- 使用有意义的块名 -->
{% block page_title %}文章列表{% endblock %}

<!-- 使用过滤器格式化数据 -->
{{ article.publish_date|date:"Y年m月d日" }}

<!-- 使用模板标签处理逻辑 -->
{% for article in articles %}
    {% include 'blog/article_card.html' %}
{% empty %}
    <p>暂无文章</p>
{% endfor %}

小结

Django的MTV架构模式提供了清晰的代码组织方式:

  1. ✅ Model负责数据存储和业务逻辑处理
  2. ✅ Template负责用户界面和数据展示
  3. ✅ View负责请求处理和业务逻辑控制
  4. ✅ 三者协同工作实现完整的Web应用功能
  5. ✅ 遵循MTV模式有助于代码维护和团队协作

理解MTV架构是掌握Django开发的基础。

下一篇

我们将学习Django应用的创建和管理。

2.2 Django应用 →

目录

返回课程目录

Released under the Apache 2.0 License.