Django REST framework:SimpleRouter 使用指南

发布于:2025-09-06 ⋅ 阅读:(20) ⋅ 点赞:(0)

1. SimpleRouter 是什么?

SimpleRouter 是 DRF(Django REST framework)提供的路由器,能根据 ViewSet 自动生成标准的 REST 路由,包括:

  • GET /resources/ → 列表(list
  • POST /resources/ → 新建(create
  • GET /resources/{lookup}/ → 详情(retrieve
  • PUT /resources/{lookup}/ → 全量更新(update
  • PATCH /resources/{lookup}/ → 局部更新(partial_update
  • DELETE /resources/{lookup}/ → 删除(destroy

SimpleRouter vs DefaultRouter

  • SimpleRouter:只生成资源路由,不包含“API 根目录”(api root) 页面。
  • DefaultRouter:在 SimpleRouter 基础上多一个“API 根目录”索引页(用于浏览器友好的入口)。

选择建议

  • 你需要简洁、纯粹的 REST 路由:SimpleRouter
  • 你希望有一个根索引页(或给产品/测试同学更友好的浏览入口):DefaultRouter

2. 快速上手(完整示例)

2.1 模型与序列化器

# app/models.py
from django.db import models

class Book(models.Model):
    isbn = models.CharField(max_length=20, unique=True)
    title = models.CharField(max_length=200)
    author = models.CharField(max_length=100)
    pub_date = models.DateField(null=True, blank=True)

    def __str__(self):
        return f"{self.title}({self.isbn})"
# app/serializers.py
from rest_framework import serializers
from .models import Book

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = ["id", "isbn", "title", "author", "pub_date"]

2.2 ViewSet(核心)

# app/views.py
from rest_framework import viewsets, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Book
from .serializers import BookSerializer

class BookViewSet(viewsets.ModelViewSet):
    """
    标准 CRUD + 一个自定义动作(按作者聚合数量)
    """
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

    # 将默认主键 lookup 切换为 ISBN(可选)
    lookup_field = "isbn"
    lookup_url_kwarg = "isbn"  # URL中的参数名(默认与lookup_field相同)

    @action(detail=False, methods=["GET"], url_path="by-author")
    def by_author(self, request):
        """
        GET /books/by-author/
        返回每位作者的图书数量
        """
        from django.db.models import Count
        data = Book.objects.values("author").annotate(count=Count("id")).order_by("-count")
        return Response(list(data))

2.3 路由

# app/urls.py
from django.urls import path, include
from rest_framework.routers import SimpleRouter
from .views import BookViewSet

router = SimpleRouter()
# prefix='books' 会得到 /books/ 与 /books/{isbn}/
# basename 用于反向解析名的前缀,若未传且能从 queryset.model 推断,则可省略
router.register(r"books", BookViewSet, basename="book")

urlpatterns = [
    path("", include(router.urls)),
]
# project/urls.py(项目根 URL)
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/v1/", include("app.urls")),  # 建议加上版本前缀
]

现在可用的路由(示例)

  • GET /api/v1/books/
  • POST /api/v1/books/
  • GET /api/v1/books/{isbn}/
  • PUT /api/v1/books/{isbn}/
  • PATCH /api/v1/books/{isbn}/
  • DELETE /api/v1/books/{isbn}/
  • GET /api/v1/books/by-author/(自定义动作)

3. register() 参数详解

router.register(prefix, viewset, basename=None)
  • prefix:URL 前缀(复数资源名,建议小写、用中划线分词如 user-profiles)。

  • viewset:继承了 ViewSet/ModelViewSet 的类。

  • basename:用于生成路由名称前缀。未提供时,DRF 会尝试从 viewset.queryset.model 推断。

    • 反向解析名形如:<basename>-list<basename>-detail<basename>-<action>

何时必须传 basename:当你的 ViewSet 没有 queryset(例如动态数据源)或无法从中推断模型时,必须显式提供,否则路由注册会报错或反向解析名缺失。


4. 路由规则与反向解析

4.1 自动生成的 URL 与名称

basename="book" 为例:

HTTP 路径 对应方法 反向解析名
GET /books/ list book-list
POST /books/ create book-list
GET /books/{lookup}/ retrieve book-detail
PUT /books/{lookup}/ update book-detail
PATCH /books/{lookup}/ partial_update book-detail
DELETE /books/{lookup}/ destroy book-detail
GET /books/by-author/(示例) @action(detail=False) book-by-author

反向解析示例:

from django.urls import reverse

reverse("book-list")               # -> "/api/v1/books/"
reverse("book-detail", kwargs={"isbn": "9787111123456"})
reverse("book-by-author")          # 自定义动作(list 级别)

5. 常用配置与细节

5.1 结尾斜杠(trailing slash)

  • DRF 提供 DEFAULT_ROUTER_TRAILING_SLASH 设置控制结尾斜杠。

    • 常用取值:

      • "/":强制以斜杠结尾(如 /books/)。
      • "":不带斜杠(如 /books)。
      • "/?":可带可不带(兼容两种风格)。
  • 统一风格非常重要;否则容易出现“有时 301/404、有时匹配不到”的瑕疵。

# settings.py
REST_FRAMEWORK = {
    "DEFAULT_ROUTER_TRAILING_SLASH": "/",
}

与 Django 的 APPEND_SLASH 行为也有关联;团队应统一 API 风格并写入测试。

5.2 自定义主键/查找字段

class BookViewSet(ModelViewSet):
    lookup_field = "isbn"        # 数据库字段
    lookup_url_kwarg = "isbn"    # URL 参数名

如需限制匹配格式(正则),可在 Django 4+ 使用 path converters(推荐)或子类化 Router(高级用法,见 §7.3)。

5.3 命名空间与多应用拆分

# project/urls.py
urlpatterns = [
    path("api/v1/books/", include(("books.urls", "books"), namespace="books")),
    path("api/v1/users/", include(("users.urls", "users"), namespace="users")),
]

# 反向解析(含命名空间)
reverse("books:book-list")

5.4 过滤、分页、权限(与路由并列的重要配置)

# settings.py
REST_FRAMEWORK = {
    "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",
    ],
}
# app/views.py
class BookViewSet(ModelViewSet):
    ...
    filterset_fields = ["author"]   # /books/?author=xxx
    search_fields = ["title", "author"]  # /books/?search=xxx
    ordering_fields = ["pub_date", "title"]  # /books/?ordering=-pub_date

6. 自定义动作(@action)

@action 能在标准 CRUD 之外添加自定义路由。

  • detail=False(集合级别):/books/top10/
  • detail=True(单资源级别):/books/{lookup}/publish/
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status

class BookViewSet(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    lookup_field = "isbn"

    @action(detail=False, methods=["GET"], url_path="top10")
    def top10(self, request):
        qs = Book.objects.order_by("-pub_date")[:10]
        return Response(BookSerializer(qs, many=True).data)

    @action(detail=True, methods=["POST"], url_path="publish")
    def publish(self, request, isbn=None):
        book = self.get_object()
        # ... 执行业务逻辑
        return Response({"isbn": book.isbn, "status": "published"}, status=status.HTTP_200_OK)

反向解析名:

  • book-top10
  • book-publish

7. 进阶:定制 Router 与嵌套路由

7.1 统一前缀与版本

# project/urls.py
from rest_framework.routers import SimpleRouter
from books.views import BookViewSet
from users.views import UserViewSet

router = SimpleRouter()
router.register(r"books", BookViewSet, basename="book")
router.register(r"users", UserViewSet, basename="user")

urlpatterns = [
    path("api/v1/", include(router.urls)),
]

7.2 多个 Router 合并(分应用注册)

# 每个 app 内部维护自己的 router
# app_a/urls.py -> router_a.urls
# app_b/urls.py -> router_b.urls

# project/urls.py
urlpatterns = [
    path("api/v1/", include("app_a.urls")),
    path("api/v1/", include("app_b.urls")),
]

7.3 自定义 Router(修改结尾斜杠、lookup 正则……)

from rest_framework.routers import SimpleRouter

class SlashOptionalRouter(SimpleRouter):
    trailing_slash = "/?"  # 允许有无斜杠都匹配

router = SlashOptionalRouter()
router.register(r"books", BookViewSet, basename="book")

更复杂的情况(如在 URL 中匹配特定格式的 lookup),建议用 path converters(Django 原生方案)或第三方 drf-nested-routers 实现嵌套资源(/authors/{id}/books/{isbn}/)。


8. 测试(强烈建议)

# tests/test_books_api.py
import pytest
from django.urls import reverse
from rest_framework.test import APIClient
from app.models import Book

@pytest.mark.django_db
def test_book_crud_flow():
    client = APIClient()

    # Create
    resp = client.post(reverse("book-list"), {
        "isbn": "9787111123456", "title": "DRF 实战", "author": "Alice"
    }, format="json")
    assert resp.status_code == 201

    # Retrieve
    url = reverse("book-detail", kwargs={"isbn": "9787111123456"})
    resp = client.get(url)
    assert resp.status_code == 200
    assert resp.data["title"] == "DRF 实战"

    # Custom action
    resp = client.get(reverse("book-by-author"))
    assert resp.status_code == 200

反向解析名(如 book-list / book-detail)写测试,可避免路径硬编码带来的回归风险。


9. 常见坑与排错

  1. 反向解析失败:多半是忘记传 basename(且无法从 queryset 推断),或命名空间未匹配(namespace:name)。
  2. 偶发 301/404:团队未统一结尾斜杠策略;请用 DEFAULT_ROUTER_TRAILING_SLASH 一次性约定。
  3. lookup_field 不生效:URL 的 kwargs 名与 lookup_url_kwarg 对不上;或某处仍用默认 pk
  4. 接口未出现在路由ViewSet 方法名不规范(必须是 list/retrieve/...@action);或没有把 router.urls include 进去。
  5. 权限/认证绕过:只在某些方法上声明 permission_classes,其他方法漏配。建议在 ViewSet 级别统一声明,特殊再覆盖。
  6. 前后端联调“接口名不固定”:团队成员直接改 prefixbasename。建议写入规范并加 API 回归测试。

10. 与文档/Schema 配合(可选)

  • 如果你要自动生成 OpenAPI / Swagger:
    推荐 drf-spectaculardrf-yasg;选择 DefaultRouter 可提供一个 root 入口,但不是必须。
  • @action 标注 detailmethodsurl_path 并补充分页/参数注释,文档会更完整。

11. 生产实践建议(Checklist)

  • 按业务域拆分应用;每个 app 内部维护自己的 router,在项目层统一 api/v{n}/ 前缀。
  • 统一 DEFAULT_ROUTER_TRAILING_SLASH;与 Nginx/网关重写规则一致。
  • 所有接口用 反向解析名 做测试与内部调用(避免硬编码路径)。
  • ViewSet 严格用标准方法名(list/retrieve/...)与 @action;自定义动作只做“业务语义上的操作”,避免滥用。
  • 统一权限、限流、分页、过滤策略;默认安全,按需放开。
  • 如需嵌套资源,优先评估是否真的需要;需要时优先用 drf-nested-routers 或清晰的扁平资源 + 查询参数。

12. 速查模板

# urls.py
from django.urls import path, include
from rest_framework.routers import SimpleRouter
from .views import FooViewSet, BarViewSet

router = SimpleRouter()
router.register(r"foos", FooViewSet, basename="foo")
router.register(r"bars", BarViewSet, basename="bar")

urlpatterns = [path("", include(router.urls))]
# views.py
from rest_framework import viewsets, permissions
from rest_framework.decorators import action
from rest_framework.response import Response

class FooViewSet(viewsets.ModelViewSet):
    queryset = Foo.objects.all()
    serializer_class = FooSerializer
    permission_classes = [permissions.IsAuthenticated]
    lookup_field = "slug"

    @action(detail=True, methods=["POST"], url_path="enable")
    def enable(self, request, slug=None):
        foo = self.get_object()
        foo.enable()
        return Response({"ok": True})
# settings.py
REST_FRAMEWORK = {
    "DEFAULT_ROUTER_TRAILING_SLASH": "/",
    "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 20,
}

【完】


网站公告

今日签到

点亮在社区的每一天
去签到