Django从零开始(七):REST Framework API开发

一、Django REST Framework简介

Django REST Framework(简称DRF)是构建RESTful API的最流行的Django第三方库。它提供了一套完整的工具链,让你能够快速构建强大且灵活的Web API。

DRF核心特性

  • 序列化器(Serializer):自动将模型实例转换为JSON/XML等格式
  • 视图集(ViewSet):一行代码实现CRUD操作
  • 认证与权限:Token、JWT、OAuth等多种认证方式
  • 分页与过滤:内置分页和过滤支持
  • 可浏览API:自带美观的API文档界面
  • 限流(Throttling):防止API被滥用

二、安装与配置

# 安装DRF
pip install djangorestframework
pip install markdown       # 可浏览API支持Markdown
pip install django-filter  # 过滤支持

# settings.py
INSTALLED_APPS = [
    ...
    'rest_framework',
    'rest_framework.authtoken',  # Token认证
    'django_filters',            # 过滤
]

# DRF全局配置
REST_FRAMEWORK = {
    # 默认认证方式
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.TokenAuthentication',
    ],
    # 默认权限
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticatedOrReadOnly',
    ],
    # 分页
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 20,
    # 过滤
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',
        'rest_framework.filters.SearchFilter',
        'rest_framework.filters.OrderingFilter',
    ],
    # 限流
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/hour',
        'user': '1000/hour',
    },
    # 日期时间格式
    'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S',
    'DEFAULT_RENDERER_CLASSES': [
        'rest_framework.renderers.JSONRenderer',
        'rest_framework.renderers.BrowsableAPIRenderer',
    ],
}

三、序列化器(Serializer)

3.1 ModelSerializer

# blog/serializers.py

from rest_framework import serializers
from .models import Article, Category, Tag, Comment


class CategorySerializer(serializers.ModelSerializer):
    """分类序列化器"""
    article_count = serializers.IntegerField(read_only=True)

    class Meta:
        model = Category
        fields = ['id', 'name', 'slug', 'description', 'article_count', 'created_at']
        read_only_fields = ['slug', 'created_at']


class TagSerializer(serializers.ModelSerializer):
    """标签序列化器"""
    class Meta:
        model = Tag
        fields = ['id', 'name', 'slug', 'color']


class ArticleListSerializer(serializers.ModelSerializer):
    """文章列表序列化器(精简版)"""
    category_name = serializers.CharField(source='category.name', read_only=True)
    author_name = serializers.CharField(source='author.username', read_only=True)
    tags = TagSerializer(many=True, read_only=True)

    class Meta:
        model = Article
        fields = [
            'id', 'title', 'slug', 'summary', 'category_name',
            'author_name', 'tags', 'views', 'status',
            'published_at', 'created_at',
        ]


class ArticleDetailSerializer(serializers.ModelSerializer):
    """文章详情序列化器(完整版)"""
    category = CategorySerializer(read_only=True)
    category_id = serializers.PrimaryKeyRelatedField(
        queryset=Category.objects.all(),
        source='category',
        write_only=True,
        required=False,
    )
    author_name = serializers.CharField(source='author.username', read_only=True)
    tags = TagSerializer(many=True, read_only=True)
    tag_ids = serializers.PrimaryKeyRelatedField(
        queryset=Tag.objects.all(),
        source='tags',
        many=True,
        write_only=True,
        required=False,
    )
    comments_count = serializers.SerializerMethodField()

    class Meta:
        model = Article
        fields = [
            'id', 'title', 'slug', 'content', 'summary',
            'category', 'category_id', 'author_name',
            'tags', 'tag_ids', 'comments_count',
            'views', 'status', 'is_top',
            'published_at', 'created_at', 'updated_at',
        ]
        read_only_fields = ['slug', 'views', 'author', 'created_at', 'updated_at']

    def get_comments_count(self, obj):
        return obj.comments.filter(is_approved=True).count()

    def validate_title(self, value):
        if len(value) < 5:
            raise serializers.ValidationError('标题至少需要5个字符')
        return value


class CommentSerializer(serializers.ModelSerializer):
    """评论序列化器"""
    author_name = serializers.CharField(source='nickname', read_only=True)
    replies = serializers.SerializerMethodField()

    class Meta:
        model = Comment
        fields = [
            'id', 'article', 'parent', 'nickname', 'email',
            'content', 'is_approved', 'replies', 'created_at',
        ]
        read_only_fields = ['is_approved', 'created_at']

    def get_replies(self, obj):
        if obj.replies.exists():
            return CommentSerializer(obj.replies.all(), many=True).data
        return []

3.2 自定义字段和验证

class ArticleCreateSerializer(serializers.ModelSerializer):
    """创建文章的序列化器"""

    class Meta:
        model = Article
        fields = ['title', 'content', 'summary', 'category', 'tags', 'status']

    def validate(self, attrs):
        # 已发布文章必须有分类
        if attrs.get('status') == 'published' and not attrs.get('category'):
            raise serializers.ValidationError({
                'category': '发布文章必须选择分类'
            })
        return attrs

    def create(self, validated_data):
        # 自动设置作者为当前用户
        validated_data['author'] = self.context['request'].user
        validated_data['slug'] = slugify(validated_data['title'])
        tags = validated_data.pop('tags', [])
        article = Article.objects.create(**validated_data)
        article.tags.set(tags)
        return article

    def update(self, instance, validated_data):
        tags = validated_data.pop('tags', None)
        for attr, value in validated_data.items():
            setattr(instance, attr, value)
        instance.save()
        if tags is not None:
            instance.tags.set(tags)
        return instance

四、视图集(ViewSet)

4.1 ModelViewSet

# blog/api_views.py

from rest_framework import viewsets, status, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticatedOrReadOnly, IsAuthenticated
from django_filters.rest_framework import DjangoFilterBackend
from .models import Article, Category, Tag, Comment
from .serializers import (
    ArticleListSerializer, ArticleDetailSerializer,
    ArticleCreateSerializer, CategorySerializer,
    TagSerializer, CommentSerializer,
)
from .permissions import IsAuthorOrReadOnly
from .filters import ArticleFilter


class ArticleViewSet(viewsets.ModelViewSet):
    """文章API视图集"""
    queryset = Article.objects.select_related('category', 'author').prefetch_related('tags')
    permission_classes = [IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly]
    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
    filterset_class = ArticleFilter
    search_fields = ['title', 'content']
    ordering_fields = ['created_at', 'views', 'published_at']
    ordering = ['-created_at']

    def get_serializer_class(self):
        """根据动作选择序列化器"""
        if self.action == 'list':
            return ArticleListSerializer
        elif self.action == 'retrieve':
            return ArticleDetailSerializer
        elif self.action in ['create', 'update', 'partial_update']:
            return ArticleCreateSerializer
        return ArticleDetailSerializer

    def perform_create(self, serializer):
        """创建时自动设置作者"""
        serializer.save(author=self.request.user)

    @action(detail=True, methods=['post'])
    def publish(self, request, pk=None):
        """发布文章"""
        article = self.get_object()
        article.status = 'published'
        article.published_at = timezone.now()
        article.save()
        return Response({'status': 'published'})

    @action(detail=True, methods=['post'])
    def like(self, request, pk=None):
        """点赞"""
        article = self.get_object()
        Article.objects.filter(pk=article.pk).update(likes=F('likes') + 1)
        return Response({'status': 'liked'})

    @action(detail=False, methods=['get'])
    def popular(self, request):
        """热门文章"""
        articles = self.get_queryset().filter(
            status='published'
        ).order_by('-views')[:10]
        serializer = ArticleListSerializer(articles, many=True)
        return Response(serializer.data)

    @action(detail=False, methods=['get'], url_path='archive')
    def archive(self, request):
        """归档统计"""
        from django.db.models import Count
        archives = Article.objects.filter(
            status='published'
        ).dates('created_at', 'month', order='DESC')
        data = []
        for date in archives:
            count = Article.objects.filter(
                created_at__year=date.year,
                created_at__month=date.month,
                status='published',
            ).count()
            data.append({'date': date.strftime('%Y年%m月'), 'count': count})
        return Response(data)


class CategoryViewSet(viewsets.ModelViewSet):
    """分类API"""
    queryset = Category.objects.annotate(article_count=Count('articles'))
    serializer_class = CategorySerializer
    permission_classes = [IsAuthenticatedOrReadOnly]
    search_fields = ['name']


class TagViewSet(viewsets.ModelViewSet):
    """标签API"""
    queryset = Tag.objects.all()
    serializer_class = TagSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]
    search_fields = ['name']


class CommentViewSet(viewsets.ModelViewSet):
    """评论API"""
    queryset = Comment.objects.all()
    serializer_class = CommentSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]

    def get_queryset(self):
        return super().get_queryset().filter(is_approved=True)

    def perform_create(self, serializer):
        serializer.save()

4.2 自定义权限

# blog/permissions.py

from rest_framework import permissions


class IsAuthorOrReadOnly(permissions.BasePermission):
    """只有作者可以修改,其他人只能查看"""
    def has_object_permission(self, request, view, obj):
        # 读取权限允许任何请求
        if request.method in permissions.SAFE_METHODS:
            return True
        # 写入权限只允许作者
        return obj.author == request.user


class IsAdminOrReadOnly(permissions.BasePermission):
    """管理员可写,其他人只读"""
    def has_permission(self, request, view):
        if request.method in permissions.SAFE_METHODS:
            return True
        return request.user and request.user.is_staff

4.3 过滤器

# blog/filters.py

import django_filters
from .models import Article


class ArticleFilter(django_filters.FilterSet):
    """文章过滤器"""
    title = django_filters.CharFilter(lookup_expr='icontains')
    status = django_filters.ChoiceFilter(choices=Article.STATUS_CHOICES)
    category = django_filters.NumberFilter(field_name='category__id')
    category_slug = django_filters.CharFilter(field_name='category__slug')
    tag = django_filters.NumberFilter(field_name='tags__id')
    date_from = django_filters.DateTimeFilter(field_name='created_at', lookup_expr='gte')
    date_to = django_filters.DateTimeFilter(field_name='created_at', lookup_expr='lte')
    min_views = django_filters.NumberFilter(field_name='views', lookup_expr='gte')

    class Meta:
        model = Article
        fields = ['status', 'category', 'tag']

五、路由注册

# blog/api_urls.py

from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .api_views import ArticleViewSet, CategoryViewSet, TagViewSet, CommentViewSet

router = DefaultRouter()
router.register(r'articles', ArticleViewSet, basename='article')
router.register(r'categories', CategoryViewSet, basename='category')
router.register(r'tags', TagViewSet, basename='tag')
router.register(r'comments', CommentViewSet, basename='comment')

urlpatterns = [
    path('', include(router.urls)),
]

# 项目urls.py
urlpatterns += [
    path('api/', include('blog.api_urls')),
]

注册后自动生成的API端点:

方法 URL 功能
GET /api/articles/ 文章列表
POST /api/articles/ 创建文章
GET /api/articles/{id}/ 文章详情
PUT /api/articles/{id}/ 完整更新
PATCH /api/articles/{id}/ 部分更新
DELETE /api/articles/{id}/ 删除文章
POST /api/articles/{id}/publish/ 发布文章
GET /api/articles/popular/ 热门文章

六、JWT认证

# 安装
pip install djangorestframework-simplejwt

# settings.py
from datetime import timedelta

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(hours=2),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': True,
    'AUTH_HEADER_TYPES': ('Bearer',),
}

# urls.py
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
    TokenVerifyView,
)

urlpatterns += [
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    path('api/token/verify/', TokenVerifyView.as_view(), name='token_verify'),
]

使用JWT访问API:

# 获取Token
curl -X POST http://localhost:8000/api/token/ \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "password123"}'

# 返回
{
    "access": "eyJhbGciOiJIUzI1NiIs...",
    "refresh": "eyJhbGciOiJIUzI1NiIs..."
}

# 使用Token访问API
curl http://localhost:8000/api/articles/ \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

# 刷新Token
curl -X POST http://localhost:8000/api/token/refresh/ \
  -H "Content-Type: application/json" \
  -d '{"refresh": "eyJhbGciOiJIUzI1NiIs..."}'

七、API文档自动生成

# 安装 drf-spectacular(推荐,支持OpenAPI 3.0)
pip install drf-spectacular

# settings.py
INSTALLED_APPS += ['drf_spectacular']

REST_FRAMEWORK = {
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}

SPECTACULAR_SETTINGS = {
    'TITLE': 'Blog API',
    'DESCRIPTION': '博客系统API文档',
    'VERSION': '1.0.0',
    'SERVE_INCLUDE_SCHEMA': False,
}

# urls.py
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView

urlpatterns += [
    path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
    path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
    path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
]

访问 http://localhost:8000/api/docs/ 即可看到交互式API文档界面。

八、总结

本章我们学习了使用DRF构建RESTful API:

  • DRF安装配置和全局设置
  • 序列化器(Serializer)的设计和使用
  • 视图集(ViewSet)快速实现CRUD
  • 自定义权限、过滤器
  • JWT认证实现
  • API文档自动生成

下一章我们将学习 Django中间件、信号与缓存,掌握高级请求处理和性能优化技巧。

发表回复

后才能评论