我們的博客有一個側邊欄功能,分別列出博客文章的分類列表、標簽列表、歸檔時間列表,通過點擊側邊欄對應的條目,還可以進入相應的頁面。例如點擊某個分類,博客將跳轉到該分類下全部文章列表頁面。這些數據的展示都需要開發對應的接口,以便前端調用獲取數據。
分類列表、標簽列表實現比較簡單,我們這里給出接口的設計規范,大家可以使用前幾篇教程中學到的知識點輕松實現(具體實現可參考 GtiHub 上的源代碼)。
分類列表接口: /categories/
標簽列表接口:/tags/
歸檔日期列表的接口實現稍微復雜一點,因為我們需要從已有文章中歸納文章發表日期。事實上,我們在上一部教程 HelloDjango - Django博客教程(第二版)的 頁面側邊欄:使用自定義模板標簽 已經講解了如何獲取歸檔日期列表,只是當時返回的歸檔日期列表直接用於模板的渲染,而這里我們需要將歸檔日期列表序列化后通過 API 接口返回。
具體來說,獲取博客文章發表時間歸檔列表的方法是調用查詢集(QuerySet)的 dates
方法,提取記錄中的日期。核心代碼就一句:
Post.objects.dates('created_time', 'month', order='DESC')
這里 Post.objects.dates
方法會返回一個列表,列表中的元素為每一篇文章(Post)的創建日期(已去重),日期都是 Python 的 date
對象,精確到月份,降序排列。
有了返回的歸檔日期列表,接下來就實現相應的 API 接口視圖函數:
blog/views.py
from rest_framework import mixins, status, viewsets
from rest_framework.decorators import action
from rest_framework.serializers import DateField
class PostViewSet(
mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
# ...
@action(
methods=["GET"], detail=False, url_path="archive/dates", url_name="archive-date"
)
def list_archive_dates(self, request, *args, **kwargs):
dates = Post.objects.dates("created_time", "month", order="DESC")
date_field = DateField()
data = [date_field.to_representation(date) for date in dates]
return Response(data=data, status=status.HTTP_200_OK)
注意這里我們涉及到了幾個以前沒有詳細講解過的用法。
一是 action
裝飾器,它用來裝飾一個視圖集中的方法,被裝飾的方法會被 django-rest-framework 的路由自動注冊為一個 API 接口。
回顧一下我們之前在使用視圖集 viewset 時提到過 action(動作)的概念,django-rest-framework 預定義了幾個標准的動作,分別為 list 獲取資源列表,retrieve 獲取單個資源、update 和 partial_update 更新資源、destroy 刪除資源,這些 action 具體的實現方法,分別由 mixins 模塊中的混入類提供。例如 用類視圖實現首頁 API 中我們介紹過 mixins.ListModelMixin
,這個混入類提供了 list 動作對應的標准實現,即 list 方法。視圖集中所有以上提及的以標准動作命名的方法,都會被 django-rest-framework 的路由自動注冊為標准的 API 接口。
django-rest-framework 默認只能識別標准命名的視圖集方法並將其注冊為 API,但我們可以添加更多非標准的 action,而為了讓 django-rest-framework 能夠識別這些方法,就需要使用 action
裝飾器進行裝飾。
其實我們可以簡單地將 action 裝飾的方法看作是一個視圖函數的實現,因此可以看到方法傳入的第一個參數為 request 請求對象,函數體就是這個視圖函數需要執行的邏輯,顯然,方法最終必須要返回一個 HTTP 響應對象。
action 裝飾器通常用於在視圖集中添加額外的接口實現。例如這里我們已有了 PostViewSet
視圖集,標准的 list 實現了獲取文章資源列表的邏輯。我們想添加一個獲取文章歸檔日期列表的接口,因此添加了一個 list_archive_dates
方法,並使用 action 進行裝飾。通常如果要在視圖集中添加額外的接口實現,可以使用如下的模板代碼:
@action(
methods=["allowed http method name"],
detail=False or True,
url_path="url/path",
url_name="url name"
)
def method_name(self, request, *args, **kwargs):
# 接口邏輯的具體實現,返回一個 Response
通常 action 裝飾器以下 4 個參數都會設置:
methods:一個列表,指定訪問這個接口時允許的 HTTP 方法(GET、POST、PUT、PATCH、DELETE)
detail:True 或者 False。設置為 True,自動注冊的接口 URL 中會添加一個 pk 路徑參數(請看下面的示例),否則不會。
url_path:自動注冊的接口 URL。
url_name:接口名,主要用於通過接口名字反解對應的 URL。
當然,我們還可以在 action 中設置所有 ViewSet
類所支持的類屬性,例如 serializer_class
、pagination_class
、permission_classes
等,用於覆蓋類視圖中設置的屬性值。
以上是 action 用法的一個基本介紹,現在來分析一下 list_archive_dates
這個 action 來加深理解。
methods
參數指定接口需要通過 GET 方法訪問,detail 為 False
,url_path
設置為 archive/dates,因此最終自動生成的接口路由就是 /posts/archive/dates/。如果我們設置 detail 為 True,那么生成的接口路由就是 /posts/<int:pk>/archive/dates/
,生成的 URL 中就會多一個 pk 路徑參數。
list_archive_dates
具體的實現邏輯中,以下幾點需要注意:
一是獨立使用序列化字段(Field)。之前序列化字段都是在序列化器(Serializer)里面使用的,因為通常來說接口需要序列化一個對象的多個字段。而這個接口中只需要序列化一個時間字段(類型為 Python 標准庫中的 datetime.date
),所以沒必要單獨定義一個序列化器了,直接拿 django-rest-framework 提供的用於序列化時間類型的 DateField
就可以了。用法也很簡單,實例化序列化字段,調用其 to_representation
方法,將需要序列化的值傳入即可(其實序列化器在序列對象的多個字段時,內部也是分別調用對應序列化字段的 to_representation
方法)。
我們通過列表推導式生成一個序列化后的歸檔日期列表,這個列表是可被序列化的。接着我們在接口返回一個 Response
, Response
將序列化后的結果包裝返回(保存在 data 屬性中),django-rest-framework 會進一步幫我們把這個 Response
中包含的數據解析為合適的格式(例如 JSON)。
status=status.HTTP_200_OK
指定這個接口返回的狀態碼,HTTP_200_OK
是一個預定義的常數,即 200。django-rest-framework 將常用 HTTP 請求的狀態碼常數預定義 status 模塊里,使用預定義的變量而不是直接使用數字的好處一是增強代碼可讀性,二是減少硬編碼。
由於 PostViewSet
視圖集已經通過 django-rest-framework 的路由進行了注冊,因此 list_archive_dates
也會被連帶着自動注冊為一個接口。啟動開發服務器,訪問 /posts/archive/dates/,就可以看到返回的文章歸檔日期列表。

注意到紅框圈出部分,django-rest-framework API 交互后台會識別到額外定義的 action 並將它們展示出來,點擊就可以進入到相應的 API 頁面。
現在,側邊欄所需要的數據接口就開發完成了,接下來實現返回某一分類、標簽或者歸檔日期下的文章列表接口。
在 使用視圖集簡化代碼 我們開發了獲取全部文章的接口。事實上,分類、標簽或者歸檔日期文章列表的 API,本質上還是返回一個文章列表資源,只不過比首頁 API 返回的文章列表資源多了個“過濾”,只過濾出了指定的部分文章而已。對於這樣的場景,我們可以在請求 API 時加上查詢參數,django-rest-framework 解析查詢參數,然后從全部文章列表中過濾出查詢所指定的文章列表再返回。
這在 RESTful API 的設計中肯定是會遇到的,因此第三方庫 django-filter 幫我們實現了上述所說的查詢過濾功能,而且和 django-rest-framework 有很好的集成,我們可以在 django-rest-framework 中非常方便地使用 django-filter。
既然要使用它,當然是先安裝它(已安裝跳過):pipenv install django-filter
接着我們來配置 PostViewSet
,為其設置用於過濾返回結果集的一些屬性,代碼如下:
from django_filters.rest_framework import DjangoFilterBackend
from .filters import PostFilter
class PostViewSet(
mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
# ...
filter_backends = [DjangoFilterBackend]
filterset_class = PostFilter
非常的簡單,僅僅設置了 filter_backends
和 filterset_class
兩個屬性。其中 filter_backends
設置為 DjangoFilterBackend
,這樣 API 在返回結果時, django-rest-framework 會調用設置的 backend(這里是 DjangoFilterBackend
) 的 filter
方法對 get_queryset
方法返回的結果進行進一步的過濾,而 DjangoFilterBackend
會依據 filterset_class
(這里是 PostFilter
)中定義的過濾規則來過濾查詢結果集。
當然 PostFilter
還沒有定義,我們來定義它。首先在 blog 應用下創建一個 filters.py 文件,用於存放自定義 filter 的代碼,PostFilter
代碼如下:
from django_filters import rest_framework as drf_filters
from .models import Post
class PostFilter(drf_filters.FilterSet):
created_year = drf_filters.NumberFilter(
field_name="created_time", lookup_expr="year"
)
created_month = drf_filters.NumberFilter(
field_name="created_time", lookup_expr="month"
)
class Meta:
model = Post
fields = ["category", "tags", "created_year", "created_month"]
PostFilter
的定義和序列化器 Serializer 非常類似。
category
,tags
兩個過濾字段因為是 Post
模型中定義的字段,因此 django-filter 可以自動推斷其過濾規則,只需要在 Meta.fields
中聲明即可。
歸檔日期下的文章列表,我們設計的接口傳遞 2 個查詢參數:年份和月份。由於這兩個字段在 Post
中沒有定義,Post
記錄時間的字段為 created_time
,因此我們需要顯示地定義查詢規則,定義的規則是:
查詢參數名 = 查詢參數值的類型(查詢的模型字段,查詢表達式)
例如示例中定義的 created_year
查詢參數,查詢參數值的類型為 number,即數字,查詢的模型字段為 created_time
,查詢表達式是 year
。當用戶傳遞 created_year
查詢參數時,django-filter 實際上會將以上定義的規則翻譯為如下的 ORM 查詢語句:
Post.objects.filter(created_time__year=created_year傳遞的值)
現在回到 API 交互后台,先進到 /post/ 接口下,默認返回了全部文章列表。可以看到右上角多了個過濾器(紅框圈出部分)。
點擊會彈出過濾參數輸入的交互面板,在這里可以交互式地輸入查詢過濾參數的值。
例如選擇如下的過濾參數,得到查詢的 URL 為:
http://127.0.0.1:10000/api/posts/?category=1&tags=1&created_year=2020&created_month=1
這條查詢返回創建於 2020 年 1 月,id 為 1 的分類下,id 為 1 的標簽下的全部文章。
通過不同的查詢參數組合,就可以得到不同的文章資源列表了。
關注公眾號加入交流群