
作者:HelloGitHub-追夢人物
大多數情況下,開發的接口都不是給開發這個接口的人用的,是以如果沒有接口文檔,别人就無法有哪些接口可以調用,即使知道了接口的 URL,也很難知道接口需要哪些參數,即使知道了這些參數,也可能無法了解這些參數的含義。是以接口文檔應該是項目必不可少的配置。
編寫接口文檔有很多種方式,最為簡單直接的方式就是打開一個記事本或者 word 文檔,将接口的詳細資訊和用法寫下來,别人就可以參考這個文檔來調用接口。這樣做雖然簡單,但弊端也很明顯:一是需要寫大量的描述文字,非常枯燥,但其實這些資訊在代碼中已有展現,有點像是使用自然語言又把代碼寫了一遍;二是一旦接口有了更新,就必須手動同步更新接口文檔,開發人員很容易搞忘這件事,導緻接口文檔的内容和接口的實際功能不一緻。
因為很多接口的資訊其實在代碼中已有展現,人們自然而然就想到能否直接從寫好的代碼中自動提取相關資訊來生成文檔,這樣改了代碼,接口文檔也會自動更新,上面說的兩個問題就都可以解決了。
當然寫接口文檔不是搞文學創作,為了直接從寫好的代碼中自動提取資訊來生成文檔,就必須要有一套标準的文檔格式,否則工具無法知道要從代碼中提取出哪些資訊,資訊提取之後,也不知道該如何組織這些資訊。
經過大家的努力,現在已經有了很多成熟的接口文檔标準和生成工具,其中 OpenAPI Specification 就是一個被廣泛接收和使用的标準,我們部落格接口使用的文檔自動化工具,也會基于 OpenAPI 标準從代碼中提取文檔資訊,然後組織為 OpenAPI 的标準格式。
小貼士:
大家更為熟悉的,和 OpenAPI 相關的一個名詞是 swagger。Swagger 提供一系列免費開源的 OpenAPI 相關的工具,他們背後的公司是 SMARTBEAR,号稱 code quality tools 開發行業的上司者。
OpenAPI 介紹
接口文檔不是文學作品,它所需要的内容基本都是固定的。例如對一個 RESTful 風格的接口來說,隻需要知道以下這些關鍵的資訊就足夠完成對它的調用了。反過來,這些資訊也就可以定義一個完整的 RESTful 風格的接口:
- 請求的 HTTP 方法和 URL。
- 接收的參數(包括 URL 中的路徑參數、查詢參數;HTTP 請求頭的參數;HTTP 請求體等參數)。
- 接口傳回的内容。
OpenAPI 對以上資訊進行了标準化,進而提出了 OpenAPI specification,隻要文檔内容符合這個标準,OpenAPI 工具就可以對它進行處理,例如可視化文檔工具就可以讀取文檔内容生成 HTML 格式的文檔。
注意:
OpenAPI specification 目前最新版本是 3,但目前大部分工具對 2 的支援最好,教程中使用的庫僅支援 2。
drf-yasg
drf-yasg 是一個 django 的第三方應用,它可以從 django-rest-framework 架構編寫的代碼中自動提取接口資訊來生成符合 OpenAPI 标準的文檔。我們将使用它來生成部落格應用的接口文檔。
第一步當然是安裝 drf-yasg,進入項目根目錄,運作指令 :
Command Tab
Linux/macOS
$ pipenv install drf-yasg
Windows
...\> pipenv install drf-yasg
然後将 drf-yasg 添加到
INSTALLED_APPS
配置項中:
# filename="blogproject/settings/common.py"
INSTALLED_APPS = [
# 其它已添加的應用...
"pure_pagination", # 分頁
"haystack", # 搜尋
"drf_yasg", # 文檔
]
接着使用 drf_yasg 提供的函數來建立一個 django 視圖,這個視圖将傳回 HTML 格式的文檔内容,這樣我們就可以直接在浏覽器檢視到部落格的接口文檔:
# filename="blogproject/urls.py"
from django.urls import include, path, re_path
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import permissions, routers
schema_view = get_schema_view(
openapi.Info(
title="HelloDjango REST framework tutorial API",
default_version="v1",
description="HelloDjango REST framework tutorial AP",
terms_of_service="",
contact=openapi.Contact(email="[email protected]"),
license=openapi.License(name="GPLv3 License"),
),
public=True,
permission_classes=(permissions.AllowAny,),
)
urlpatterns = [
# 其它已注冊的 URL 模式...
# 文檔
re_path(
r"swagger(?P<format>\.json|\.yaml)",
schema_view.without_ui(cache_timeout=0),
name="schema-json",
),
path(
"swagger/",
schema_view.with_ui("swagger", cache_timeout=0),
name="schema-swagger-ui",
),
path("redoc/", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"),
]
隻需要使用
get_schema_view
就可以生成一個文檔視圖,然後我們将這個視圖函數映射到了 4 個 URL。
現在進入項目根目錄,啟動開發伺服器:
Linux/macOS
$ pipenv run python manage.py runserver
Windows
...\> pipenv run python manage.py runserver
然後通路 http://127.0.0.1:8000/swagger/ 或者 http://127.0.0.1:8000/redoc/,你就可以看到 drf-yasg 自動生成的 HTML 格式的接口文檔了。如果通路 http://127.0.0.1:8000/swagger.json 或者 http://127.0.0.1:8000/swagger.yaml 就可以看到原始的 OpenAPI 标準文檔,swagger 和 redoc 都是基于這個标準文檔來生成可視化的 UI 界面的。
完善文檔
drf-yasg 畢竟不是使用人工智能開發的,即使是使用人工智能,也很難做到 100% 的正确,畢竟由人類寫的代碼可能是千變萬化的,工具無法預料到所有可能的情況,一旦它遇到無法處理的地方,自動生成的文檔就可能出錯,或者生成的内容不符合我們的預期。
我們不妨通路 http://127.0.0.1:8000/swagger/ 先來看看沒做任何定制化之前生成的效果。可以看到内容大體上是正确的,接口基本上都羅列了出來,但是仔細檢查各個接口的内容,就會發現一些問題:
- GET /api-version/test/ 這個接口是我們用來測試的,不希望它顯示在文檔裡。
- 基本上沒有任何描述資訊來說明這個接口的功能。
- 接口的部分參數也沒有描述資訊,可能會讓接口的使用者無法知道其準确含義。
- GET /posts/archive/dates/ 這個接口顯示的參數是錯誤的,它不應該接受任何查詢參數,接口響應參數也是錯誤的。
- GET /posts/{id}/comments/ 這個接口應該還支援分頁查詢的參數,但生成的文檔中沒有列出,接口響應參數也是錯誤的,正确的應該是一個分頁後的評論清單,但文檔中是單個評論對象。
- GET /search/ 沒有列出搜尋參數 text。
- 多出一個 GET /search/{id}/ 接口,這個接口我們并不需要其被使用,是以也無需在文檔列出。
接下來我們就一個個地來解決上面的問題,隻需要稍加改變一下 drf-yasg 的預設行為,就能夠生成我們預期的文檔内容。
隐藏不需要的接口
首先将第 1 點和第 7 點提到的不需要的接口從自動生成的文檔中隐藏。
對于 GET /api-version/test/ 這個接口,它對應的視圖集是
ApiVersionTestViewSet
,給這個視圖集添加一個
swagger_schema
類屬性,将值設為
None
,這樣 drf-yasg 就知道忽略這個視圖集對應的接口了。
# filename="blog/views.py"
class ApiVersionTestViewSet(viewsets.ViewSet): # pragma: no cover
swagger_schema = None
隐藏 GET /search/{id}/ 接口的方式稍微有點不同,因為對應的視圖集
PostSearchView
不隻這一個接口,上面的處理方式會把整個視圖集的接口都隐藏,我們需要想辦法隐藏指定 action 對應的接口。
drf-yasg 提供了一個
swagger_auto_schema
裝飾器來裝飾視圖,隻需要為裝飾器設定
auto_shema=None
就可以讓 drf-yasg 忽略掉被裝飾的視圖,具體用法如下:
# filename="blog/views.py"
from django.utils.decorators import method_decorator
from drf_yasg.utils import swagger_auto_schema
@method_decorator(
name="retrieve",
decorator=swagger_auto_schema(
auto_schema=None,
),
)
class PostSearchView(HaystackViewSet):
index_models = [Post]
serializer_class = PostHaystackSerializer
throttle_classes = [PostSearchAnonRateThrottle]
需要隐藏的接口對應 retrieve 這個 action,是以我們裝飾的是這個方法。因為
PostSearchView
繼承自
HaystackViewSet
,在代碼中并沒有顯示地定義
retrieve
這個方法,而是從父類繼承而來,是以我們借助 django 提供的輔助函數
method_decorator
非侵入式地為類的某個方法添加裝飾器。
現在通路接口文檔位址,可以看到不需要的接口已經從文檔中隐藏了。
添加接口功能描述資訊
接下來解決第 2 個問題,為接口添加必要的功能描述。drf-yasg 支援從視圖的 docstring 解析接口對應的描述資訊,隻要符合指定的格式即可。
先來一個簡單例子,為 GET /categories/ 這個接口添加描述資訊,找到
CategoryViewSet
視圖集,添加格式化的 docstring:
# filename="blog/views.py"
class CategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
"""
部落格文章分類視圖集
list:
傳回部落格文章分類清單
"""
CategoryViewSet
視圖集就一個接口,對應的 action 是
list
,是以 docstring 的格式就像上面那樣,文檔中的效果如下:
可以看到接口請求 URL 下方多出了我們寫的描述内容。其它一些簡單的接口都可以用這種方式來添加功能描述資訊,留作練習的内容交給你自己了。
tip 描述的内容還支援 Markdown 格式,這樣我們可以根據需要寫出格式豐富的内容。
對于稍微複雜一點視圖集,例如
PostViewSet
,這個視圖集含有多個 action 對應多個接口,功能描述資訊的格式差不多是一樣的,關鍵點是指明每個 action 對應的内容:
# filename="blog/views.py"
class PostViewSet(
mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
"""
部落格文章視圖集
list:
傳回部落格文章清單
retrieve:
傳回部落格文章詳情
list_comments:
傳回部落格文章下的評論清單
list_archive_dates:
傳回部落格文章歸檔日期清單
"""
添加參數說明
接着我們來完善接口的參數說明文檔。通過檢視自動生成的文檔中各個接口的參數,發現主要有這麼幾個問題:
- 有些參數沒有說明,無法準确知道其含義。
- 有些接口該有的參數,文檔中沒有列出。
- 有些接口不該有的參數,文檔中卻列出來了。
例如我們可以看到 GET /posts/{id}/ 這個接口的響應參數,其中大部分有中文資訊的描述,我們可以推斷,這些說明都是 drf-yasg 自動從定義在
Post
模型各字段的
verbose_name
參數的值提取的。其中
toc
和 body_html 因為不是
Post
中定義的字段,是以 drf-yasg 無法知道關于這兩個字段的說明。
drf-yasg 是如何知道這個接口會傳回哪些響應參數的呢?原理是 drf-yasg 會嘗試去解析接口對應的序列化器(Serializer),從序列化器中提取出對應的請求和響應字段(如果序列化器中找不到,它會進一步去序列化器關聯的模型中找),是以我們就可以給序列化器中定義的字段添加說明資訊。例如我們來給
toc
和 body_html 添加
label
參數:
# filename="blog/views.py"
class PostRetrieveSerializer(serializers.ModelSerializer):
toc = serializers.CharField(label="文章目錄")
body_html = serializers.CharField(label="文章内容")
通路接口文檔位址,找到對應的接口,可以看到文檔中這兩個字段添加了對應的說明資訊,還可以通過
help_text
(Model 中的字段也支援這個參數)來添加更為詳細的描述,例如:
# filename="blog/serializers.py"
class PostRetrieveSerializer(serializers.ModelSerializer):
toc = serializers.CharField(label="文章目錄", help_text="HTML 格式,每個目錄條目均由 li 标簽包裹。")
body_html = serializers.CharField(
label="文章内容", help_text="HTML 格式,從 `body` 字段解析而來。"
)
這樣兩個字段的含義就非常清晰了,效果如下:
其它一些沒有說明資訊的字段都可以根據這種方式來添加,隻需要找到文檔中的參數在代碼中對應的來源字段就可以了。除了在序列化器(Serializer)、模型(Model)裡面添加。查詢過濾參數也是可以這樣設定的,例如先來看一下 GET /posts/ 的參數:
可以看到用來過濾文章清單的參數都沒有說明,這些字段都定義在
PostFilter
中,我們來改一下代碼,添加必要的說明資訊後再去文檔中看看效果吧!
# filename="blog/filters.py"
from .models import Category, Post, Tag
class PostFilter(drf_filters.FilterSet):
created_year = drf_filters.NumberFilter(
field_name="created_time", lookup_expr="year", help_text="根據文章發表年份過濾文章清單"
)
created_month = drf_filters.NumberFilter(
field_name="created_time", lookup_expr="month", help_text="根據文章發表月份過濾文章清單"
)
category = drf_filters.ModelChoiceFilter(
queryset=Category.objects.all(),
help_text="根據分類過濾文章清單",
)
tags = drf_filters.ModelMultipleChoiceFilter(
queryset=Tag.objects.all(),
help_text="根據标簽過濾文章清單",
)
class Meta:
model = Post
fields = ["category", "tags", "created_year", "created_month"]
接着我們來看 GET /posts/archive/dates/ 和 GET /posts/{id}/comments/ 這兩個接口。前者文檔中顯示了一些錯誤的參數,後者本應該有分頁參數,但是文檔卻沒有列出。
先來看 GET /posts/archive/dates/,它對應的 action 是
list_archive_dates
,由于 action 預設會從它所在的視圖集中繼承一些屬性,而 drf-yasg 會從這些屬性去解析接口支援的參數,例如視圖集設定了
filterset_class = PostFilter
和
pagination_class=PageNumberPagination
(雖然不在視圖集中顯示定義,但在全局進行了配置),在解析
list_archive_dates
的參數時,drf-yasg 錯誤地解析到了從視圖集繼承來的
PostFilter
PageNumberPagination
,是以就把這兩個類中定義的參數也包含進文檔了。
知道了原因,解決方法也就有了,在
list_archive_dates
action 中把這兩個屬性設為
None
,覆寫掉視圖集中的預設設定:
# filename="blog/views.py"
class PostViewSet(
mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
@action(
# ...
filter_backends=None, # 将 filter_backends 設為 None,filterset_class 也就不起作用了。
pagination_class=None,
)
def list_archive_dates(self, request, *args, **kwargs):
# ...
再來看看這個接口,就沒有那些錯誤的參數了。
接着處理 GET /posts/{id}/comments/ 接口,我們需要文檔列出分頁參數。這個接口對應的 action 是
list_comment
。從上面的分析來看,這個 action 明明已經指定了
pagination_class=LimitOffsetPagination
,為什麼 drf-yasg 無法自動檢測到分頁參數呢?原因是這個 action 設定了
detail=True
。當
detial=True
時,drf-yasg 會将這個 action 對應的接口看做擷取單個資源的接口,是以它認為分頁是不需要的。但實際上我們對這個接口進行了定制,它傳回的其實是評論清單。解決辦法是應該告訴 drf-yasg,這個接口傳回的是清單結果,請去解析清單接口相關的一些參數:
# filename="blog/views.py"
class PostViewSet(
mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
@action(
methods=["GET"],
detail=True,
# ...
suffix="List", # 将這個 action 傳回的結果标記為清單,否則 drf-yasg 會根據 detail=True 誤判為這是傳回單個資源的接口
pagination_class=LimitOffsetPagination,
serializer_class=CommentSerializer,
)
def list_comments(self, request, *args, **kwargs):
# ...
但是 drf-yasg 還是不夠聰明,當它去解析清單接口可能的參數時,順便又把
PostFilter
中的字段也一并解析了,這是用來過濾部落格文章的,顯然不能用于過濾評論清單,我們需要将這些無關參數移除,解決方法在處理 GET /posts/archive/dates/ 接口時就講過了,把
filter_backends
設定成 None 就可以了。
更正錯誤的響應參數
仔細看生成的接口文檔,發現有 2 個接口的傳回内容是錯誤的。
一是 GET /posts/{id}/comments/,最初我們發現這個接口文檔的響應是一個單一的評論對象,原因我們上面也分析了,drf-yasg 根據
detail=True
誤地将其作為傳回單一資源的接口處理了。随着為其添加更多資訊,告訴 drf-yasg 這是一個傳回資源清單的接口,問題也就順便解決了。
二是 GET /posts/archive/dates/,這個接口的傳回内容應該是一個日期清單,但是文檔中顯示的竟然是部落格文章清單。drf-yasg 推斷的響應類型是正确的,但内容不對。原因也很明顯,這個接口對應的 action 是
list_archive_dates
,drf-yasg 在這個 action 中沒有找到解析響應結果的序列化器(Serializer),是以它跑去視圖集
PostViewSet
中去找了,結果找到了
PostListSerializer
,然後把這個當成了接口傳回的内容進行解析了。
由于這個接口傳回的僅僅是一個簡單的日期清單,并不涉及到序列化器,是以這裡我們不使用指定
serializer_class
屬性值的方式,而是使用
swagger_auto_schema
裝飾器,直接告訴 drf-yasg 接口傳回的響應:
# filename="blog/views.py"
class PostViewSet(
mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
@swagger_auto_schema(responses={200: "歸檔日期清單,時間倒序排列。例如:['2020-08', '2020-06']"})
@action(
methods=["GET"],
detail=False,
url_path="archive/dates",
url_name="archive-date",
filter_backends=None,
pagination_class=None,
)
def list_archive_dates(self, request, *args, **kwargs):
# ...
responses
參數的值是一個字典,字典的鍵是 HTTP 響應碼,值可以是一個序列化器,這樣 drf-yasg 會拿這個序列化器去解析接口響應的參數;也可以是一個字元串,drf-yasg 會把字元串直接當做接口響應結果寫入文檔中。看看修改後的效果:
至此,我們就有了一套比較完善的部落格接口文檔了,而且大部分内容均由 drf-yasg 為我們自動生成,省去了不少手寫文檔的麻煩。
參考資料
以下是教程中用到的一些參考:
- OpenAPI Specification
- drf-yasg 源碼倉庫
- drf-yasg 官方文檔
drf-yasg 的官方文檔對于這個庫的使用方法寫的不是很清晰,這篇文章中列出的一些用法都是從源碼中看出來的。如果你在使用過程中遇到了問題,首先嘗試分析問題的原因,然後順藤摸瓜去找到相關的源碼,看看庫的内部是如何處理你所遇到的問題的,這樣就可以針對性地給出解決方案了,這篇教程中列出的很多問題以及最後給出的解決方案,都是使用的這種方式。
關注公衆号加入我們
作者:削微寒
掃描左側的二維碼可以聯系到我
本作品采用署名-非商業性使用-禁止演繹 4.0 國際 進行許可。