第 13 篇:DRF 框架之 API 版本管理

削微寒發表於2020-07-24

作者:HelloGitHub-追夢人物

API 不可能一成不變,無論是新增或者刪除已有 API,都會對呼叫它的客戶端產生影響。如果對 API 的增刪沒有管理,隨著 API 的增增減減,呼叫它的客戶端就會逐漸陷入迷茫,到底哪個 API 是可用的?為什麼之前可用的 API 又不可用了,新增了哪些 API 可以使用?為了方便 API 的管理,我們引入版本功能。

給 API 打上版本號,在某個特定版本下,原來已有的 API 總是可用的。如果要對 API 做重大變更,可以釋出一個新版本的 API,並及時提醒使用者 API 已變更,敦促使用者遷移到新的 API,這樣可以給客戶端提供一個緩衝過渡期,不至於昨天能用的 API,今天突然報錯了。

django-rest-framework 提供了多個 API 版本輔助類,分別實現不同的 API 版本管理方式。比較實用的有:

AcceptHeaderVersioning

這個類要求客戶端在 HTTP 的 Accept 請求頭加上版本號以表明想請求的 API 版本,例如如下請求:

GET /bookings/ HTTP/1.1
Host: example.com
Accept: application/json; version=1.0

這將請求版本號為 1.0 的介面。

URLPathVersioning

這個類要求客戶端在請求的 url 中指定版本號,一個缺點是你在書寫 URL 模式時,必須包含關鍵字為 version 的模式,例如官網的一個例子:

urlpatterns = [
    url(
        r'^(?P<version>(v1|v2))/bookings/$',
        bookings_list,
        name='bookings-list'
    ),
    url(
        r'^(?P<version>(v1|v2))/bookings/(?P<pk>[0-9]+)/$',
        bookings_detail,
        name='bookings-detail'
    )
]

這樣的話很不方便,因此我們一般不使用。

NamespaceVersioning

和上面提到的 URLPathVersioning 類似,只不過版本號不是在 URL 模式中指定,而是通過 namespace 引數指定 (稍後我們將看到它的具體用法)。

當然,django-rest-framework 還提供了其它諸如 HostNameVersioningQueryParameterVersioning 的版本管理輔助類,可自行檢視文件瞭解:https://www.django-rest-framework.org/api-guide/versioning/

綜合來看,NamespaceVersioning 模式便於 URL 的設計與管理,因此我們的部落格應用決定採用這種 API 版本管理方式。

為了開啟 api 版本管理,在專案的配置中加入如下配置:

settings/common.py

REST_FRAMEWORK = {
    'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning',
    'DEFAULT_VERSION': 'v1'
}

以上兩項設定分別全域性指定使用的 API 版本管理方式和客戶端預設版本號的情況下預設請求的 API 版本。儘管這些配置項也可以在單個檢視或者檢視集的範圍內指定,但是,統一的版本管理模式更為可取,因此我們在全域性配置中指定。

接著在註冊的 API 介面前帶上版本號:

blogproject/urls.py

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

注意這裡比之前多了個 namespace 引數,引數值為 v1,代表包含的 URL 模式均屬於 v1 這個名稱空間。還有一點需要注意,對於 include 函式,如果指定了 namespace 的值,第一個引數必須是一個元組,形式為:(url_patterns, app_name),這裡我們將 app_name 指定為 api。

一旦我們開啟了版本管理,所有請求物件 request 就會多出一個屬性 version,其值為使用者請求的版本號(如果沒有指定,就為預設的 DEFAULT_VERSION 的值)。因此,我們可以在請求中針對不同版本的請求執行不同的程式碼邏輯。比如我們的部落格修改文章列表 API,序列化器對返回資料的欄位做了一些改動,釋出在版本 v2,那麼可以根據使用者使用者請求的版本,返回不同的資料,即新增了 API,又保持對原 api 的相容:

if request.version == 'v1':
	return PostSerializerV1()
return PostSerializer

if 分支可以視為一段臨時程式碼,我們可以通過適當的方式提醒使用者,API 已經更改,請儘快遷移到新的版本 v2,並且在未來的某個時間,確認大部分使用者都成功遷移到新版api後移除掉這些程式碼,並將預設版本設為v2,這樣原本的 v1 版本的 API 就徹底被廢棄了。

當然,我們目前的部落格介面還暫時沒有需要修改升級的地方,不過為了測試 API 版本管理的設定是否生效了,我們認為新增一個測試用的檢視集,在裡面做針對不同版本請求的處理,看看不同版本的請求下是否會返回符合預期的不同內容。

首先在 blog/views.py 中加一個簡單的測試檢視集,這個檢視集中有個測試用的介面,介面處理邏輯是根據不同的版本號,返回不同的內容:

class ApiVersionTestViewSet(viewsets.ViewSet):
    @action(
        methods=["GET"], detail=False, url_path="test", url_name="test",
    )
    def test(self, request, *args, **kwargs):
        if request.version == "v1":
            return Response(
                data={
                    "version": request.version,
                    "warning": "該介面的 v1 版本已廢棄,請儘快遷移至 v2 版本",
                }
            )
        return Response(data={"version": request.version})

當然檢視集別忘了在 router 中註冊:

blogproject/urls.py

# 僅用於 API 版本管理測試
router.register(
    r"api-version", blog.views.ApiVersionTestViewSet, basename="api-version"
)

這相當於一次介面版本升級,我們再加入 v2 名稱空間的介面:

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

可以看到,包含的 URL 都是一樣的,只是 namespace 是 v2。

來測試一下效果,啟動開發伺服器,先訪問版本號為 v1 的測試介面,請求返回結果如下,可以看到如期返回了 v1 版本下的內容:

GET /api/v1/api-version/test/

HTTP 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "version": "v1",
    "warning": "該介面的 v1 版本已廢棄,請儘快遷移至 v2 版本"
}

再訪問版本號為 v2 的測試介面,返回的內容就是 v2 了。

GET /api/v2/api-version/test/

HTTP 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "version": "v2"
}

對於其它介面,無論 v1,v2 版本的介面均可以訪問,這樣就相當於完成了一次相容的介面升級。


關注公眾號加入我們

相關文章