Django从零开始(三):视图与URL路由详解

一、Django视图概述

视图(View)是Django的MVT架构中的"V",负责处理用户的HTTP请求,执行业务逻辑,并返回HTTP响应。你可以把视图理解为URL与数据处理之间的桥梁。

Django提供了两种定义视图的方式:

  • 函数视图(FBV):基于函数,简单直观,适合初学者
  • 类视图(CBV):基于类,面向对象,支持继承和混入,适合复杂场景

二、函数视图(FBV)

2.1 基本函数视图

# blog/views.py

from django.http import HttpResponse, JsonResponse, HttpResponseRedirect
from django.shortcuts import render, get_object_or_404, redirect
from .models import Article, Category, Tag


def article_list(request):
    """文章列表视图"""
    articles = Article.objects.filter(status='published').order_by('-created_at')
    categories = Category.objects.all()

    context = {
        'articles': articles,
        'categories': categories,
        'total_count': articles.count(),
    }
    return render(request, 'blog/article_list.html', context)


def article_detail(request, slug):
    """文章详情视图"""
    article = get_object_or_404(Article, slug=slug, status='published')
    # 使用F对象更新浏览量,避免竞态条件
    Article.objects.filter(pk=article.pk).update(views=F('views') + 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)

2.2 处理不同HTTP方法

def article_create(request):
    """创建文章(处理GET和POST)"""
    if request.method == 'POST':
        title = request.POST.get('title', '').strip()
        content = request.POST.get('content', '').strip()
        category_id = request.POST.get('category')

        # 数据验证
        if not title or not content:
            return render(request, 'blog/article_form.html', {
                'error': '标题和内容不能为空',
                'categories': Category.objects.all(),
            })

        article = Article.objects.create(
            title=title,
            slug=slugify(title),
            content=content,
            author=request.user,
            category_id=category_id,
            status='draft',
        )
        return redirect('blog:article_detail', slug=article.slug)

    # GET请求 - 显示创建表单
    categories = Category.objects.all()
    return render(request, 'blog/article_form.html', {'categories': categories})


def article_api(request):
    """处理多种HTTP方法的API视图"""
    if request.method == 'GET':
        articles = list(Article.objects.filter(status='published').values(
            'id', 'title', 'slug', 'views', 'created_at'
        ))
        return JsonResponse({'articles': articles, 'total': len(articles)})

    elif request.method == 'POST':
        import json
        data = json.loads(request.body)
        article = Article.objects.create(
            title=data['title'],
            content=data['content'],
            author=request.user,
        )
        return JsonResponse({'id': article.id, 'message': '创建成功'}, status=201)

    elif request.method == 'PUT':
        data = json.loads(request.body)
        article = Article.objects.get(pk=data['id'])
        article.title = data.get('title', article.title)
        article.save()
        return JsonResponse({'message': '更新成功'})

    elif request.method == 'DELETE':
        data = json.loads(request.body)
        Article.objects.filter(pk=data['id']).delete()
        return JsonResponse({'message': '删除成功'})

    return JsonResponse({'error': '不支持的请求方法'}, status=405)

2.3 常用请求对象属性

def request_info(request):
    """展示request对象的常用属性"""

    # 请求方法
    request.method          # 'GET', 'POST', 'PUT', 'DELETE'等

    # 请求头信息
    request.META            # 所有请求头的字典
    request.META.get('HTTP_USER_AGENT')  # 浏览器信息
    request.META.get('REMOTE_ADDR')      # 客户端IP

    # GET参数(URL查询参数)
    request.GET.get('page', 1)          # ?page=2
    request.GET.getlist('tags')         # ?tags=1&tags=2(多值)

    # POST数据
    request.POST.get('title')           # 表单字段
    request.POST.getlist('categories')  # 多选字段

    # JSON请求体
    import json
    data = json.loads(request.body)

    # 文件上传
    request.FILES.get('avatar')         # 上传的文件对象
    file = request.FILES['document']
    file.name       # 文件名
    file.size       # 文件大小(字节)
    file.content_type  # MIME类型

    # Cookie
    request.COOKIES.get('sessionid')

    # 用户信息(需要认证中间件)
    request.user               # 当前用户对象
    request.user.is_authenticated  # 是否已登录
    request.user.username      # 用户名

三、类视图(CBV)

3.1 基本类视图

from django.views import View
from django.views.generic import (
    ListView, DetailView, CreateView, UpdateView, DeleteView,
    TemplateView
)


class ArticleListView(ListView):
    """文章列表视图"""
    model = Article
    template_name = 'blog/article_list.html'
    context_object_name = 'articles'
    paginate_by = 10  # 每页显示10条

    def get_queryset(self):
        """自定义查询集"""
        queryset = Article.objects.filter(status='published')
        # 支持分类筛选
        category_slug = self.kwargs.get('category_slug')
        if category_slug:
            queryset = queryset.filter(category__slug=category_slug)
        # 支持搜索
        search = self.request.GET.get('q')
        if search:
            queryset = queryset.filter(title__icontains=search)
        return queryset.select_related('category', 'author').prefetch_related('tags')

    def get_context_data(self, **kwargs):
        """添加额外上下文数据"""
        context = super().get_context_data(**kwargs)
        context['categories'] = Category.objects.all()
        context['tags'] = Tag.objects.all()
        context['search'] = self.request.GET.get('q', '')
        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')

    def get_object(self, queryset=None):
        """重写获取对象方法,增加浏览量"""
        article = super().get_object(queryset)
        Article.objects.filter(pk=article.pk).update(views=F('views') + 1)
        # 重新获取更新后的对象
        article.refresh_from_db()
        return article


class ArticleCreateView(CreateView):
    """创建文章视图"""
    model = Article
    template_name = 'blog/article_form.html'
    fields = ['title', 'category', 'tags', 'content', 'status']
    success_url = '/blog/'

    def form_valid(self, form):
        """表单验证通过后,自动设置作者"""
        form.instance.author = self.request.user
        form.instance.slug = slugify(form.instance.title)
        return super().form_valid(form)


class ArticleUpdateView(UpdateView):
    """更新文章视图"""
    model = Article
    template_name = 'blog/article_form.html'
    fields = ['title', 'category', 'tags', 'content', 'status']
    context_object_name = 'article'

    def get_success_url(self):
        return reverse('blog:article_detail', kwargs={'slug': self.object.slug})


class ArticleDeleteView(DeleteView):
    """删除文章视图"""
    model = Article
    success_url = '/blog/'

    def form_valid(self, form):
        messages.success(self.request, '文章删除成功!')
        return super().form_valid(form)

3.2 通用类视图继承关系

# Django通用视图继承层次
View                          # 最基础的视图类
├── TemplateView              # 渲染模板
├── RedirectView              # 重定向
├── ListView                  # 列表展示
├── DetailView                # 详情展示
├── FormView                  # 表单展示
├── CreateView                # 创建记录
├── UpdateView                # 更新记录
└── DeleteView                # 删除记录

# ListView 提供的方法
get_queryset()        # 获取查询集
get_context_data()    # 获取上下文数据
get_paginate_by()     # 获取每页数量
paginate_queryset()   # 分页处理

# DetailView 提供的方法
get_object()          # 获取单个对象
get_context_data()    # 获取上下文数据
get_slug_field()      # 获取slug字段名

3.3 使用Mixin扩展功能

from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin


class LoginRequiredMixin(LoginRequiredMixin):
    """要求用户登录"""
    login_url = '/accounts/login/'
    redirect_field_name = 'next'


class AuthorRequiredMixin:
    """要求当前用户是文章作者"""
    def get_queryset(self):
        return super().get_queryset().filter(author=self.request.user)


class ArticleCreateView(LoginRequiredMixin, CreateView):
    """只有登录用户才能创建文章"""
    model = Article
    fields = ['title', 'category', 'tags', 'content', 'status']

    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)


class ArticleUpdateView(LoginRequiredMixin, AuthorRequiredMixin, UpdateView):
    """只有文章作者才能编辑"""
    model = Article
    fields = ['title', 'category', 'tags', 'content', 'status']

四、URL路由配置

4.1 基本URL配置

# blog/urls.py

from django.urls import path, re_path
from . import views

app_name = 'blog'

urlpatterns = [
    # 基本路径
    path('', views.article_list, name='article_list'),
    path('detail/<slug:slug>/', views.article_detail, name='article_detail'),

    # 类视图URL
    path('list/', views.ArticleListView.as_view(), name='article_list_cbv'),
    path('create/', views.ArticleCreateView.as_view(), name='article_create'),
    path('update/<slug:slug>/', views.ArticleUpdateView.as_view(), name='article_update'),
    path('delete/<int:pk>/', views.ArticleDeleteView.as_view(), name='article_delete'),
]

4.2 路径转换器(Path Converters)

# Django内置路径转换器

# int - 匹配正整数
path('article/<int:id>/', views.article_by_id)

# str - 匹配非空字符串(不含/)
path('category/<str:name>/', views.category_articles)

# slug - 匹配slug字符串(字母、数字、连字符、下划线)
path('article/<slug:slug>/', views.article_detail)

# uuid - 匹配UUID格式
path('doc/<uuid:uid>/', views.document_detail)

# path - 匹配任意字符串(包含/)
path('file/<path:filepath>/', views.serve_file)

4.3 正则表达式路由

# 使用re_path进行正则匹配
from django.urls import re_path

urlpatterns = [
    # 匹配年份和月份
    re_path(r'^archive/(?P<year>\d{4})/(?P<month>\d{2})/$', views.archive),

    # 匹配文章ID(1-99999)
    re_path(r'^article/(?P<pk>\d{1,5})/$', views.article_detail),

    # 匹配多种格式
    re_path(r'^feed\.(json|xml)$', views.feed),
]

4.4 URL反向解析

# 在视图和模板中使用reverse反向解析URL
from django.urls import reverse

# 基本反向解析
url = reverse('blog:article_list')                    # /blog/

# 带参数的反向解析
url = reverse('blog:article_detail', kwargs={'slug': 'django-tutorial'})
# /blog/detail/django-tutorial/

# 带查询参数
url = reverse('blog:article_list') + '?page=2'
# /blog/?page=2

# 在模板中使用
# <a href="{% url 'blog:article_detail' slug=article.slug %}">{{ article.title }}</a>

# 获取当前URL
current_url = request.path
full_url = request.build_absolute_uri()

4.5 URL命名空间

# 项目urls.py - 使用include创建命名空间
urlpatterns = [
    path('blog/', include('blog.urls', namespace='blog')),
    path('shop/', include('shop.urls', namespace='shop')),
    path('admin/', admin.site.urls),
]

# 在视图和模板中通过 命名空间:name 引用
reverse('blog:article_list')
reverse('shop:product_detail', kwargs={'pk': 1})

# 模板中
# {% url 'blog:article_detail' slug=article.slug %}

五、请求与响应对象

5.1 响应类型

from django.http import (
    HttpResponse, JsonResponse, HttpResponseRedirect,
    FileResponse, StreamingHttpResponse, Http404
)
from django.template.loader import render_to_string


# HTML响应
def html_response(request):
    return HttpResponse('<h1>Hello</h1>', content_type='text/html')

# JSON响应
def json_response(request):
    data = {'message': 'success', 'code': 200}
    return JsonResponse(data, safe=False)  # safe=False允许返回非字典对象

# 重定向
def redirect_view(request):
    return HttpResponseRedirect('/blog/')
    # 或使用快捷方式
    return redirect('blog:article_list')

# 文件下载
def download_file(request, filename):
    file_path = os.path.join(settings.MEDIA_ROOT, filename)
    return FileResponse(open(file_path, 'rb'), as_attachment=True)

# 流式响应(大文件)
def stream_file(request):
    def file_generator():
        with open('large_file.zip', 'rb') as f:
            while chunk := f.read(8192):
                yield chunk
    return StreamingHttpResponse(file_generator(), content_type='application/zip')

# 404错误
def custom_404(request):
    raise Http404('页面不存在')

# 自定义状态码
def custom_status(request):
    return HttpResponse('Created', status=201)
    return JsonResponse({'error': '参数错误'}, status=400)

5.2 快捷函数

from django.shortcuts import render, get_object_or_404, get_list_or_404, redirect

# render - 渲染模板
def my_view(request):
    return render(request, 'template.html', {'key': 'value'})
    # 指定内容类型
    return render(request, 'template.html', context, content_type='text/html')

# get_object_or_404 - 获取对象或返回404
article = get_object_or_404(Article, pk=1)
article = get_object_or_404(Article, slug='django', status='published')

# get_list_or_404 - 获取列表或返回404
articles = get_list_or_404(Article, status='published')

# redirect - 重定向
return redirect('/blog/')
return redirect('blog:article_detail', slug='django')
return redirect(article)  # 自动调用get_absolute_url()

六、装饰器

from django.views.decorators.http import require_http_methods, require_GET, require_POST
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.decorators import login_required


# 限制HTTP方法
@require_http_methods(['GET', 'POST'])
def article_form(request):
    pass

@require_GET
def article_list(request):
    pass

@require_POST
def article_create(request):
    pass

# 要求登录
@login_required(login_url='/accounts/login/')
def dashboard(request):
    pass

# 类视图使用装饰器
from django.utils.decorators import method_decorator

@method_decorator(login_required, name='dispatch')
class ArticleCreateView(CreateView):
    model = Article
    fields = ['title', 'content']

# 豁免CSRF(API场景)
@csrf_exempt
def api_endpoint(request):
    pass

七、错误处理

# blog/views.py

def custom_error_404(request, exception):
    """自定义404错误页面"""
    return render(request, 'errors/404.html', status=404)

def custom_error_500(request):
    """自定义500错误页面"""
    return render(request, 'errors/500.html', status=500)

# urls.py 中配置
handler404 = 'blog.views.custom_error_404'
handler500 = 'blog.views.custom_error_500'

八、总结

本章我们学习了Django视图和URL路由的核心知识:

  • 函数视图(FBV)和类视图(CBV)的用法和区别
  • 请求对象(request)的常用属性和方法
  • 多种响应类型和快捷函数
  • URL路由配置:path、re_path、路径转换器
  • URL反向解析和命名空间
  • 装饰器和Mixin的使用

下一章我们将学习 Django模板系统,掌握如何在前端页面中展示动态数据。

发表回复

后才能评论