Skip to content

第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.signals

models.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,会产生 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',
]

应用配置类的优势

使用完整的应用配置类路径有以下优势:

  1. 明确性:清楚地指定使用哪个配置类
  2. 自定义:可以在 BlogConfig 类中添加自定义配置
  3. 信号处理:可以在 ready() 方法中注册信号处理器

简单博客应用示例

让我们创建一个完整的博客应用示例:

1. 创建模板目录

bash
# 在应用目录下创建模板目录
mkdir -p blog/templates/blog

2. 创建基础模板

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 createsuperuser

6. 测试应用

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':
        # 发送通知、更新缓存等
        pass

3. 可重用性设计

设计应用时考虑可重用性:

python
# 使用配置使应用更灵活
from django.conf import settings

BLOG_PAGINATION = getattr(settings, 'BLOG_PAGINATION', 10)
BLOG_EXCERPT_LENGTH = getattr(settings, 'BLOG_EXCERPT_LENGTH', 200)

小结

Django应用是组织代码的重要方式:

  1. ✅ 使用 python manage.py startapp 创建应用
  2. ✅ 理解应用的目录结构和各文件作用
  3. ✅ 在 settings.py 中注册应用
  4. ✅ 遵循单一职责原则设计应用
  5. ✅ 保持应用间的低耦合

通过合理的应用划分,可以让Django项目更加模块化、可维护和可扩展。

下一篇

我们将深入学习Django的URL路由系统。

3.1 Django URL配置基础 →

目录

返回课程目录

Released under the Apache 2.0 License.