如何撰寫立即可交付的元件化 Swagger 檔案

leochien發表於2018-05-09

雖然公司的部分專案已經使用 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

相關文章