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,
}
【完】