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.1Host: example.comAccept: 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 還提供了其它諸如 HostNameVersioning、QueryParameterVersioning 的版本管理輔助類,可自行檢視文件了解:https://www.django-rest-framework.org/api-guide/versioning/
綜合來看,NamespaceVersioning 模式便於 URL 的設計與管理,因此我們的部落格應用決定採用這種 API 版本管理方式。
為了開啟 api 版本管理,在專案的配置中加入如下配置:
settings/common.pyREST_FRAMEWORK = { 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning', 'DEFAULT_VERSION': 'v1'}
以上兩項設定分別全域性指定使用的 API 版本管理方式和客戶端預設版本號的情況下預設請求的 API 版本。儘管這些配置項也可以在單個檢視或者檢視集的範圍內指定,但是,統一的版本管理模式更為可取,因此我們在全域性配置中指定。
接著在註冊的 API 介面前帶上版本號:
blogproject/urls.pyurlpatterns = [ # ... 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 OKAllow: GET, HEAD, OPTIONSContent-Type: application/jsonVary: Accept{ "version": "v1", "warning": "該介面的 v1 版本已廢棄,請儘快遷移至 v2 版本"}
再訪問版本號為 v2 的測試介面,返回的內容就是 v2 了。
GET /api/v2/api-version/test/HTTP 200 OKAllow: GET, HEAD, OPTIONSContent-Type: application/jsonVary: Accept{ "version": "v2"}
對於其它介面,無論 v1,v2 版本的介面均可以訪問,這樣就相當於完成了一次相容的介面升級。