雖然公司的部分專案已經使用 Swagger 一段時間,但我總覺得 Swagger 哪裡不對勁,真要說主因應該是甚少有人討論如何撰寫「元件化」的 Swagger。
什麼是元件化我想各位都明白,在物件導向大行其道的現在,大家無不想方設法分離程式邏輯,以撰寫更加易讀好維護的程式碼,MVC、MVVM 等設計模式也都試圖解決這類問題。但是,Swagger 卻反其道而行,所有人,所有範例都一致的將所有 API 檔案寫在同一支檔案內,連我們之前的專案也都是這麼做,社群裡僅有部分的聲音討論要改善這個問題。
但問題來了,最近我們平臺決定實現微服務架構,採完全前後端分離。為原本龐大且錯綜複雜的專案寫檔案的過程,發現光其中一個微服務就有超過 1000 行程式碼的檔案,推算下來整份檔案會有數萬行。這樣龐大的檔案要如何維護?先別說維護了,光想到要開啟這份檔案並編輯就令人胃痛。
另一個問題就是 Swagger Editor 在撰寫的過程實際上是沒有即時儲存的,也就是過程中發生意外都可能導致寫的檔案遺失,且寫完後還得再經過一個「輸出為(醜陋的)HTML」,或「存為 YAML,請前端人員自行想辦法開啟」的抉擇,為了美感我們選擇後者,但這樣的作法不但在教育訓練時需要浪費不少時間,從 Swagger 是什麼開始講起,且大家撰寫及交付的流程也很難統一。
幾經研究之後,我們整理歸納出一套方法,可以用有規範的目錄結構來達到檔案分離,本文我以官方範例的 Petstore 為例,示範如何分離。
Let's Begin
首先我們為專案建一個資料夾,並建立主要檔案入口 index.yaml
:
$ mkdir doc
$ cd doc
$ touch index.yaml
開啟官方範例的 Petstore,可以看到如下 YAML,我們將其複製貼上到剛剛建立的 index.yaml
中:
openapi: "3.0.0"
info:
version: 1.0.0
title: Swagger Petstore
license:
name: MIT
servers:
- url: http://petstore.swagger.io/v1
paths:
/pets:
get:
summary: List all pets
operationId: listPets
tags:
- pets
parameters:
- name: limit
in: query
description: How many items to return at one time (max 100)
required: false
schema:
type: integer
format: int32
responses:
'200':
description: An paged array of pets
headers:
x-next:
description: A link to the next page of responses
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/Pets"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
post:
summary: Create a pet
operationId: createPets
tags:
- pets
responses:
'201':
description: Null response
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/pets/{petId}:
get:
summary: Info for a specific pet
operationId: showPetById
tags:
- pets
parameters:
- name: petId
in: path
required: true
description: The id of the pet to retrieve
schema:
type: string
responses:
'200':
description: Expected response to a valid request
content:
application/json:
schema:
$ref: "#/components/schemas/Pets"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
components:
schemas:
Pet:
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string
Pets:
type: array
items:
$ref: "#/components/schemas/Pet"
Error:
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string
可以看到這份簡單的檔案當中包含 3 支 API 就需要 109 行程式碼。我們需要將其拆解
分離 Paths
第一步我們先在目錄下新增 paths
資料夾,並在其中建立 pets
資料夾以供後續使用
$ mkdir paths
$ cd paths
$ mkdir pets
在這裡,我們可以建立第一支元件,我們在 pets
目錄下建立 index.yaml
$ touch index.yaml
並將根目錄中的 /index.yaml
分離到新建的 /paths/pets/index.yaml
// /paths/pets/index.yaml
get:
summary: List all pets
operationId: listPets
tags:
- pets
parameters:
- name: limit
in: query
description: How many items to return at one time (max 100)
required: false
schema:
type: integer
format: int32
responses:
'200':
description: An paged array of pets
headers:
x-next:
description: A link to the next page of responses
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/Pets"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
post:
summary: Create a pet
operationId: createPets
tags:
- pets
responses:
'201':
description: Null response
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
並於根目錄 /index.yaml
中引入
// /index.yaml
...
paths:
/pets:
$ref: /paths/pets/index.yaml
/pets/{petId}:
...
/index.yaml
中的另一個 paths
也同理,我們將其放至 /paths/pets
中的 specified.yaml
(不知道怎麼命名比較好?)
// /paths/pets/specified.yaml
get:
summary: Info for a specific pet
operationId: showPetById
tags:
- pets
parameters:
- name: petId
in: path
required: true
description: The id of the pet to retrieve
schema:
type: string
responses:
'200':
description: Expected response to a valid request
content:
application/json:
schema:
$ref: "#/components/schemas/Pets"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
並修改 /index.yaml
// /index.yaml
...
paths:
/pets:
$ref: /paths/pets/index.yaml
/pets/{petId}:
$ref: /paths/pets/specified.yaml
...
到此為止我們的 paths
就分離完畢了
分離 Schemas
接著我們繼續分離 schemas
,我們在根目錄中建立 schemas
資料夾
$ mkdir schemas
$ cd schemas
並參考上方作法,我們加入 pets.yaml
用於存放 pets
相關模型,及 common.yaml
用於存放通用模型(如 Error)
$ touch pets.yaml common.yaml
並將 /index.yaml
中的 components/schemas/Pet&Pets
放入 /schemas/pets.yaml
// /schemas/pets.yaml
Pet:
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string
Pets:
type: array
items:
$ref: "#Pet"
components/schemas/Error
也一樣,我們放入 /schemas/common.yaml
// /schemas/common.yaml
Error:
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string
並將這些 components
從 /index.yaml
中移除,到這裡為止,我們的 /index.yaml
應該只剩 13 行,如下:
// /index.yaml
openapi: "3.0.0"
info:
version: 1.0.0
title: Swagger Petstore
license:
name: MIT
servers:
- url: http://petstore.swagger.io/v1
paths:
/pets:
$ref: /paths/pets/index.yaml
/pets/{petId}:
$ref: /paths/pets/specified.yaml
我們使用 Swagger UI 開啟 /index.yaml 會發現沒辦法正確解析
提示的錯誤告訴我們不能使用相對路徑,這是由於我們並不是在伺服器的環境下開啟,所以我們需要使用另一個 NPM 套件 live-server
來提供伺服器環境
$ npm install -g live-server
$ live-server .
並於 Swagger UI 中重新載入
這時候可以看到錯誤提示已經不同,變成我們引用的 components
路徑不存在,因此我們需要做最後一項調整
我們開啟 /path/pets/index.yaml
,將任何引用到 #/components/schemas
的地方都改為我們的剛剛分離出去的 schemas
// /paths/pets/index.yaml
...
content:
application/json:
schema:
$ref: /schemas/pets.yaml#Pets
default:
description: unexpected error
content:
application/json:
schema:
$ref: /schemas/common.yaml#Error
post:
...
default:
description: unexpected error
content:
application/json:
schema:
$ref: /schemas/common.yaml#Error
/paths/pets/specified.yaml
也一樣
// /paths/pets/specified.yaml
'200':
description: Expected response to a valid request
content:
application/json:
schema:
$ref: /schemas/pets.yaml#Pet
default:
description: unexpected error
content:
application/json:
schema:
$ref: /schemas/common.yaml#Error
回到 Swagger UI,可以看到錯誤訊息都不見了,各 API 也都正確載入 Schemas
到這邊為止,我們已經完成了本篇教學主要的部分,但還有一個問題尚待解決:我們要如何寫完當下即可交付,並且讓對方不用懂 Swagger 就能立即使用檔案呢?
準備交付
這邊我們需要先下載 Swagger UI,並將 dist中的所有檔案放到我們的專案根目錄下
再來我們修改 index.html
,讓 Swagger UI 開啟時即載入我們自定義的 index.yaml
// /index.html
...
const ui = SwaggerUIBundle({
url: "/index.yaml", // 這裡需要修改
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
...
再此開啟 live-server
,即可看到預設讀取的檔案已經變為我們的 /index.yaml
接著,我們剛剛為了本地開發方便,我們將 live-server
裝在全域中,現在我們要將他整合到專案內
$ npm init
$ npm install --save-dev live-server
並在 package.json
中加入 script
方便使用
// /package.json
...
"scripts": {
"doc": "live-server .",
"test": "echo \"Error: no test specified\" && exit 1"
},
...
恭喜,我們完成了整個 Swagger 開發及交付環境,現在我們修改目錄下的任一檔案都會觸發瀏覽器重新整理,讓我們能即時預覽結果,並且撰寫完畢後,只要丟給你的夥伴,請他輸入 npm run doc
的指令就可以開始服用檔案了。
上面的範例我有做成 模板 並放到 Github,喜歡的話可以點個 star 來支援我,有任何討論指教都歡迎在下面留言或聯絡我(s811657@gmail.com),謝謝。
後記
對了,文章開頭提到的那支 1000 行的檔案,在採用這種架構後只剩 41 行,真是可喜可賀 ?
左邊跟右邊,你比較喜歡哪一個呢?
部落格文章:https://bit.ly/2IqzZbn
本作品採用《CC 協議》,轉載必須註明作者和本文連結
簡永哲 Leo Chien
IT Director | 大師鏈 MasterChain
E: s950329@hotmail.com