第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 | 说明 |
|---|---|---|---|
| 数据层 | Model | Model | 数据模型和业务逻辑 |
| 表示层 | Template | View | 用户界面和数据展示 |
| 控制层 | View | Controller | 请求处理和逻辑控制 |
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()模型的关键特性
- 字段类型:CharField、TextField、DateTimeField等
- 字段选项:max_length、blank、null、default等
- 关系字段:ForeignKey、ManyToManyField、OneToOneField
- Meta类:定义模型的元数据
- 模型方法:自定义业务逻辑方法
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>© 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>模板语言特性
- 变量:
- 标签:
{% tag %} - 过滤器:
0 - 模板继承:
{% extends %}和{% block %} - 包含模板:
{% 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架构模式提供了清晰的代码组织方式:
- ✅ Model负责数据存储和业务逻辑处理
- ✅ Template负责用户界面和数据展示
- ✅ View负责请求处理和业务逻辑控制
- ✅ 三者协同工作实现完整的Web应用功能
- ✅ 遵循MTV模式有助于代码维护和团队协作
理解MTV架构是掌握Django开发的基础。
下一篇
我们将学习Django应用的创建和管理。