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 modelsclass 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 Bookclass BookSerializer(serializers.ModelSerializer):class Meta:model = Bookfields = ["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 BookSerializerclass BookViewSet(viewsets.ModelViewSet):"""标准 CRUD + 一个自定义动作(按作者聚合数量)"""queryset = Book.objects.all()serializer_class = BookSerializerpermission_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 Countdata = 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 BookViewSetrouter = 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, includeurlpatterns = [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 reversereverse("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=xxxsearch_fields = ["title", "author"] # /books/?search=xxxordering_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 statusclass BookViewSet(ModelViewSet):queryset = Book.objects.all()serializer_class = BookSerializerlookup_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 UserViewSetrouter = 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 SimpleRouterclass 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()# Createresp = client.post(reverse("book-list"), {"isbn": "9787111123456", "title": "DRF 实战", "author": "Alice"}, format="json")assert resp.status_code == 201# Retrieveurl = reverse("book-detail", kwargs={"isbn": "9787111123456"})resp = client.get(url)assert resp.status_code == 200assert resp.data["title"] == "DRF 实战"# Custom actionresp = client.get(reverse("book-by-author"))assert resp.status_code == 200
用 反向解析名(如
book-list
/book-detail
)写测试,可避免路径硬编码带来的回归风险。
9. 常见坑与排错
- 反向解析失败:多半是忘记传
basename
(且无法从queryset
推断),或命名空间未匹配(namespace:name
)。 - 偶发 301/404:团队未统一结尾斜杠策略;请用
DEFAULT_ROUTER_TRAILING_SLASH
一次性约定。 lookup_field
不生效:URL 的kwargs
名与lookup_url_kwarg
对不上;或某处仍用默认pk
。- 接口未出现在路由:
ViewSet
方法名不规范(必须是list/retrieve/...
或@action
);或没有把router.urls
include 进去。 - 权限/认证绕过:只在某些方法上声明
permission_classes
,其他方法漏配。建议在ViewSet
级别统一声明,特殊再覆盖。 - 前后端联调“接口名不固定”:团队成员直接改
prefix
或basename
。建议写入规范并加 API 回归测试。
10. 与文档/Schema 配合(可选)
- 如果你要自动生成 OpenAPI / Swagger:
推荐 drf-spectacular 或 drf-yasg;选择 DefaultRouter 可提供一个 root 入口,但不是必须。 - 为
@action
标注detail
、methods
、url_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, BarViewSetrouter = 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 Responseclass FooViewSet(viewsets.ModelViewSet):queryset = Foo.objects.all()serializer_class = FooSerializerpermission_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,
}
【完】