第2章:Django MTV架构
2.2 Django应用(App)
什么是Django应用
Django应用(App)是一个Python包,包含了特定功能的模型、视图、模板和URL配置。一个Django项目可以包含多个应用,每个应用负责不同的功能模块。
项目 vs 应用
- 项目(Project):整个Web应用的集合,包含配置和多个应用
- 应用(App):项目中的一个功能模块,如博客、用户管理、评论系统等
mysite/ # Django项目
├── manage.py
├── mysite/ # 项目配置
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── blog/ # 博客应用
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── models.py
│ ├── views.py
│ └── urls.py
└── users/ # 用户管理应用
├── __init__.py
├── admin.py
├── apps.py
├── models.py
├── views.py
└── urls.py创建应用命令
python
python manage.py startapp <app_name>创建博客应用
bash
# 在项目根目录下创建应用
python manage.py startapp blog
# 查看创建的应用结构
tree blog/ # Windows/Linux
# 或
ls -la blog/创建后的应用结构:
blog/
├── __init__.py # Python包标识文件
├── admin.py # Django管理后台配置
├── apps.py # 应用配置
├── migrations/ # 数据库迁移文件目录
│ └── __init__.py
├── models.py # 数据模型
├── tests.py # 测试文件
└── views.py # 视图函数创建多个应用
一次只能创建一个应用。
bash
# 创建用户管理应用
python manage.py startapp users
# 创建评论系统应用
python manage.py startapp comments
# 创建API应用
python manage.py startapp api应用结构详解
__init__.py
空文件,标识这是一个Python包。
apps.py
应用配置文件:
python
# blog/apps.py
from django.apps import AppConfig
class BlogConfig(AppConfig):
"""博客应用配置"""
default_auto_field = 'django.db.models.BigAutoField'
name = 'blog'
verbose_name = '博客管理'
# 可选
def ready(self):
"""应用启动时执行的代码"""
# 导入信号处理器
import blog.signalsmodels.py
python
# blog/models.py
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
from django.utils import timezone
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
def get_absolute_url(self):
return reverse('blog:category_detail', kwargs={'slug': self.slug})
class Tag(models.Model):
"""文章标签模型"""
name = models.CharField(max_length=50, unique=True, verbose_name="标签名称")
slug = models.SlugField(unique=True, verbose_name="URL别名")
class Meta:
verbose_name = "标签"
verbose_name_plural = "标签"
ordering = ['name']
def __str__(self):
return self.name
class PublishedManager(models.Manager):
"""已发布文章管理器"""
def get_queryset(self):
return super().get_queryset().filter(
status='published',
publish_date__lte=timezone.now()
)
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="内容")
excerpt = models.TextField(max_length=300, blank=True, verbose_name="摘要")
# 关联字段
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="作者")
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="分类")
tags = models.ManyToManyField(Tag, blank=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="浏览次数")
# 管理器
objects = models.Manager() # 默认管理器
published = PublishedManager() # 自定义管理器
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):
return reverse('blog:article_detail', kwargs={
'year': self.publish_date.year,
'month': self.publish_date.month,
'day': self.publish_date.day,
'slug': self.slug
})
def save(self, *args, **kwargs):
"""重写保存方法,自动生成摘要"""
if not self.excerpt:
self.excerpt = self.content[:200] + '...' if len(self.content) > 200 else self.content
super().save(*args, **kwargs)views.py
python
# blog/views.py
from django.shortcuts import render, get_object_or_404
from django.core.paginator import Paginator
from django.db.models import Q, F, Count
from django.utils import timezone
from django.views.generic import ListView, DetailView
from .models import Article, Category, Tag
def home(request):
"""首页视图"""
# 获取最新发布的文章
latest_articles = Article.published.all()[:5]
# 获取热门文章(按浏览量排序)
popular_articles = Article.published.order_by('-view_count')[:5]
# 获取所有分类
categories = Category.objects.annotate(
article_count=Count('article', filter=Q(article__status='published'))
).filter(article_count__gt=0)
context = {
'latest_articles': latest_articles,
'popular_articles': popular_articles,
'categories': categories,
}
return render(request, 'blog/home.html', context)
class ArticleListView(ListView):
"""文章列表视图"""
model = Article
template_name = 'blog/article_list.html'
context_object_name = 'articles'
paginate_by = 10
queryset = Article.published.all().select_related('author', 'category')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['categories'] = Category.objects.all()
context['popular_tags'] = Tag.objects.annotate(
article_count=Count('article')
).order_by('-article_count')[:10]
return context
class ArticleDetailView(DetailView):
"""文章详情视图"""
model = Article
template_name = 'blog/article_detail.html'
context_object_name = 'article'
def get_queryset(self):
return Article.published.all()
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.published.filter(
category=self.object.category
).exclude(pk=self.object.pk)[:5]
return context
def category_detail(request, slug):
"""分类详情视图"""
category = get_object_or_404(Category, slug=slug)
articles = Article.published.filter(category=category)
# 分页
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)
def search(request):
"""搜索功能视图"""
query = request.GET.get('q', '')
articles = []
if query:
articles = Article.published.filter(
Q(title__icontains=query) |
Q(content__icontains=query) |
Q(tags__name__icontains=query)
).distinct()
# 分页
paginator = Paginator(articles, 10)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
context = {
'query': query,
'articles': page_obj,
'is_paginated': page_obj.has_other_pages(),
'page_obj': page_obj,
'total_results': paginator.count,
}
return render(request, 'blog/search_results.html', context)select_related
- 没有使用 select_related,会产生 N+1 次查询(1次获取文章 + N次获取作者)
articles = Article.objects.all()
for article in articles:
print(article.author.name) # 每次循环都会查询一次数据库获取author- 使用 select_related,只产生 1 次查询(使用JOIN一次性获取所有数据)
articles = Article.objects.select_related('author').all()
for article in articles:
print(article.author.name) # 作者信息已经在第一次查询中获取适用场景
- 外键关系(ForeignKey)
- 一对一关系(OneToOneField)
- 正向关系(从主对象到关联对象)
不适用场景
- 多对多关系(ManyToManyField)→ 使用 prefetch_related
- 反向关系(从关联对象到主对象)
admin.py
python
# blog/admin.py
from django.contrib import admin
from django.utils.html import format_html
from .models import Category, Tag, Article
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
"""分类管理"""
list_display = ['name', 'slug', 'article_count', 'created_at']
list_filter = ['created_at']
search_fields = ['name', 'description']
prepopulated_fields = {'slug': ('name',)}
def article_count(self, obj):
"""显示分类下的文章数量"""
count = obj.article_set.count()
return format_html('<span style="color: blue;">{}</span>', count)
article_count.short_description = '文章数量'
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
"""标签管理"""
list_display = ['name', 'slug', 'article_count']
search_fields = ['name']
prepopulated_fields = {'slug': ('name',)}
def article_count(self, obj):
return obj.article_set.count()
article_count.short_description = '文章数量'
class TagInline(admin.TabularInline):
"""文章标签内联编辑"""
model = Article.tags.through
extra = 1
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
"""文章管理"""
list_display = ['title', 'author', 'category', 'status', 'view_count', 'publish_date', 'created_at']
list_filter = ['status', 'category', 'author', 'created_at', 'publish_date']
search_fields = ['title', 'content']
prepopulated_fields = {'slug': ('title',)}
raw_id_fields = ['author']
date_hierarchy = 'publish_date'
ordering = ['-created_at']
# 字段分组
fieldsets = (
('基本信息', {
'fields': ('title', 'slug', 'author', 'category', 'status')
}),
('内容', {
'fields': ('content', 'excerpt')
}),
('发布设置', {
'fields': ('publish_date',),
'classes': ('collapse',)
}),
('统计信息', {
'fields': ('view_count',),
'classes': ('collapse',)
}),
)
# 过滤器
def get_queryset(self, request):
qs = super().get_queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(author=request.user)
def save_model(self, request, obj, form, change):
"""保存时自动设置作者"""
if not change: # 新建文章
obj.author = request.user
super().save_model(request, obj, form, change)
# 批量操作
actions = ['make_published', 'make_draft']
def make_published(self, request, queryset):
"""批量发布文章"""
updated = queryset.update(status='published')
self.message_user(request, f'{updated} 篇文章已发布。')
make_published.short_description = '发布选中的文章'
def make_draft(self, request, queryset):
"""批量设为草稿"""
updated = queryset.update(status='draft')
self.message_user(request, f'{updated} 篇文章已设为草稿。')
make_draft.short_description = '将选中的文章设为草稿'urls.py
python
# blog/urls.py
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
# 首页
path('', views.home, name='home'),
# 文章相关
path('articles/', views.ArticleListView.as_view(), name='article_list'),
path('article/<int:year>/<int:month>/<int:day>/<slug:slug>/',
views.ArticleDetailView.as_view(), name='article_detail'),
# 分类相关
path('category/<slug:slug>/', views.category_detail, name='category_detail'),
# 搜索
path('search/', views.search, name='search'),
]应用注册
创建应用后,需要在项目的 settings.py 中注册:
python
# mysite/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 自定义应用
'blog.apps.BlogConfig', # 推荐方式,使用应用配置类
# 或简写为:'blog',
]应用配置类的优势
使用完整的应用配置类路径有以下优势:
- 明确性:清楚地指定使用哪个配置类
- 自定义:可以在
BlogConfig类中添加自定义配置 - 信号处理:可以在
ready()方法中注册信号处理器
简单博客应用示例
让我们创建一个完整的博客应用示例:
1. 创建模板目录
bash
# 在应用目录下创建模板目录
mkdir -p blog/templates/blog2. 创建基础模板
html
<!-- blog/templates/blog/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">
<style>
.sidebar {
background-color: #f8f9fa;
min-height: calc(100vh - 56px);
}
.article-meta {
color: #6c757d;
font-size: 0.9em;
}
.tag {
display: inline-block;
background-color: #e9ecef;
color: #495057;
padding: 0.25rem 0.5rem;
margin-right: 0.5rem;
border-radius: 0.25rem;
text-decoration: none;
font-size: 0.8em;
}
.tag:hover {
background-color: #dee2e6;
color: #495057;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{% url 'blog:home' %}">我的博客</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'blog:home' %}">首页</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'blog:article_list' %}">文章</a>
</li>
</ul>
<form class="d-flex" method="get" action="{% url 'blog:search' %}">
<input class="form-control me-2" type="search" name="q" placeholder="搜索文章..." value="{{ request.GET.q }}">
<button class="btn btn-outline-light" type="submit">搜索</button>
</form>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<main class="col-md-9">
{% block content %}
{% endblock %}
</main>
<aside class="col-md-3 sidebar p-3">
{% block sidebar %}
<div class="mb-4">
<h5>分类</h5>
{% for category in categories %}
<div>
<a href="{{ category.get_absolute_url }}" class="text-decoration-none">
{{ category.name }}
</a>
</div>
{% endfor %}
</div>
<div class="mb-4">
<h5>热门标签</h5>
{% for tag in popular_tags %}
<a href="#" class="tag">{{ tag.name }}</a>
{% endfor %}
</div>
{% endblock %}
</aside>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>3. 创建首页模板
html
<!-- blog/templates/blog/home.html -->
{% extends 'blog/base.html' %}
{% block title %}首页 - {{ block.super }}{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row">
<div class="col-12">
<h1 class="mb-4">欢迎来到我的博客</h1>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3>最新文章</h3>
{% for article in latest_articles %}
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">
<a href="{{ article.get_absolute_url }}" class="text-decoration-none">
{{ article.title }}
</a>
</h5>
<p class="article-meta">
{{ article.author.username }} · {{ article.publish_date|date:"m月d日" }}
{% if article.category %}
· {{ article.category.name }}
{% endif %}
</p>
<p class="card-text">{{ article.excerpt }}</p>
</div>
</div>
{% empty %}
<p class="text-muted">暂无文章发布。</p>
{% endfor %}
</div>
<div class="col-md-6">
<h3>热门文章</h3>
{% for article in popular_articles %}
<div class="card mb-3">
<div class="card-body">
<h6 class="card-title">
<a href="{{ article.get_absolute_url }}" class="text-decoration-none">
{{ article.title }}
</a>
</h6>
<p class="article-meta">
浏览 {{ article.view_count }} 次 · {{ article.publish_date|date:"m月d日" }}
</p>
</div>
</div>
{% empty %}
<p class="text-muted">暂无热门文章。</p>
{% endfor %}
</div>
</div>
</div>
{% endblock %}4. 设置URL路由
在项目的主URL配置中包含应用的URL:
python
# mysite/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('blog.urls')), # 包含博客应用的URL
]5. 执行数据库迁移
bash
# 创建迁移文件
python manage.py makemigrations blog
# 执行迁移
python manage.py migrate
# 创建超级用户
python manage.py createsuperuser6. 测试应用
bash
# 启动开发服务器
python manage.py runserver
# 访问以下URL测试:
# http://127.0.0.1:8000/ - 首页
# http://127.0.0.1:8000/admin/ - 管理后台
# http://127.0.0.1:8000/articles/ - 文章列表应用最佳实践
1. 应用职责单一
每个应用应该专注于一个特定的功能:
bash
# 好的应用划分
blog/ # 博客功能
users/ # 用户管理
comments/ # 评论系统
api/ # API接口
# 避免的应用划分
main/ # 太宽泛
utils/ # 工具类不是业务功能2. 应用间解耦
使用信号、中间件或服务层来处理应用间的交互:
python
# blog/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Article
@receiver(post_save, sender=Article)
def article_published(sender, instance, created, **kwargs):
"""文章发布时的信号处理"""
if instance.status == 'published':
# 发送通知、更新缓存等
pass3. 可重用性设计
设计应用时考虑可重用性:
python
# 使用配置使应用更灵活
from django.conf import settings
BLOG_PAGINATION = getattr(settings, 'BLOG_PAGINATION', 10)
BLOG_EXCERPT_LENGTH = getattr(settings, 'BLOG_EXCERPT_LENGTH', 200)小结
Django应用是组织代码的重要方式:
- ✅ 使用
python manage.py startapp创建应用 - ✅ 理解应用的目录结构和各文件作用
- ✅ 在
settings.py中注册应用 - ✅ 遵循单一职责原则设计应用
- ✅ 保持应用间的低耦合
通过合理的应用划分,可以让Django项目更加模块化、可维护和可扩展。
下一篇
我们将深入学习Django的URL路由系统。