iT邦幫忙

0

【Karman.js】一款專為建構 API 抽象層的前端套件 - 01

  • 分享至 

  • xImage
  •  

HI~大家好我是 VIC,不知道各位前端好朋友是否曾經遇過...

接手了一份專案,卻沒有 API 文件,或是文件內容東缺西漏,當今天接到一張與某支 API 相關聯的維護單,詢問了後端關於那支 API 詳細規格,他也只是苦笑地搖著頭跟你說他也不知道,然後順手貼給你那支 API 的程式碼叫你自己看,但為了完成這張單,你也只好硬著頭皮想辦法當「通靈王」。

又或是今天接到開發單,而要開發的新功能可能會使用到一些既有的 API,但今天文件缺失的狀態下,你根本不知道有什麼 API 可以用,只能去已完成開發的功能裡面慢慢尋找自己可用的 API,找到後也只是複製貼上到自己要開發的功能,頂多加個簡單的註解,殊不知數個月後接到這新功能的維護單,又得重新去通靈這些 API 的規格,如此惡性循環下去...

以上也只是簡單舉例一下前後端對接可能會遇到的鬼故事。然而,我就在想,是否能在專案中建置一個「API 服務」,而這個服務必須滿足以下條件:

  • 封裝 API,隱藏所有發起請求的細節,專注於 API 本身所實現的功能:隱藏的細節包含像 URL、HTTP 方法、參數類型(路徑參數、查詢參數、請求體 .etc)、FormData 建立等。
  • 統一所有 API 的輸入介面,提供輸出介面的配置:最好是能夠統一以 requestFn(param[, config]) 作為封裝後的 API 的使用介面,簡潔明瞭,並且 param 能夠隨著每支 API 需求參數不同而顯示不同規格,而在輸出(response)的部分,也需要讓 JS 的族群能夠對響應規格進行設定,以利後續使用 API 時,能夠獲得完整的語法提示。
  • 單一入口:能夠通過一個物件訪問專案下所有 API,並且支援完整的語法提示與顯示註解內容。

綜合以上條件,這份專案的靈感孕育而生...

Karman.js

版本:v1.2.2

Karman 是一款 JS 套件,用於建構 API 抽象層,特色包括像:

  • 可選擇封裝單一支 API 或是一次封裝很多 API,讓後面調用的開發人員,可以以「最低成本」發起請求。
  • 若要一次封裝多支 API,Karman 會用樹狀結構管理 API 的路由、路由上的方法以及共同配置等內容,最後通過單一入口點來訪問這些被管理的路由或 API。
  • 提供所有封裝後的 API 統一、高彈性、動態型別的輸入/輸出介面。
  • 統一所有 API 的程式流,從配置繼承、參數驗證、URL 組成、請求建立等等。
  • 支援撰寫樹狀結構節點、封裝後的 API 與 API 所需參數的 JSDoc 註解,這些註解將會在後續調用到該方法或節點時,自動顯示於懸停提示中。
  • 參數驗證引擎,基本如型別、最大最小值、正則表達式等等,也能夠使用客製化驗證函式,或是將以上所有規則組成聯集或交集規則,另外,搭配 Schema API 還可以做到更複雜的物件驗證。

綜上所述,與其說 Karman 是一個 HTTP Client,更像是說 HTTP Client 只是 Karman 的其中一項功能,而其他核心像 Karman Tree、Validation Engine、Schema API 等等,目的都是要將複雜的請求前置準備隱藏到底層,讓開發人員可以專注在請求的實際完成的任務,而不是還要去翻找文件看哪支 API 有什麼參數或該怎麼敲,讓文件可以跟程式碼結合,通過抽象層就能直接在 IDE 瀏覽到所有專案中的 API,並且還能直接發起請求,正所謂讓抽象層成為一個「可以發送請求的 API 文件」

而在 Karman 的文件中有提到,這套件的核心理念就是「先封裝、再使用」,並且整個套件是面相開發人員,目的要增加開發時的體驗,因此建議以一份簡易的專案實際操作看看,這系列的文章最後將以 Fake Store API 示範 Karman 是怎麼封裝這些 API 以及封裝完該怎麼使用。

安裝

目前此套件僅在 npm 上架,未來視情況可能會推出 cdn 版本((希望啦。

這邊使用 npm 進行安裝:

$ npm install @vic0627/karman

因為此套件有使用到特殊的架構,如果你的打包工具會自動將套件程式碼最佳化,建議將此套件排除在最佳化之外,這邊以 vite 為例:

// vite.config.js
export default {
    optimizeDeps: {
        exclude: ["@vic0627/karman"]
    }
}

封裝一支 API

一般常見的 HTTP Client 通常在使用當下,馬上就會依照傳入的配置發起請求,例如說下面這支 API:

// Get all products
fetch("https://fakestoreapi.com/products")
  .then((res) => res.json())
  .then((json) => console.log(json))

但若要透過 Karman 發送單一請求,則須先進行配置在調用:

// 導入
import { defineAPI } from "@vic0627/karman"

// 封裝
const getAllProducts = defineAPI({
    url: "https://fakestoreapi.com/products"
})

// 調用
getAllProducts()[0].then((res) => console.log(res))

defineAPI 是 Karman 的其中一支核心函式,用於封裝一支 API 並返回一個新的函式(後面都將以 FinalAPI 稱呼返回的新函式),想要正式發起請求需要調用被 defineAPI 返回的 FinalAPI,而 FinalAPI 都會擁有統一的使用介面,以下是 defineAPI 與 FinalAPI 的基本語法:

// 封裝
const finalAPI = defineAPI({
    url,             // 請求 url
    method,          // HTTP 方法,預設 GET
    payloadDef,      // 定義 `finalAPI` 所需參數
    dto,             // 響應規格
    // ...略
})

// 調用
const [
    resPromise,      // 響應的 Promise 物件
    abort            // 取消請求函式
] = finalAPI(
    payload,         // API 所需參數,由 `payloadDef` 決定這邊要帶甚麼參數
    config           // runtime 設定,可強制複寫部分在封裝時的設定
)

基本上 method 沒甚麼好解釋的,而 dto 會在後面講到動態型別註解時會說明,這邊主要需要先認識 payloadDef 這個參數。

參數定義物件(payloadDef

payloadDef 主要的功能是,決定 FinalAPI 的 payload 所需傳入的參數,而還有其他功能像是:

  • 決定傳入的參數要用在哪裡(position
  • 參數是否必須(required
  • 參數的驗證規則(rules
  • 參數的預設值(defaultValue

實際以程式碼的結構來看的話,payloadDef 會是一個物件,key 是所需參數的名稱,value 是上面提到的四點功能:

const finalAPI = defineAPI({
    // ...
    payloadDef: {
        // 定義需求參數 `param01`
        param01: {
    	    position,
    	    required,
    	    rules,
    	    defaultValue
    	}
    }
})

finalAPI({ param01 }) // 傳入參數 `param01`

上面的幾種參數中,requiredrule 隸屬於驗證引擎,由於驗證引擎較為複雜,之後會花一個篇幅來專門講解,因此這邊先不贅述。

接下來我們來看 position 這參數有哪些容許值:

  • undefined"body" 會將該參數用於請求主體(Request Body)。
  • "path" 會將該參數用於路經參數(Path Parameter)。
  • "query" 會將該參數用於查詢參數(Query String Parameter)。
  • ("body" | "path" | "query")[] 會將該參數同時用於上述三種不同地方。

在這幾種容許值當中,較為特別的是 "path",它需要先以 ":param_name" 的格式將參數要插入的位置定義於 url 之中,否則無法生效,另外,在沒有啟動驗證引擎的狀況下,所有接收參數皆為非必填,所以路經參數若是沒皆收到值,會自動轉換成空字串並減少一層路徑,若 url 有兩個以上(含)的路經參數,可能就要注意到是否需要啟動必填驗證機制。

const getProductById = defineAPI({
    url: "https://fakestoreapi.com/products/:id",
    payloadDef: {
        id: {
            position: "path"
        }
    }
})

getProductById({ id: 4 })    // 傳入參數 `id: 4`,最後請求的 `url` 會是 `https://fakestoreapi.com/products/4`
getProductById()             // 不傳入參數,最後請求的 `url` 會是 `https://fakestoreapi.com/products`

而在查詢參數的部分,Karman 會自動取用你所定義的參數名稱作為 Query Key,加上接收的值來組成完整的 pair:

const getProducts = defineAPI({
    url: "https://fakestoreapi.com/products",
    payloadDef: {
        limit: {
            position: "query"
        }
    }
})

getProducts({ limit: 10 })    // 傳入參數 `limit: 10`,最後請求的 `url` 會是 `https://fakestoreapi.com/products?limit=10`

最後在 "body" 的部分,它本身就是 position 的預設值,因此假設說該參數沒有驗證規則或預設值等其他設定,是可以直接傳 null 給那個參數的,再更隨便一點,如果所有所需參數都只要給請求主體使用,且沒有其他額外設定,payloadDef 甚至可以只傳參數名稱的陣列:

const login = defineAPI({
    url: "https://fakestoreapi.com/auth/login",
    method: "POST",
    payloadDef: {
        username: {
            position: "body"
        },
        password: {},
        // or
        password: null
    },
    // or
    payloadDef: ["username", "password"],
    headers: {
        "Content-Type": "application/json"
    }
})

這邊 headers 需要傳入 Content-Type 來觸發 JSON 格式物件的自動轉換,否則請求送出時的 payload 會是 [object Object]

參數預設值(defaultValue

參數預設值的部分就較為單純,運作機制就是「沒接收到值,就用預設值」,但要稍微注意的是,defaultValue 必須是一個函式,返回的值才是該參數的預設值,不論你的預設值是 call by value 還是 call by reference 都必須是函式,另外,若在有啟動驗證引擎且有驗證規則的情況下,預設值是會被驗證的,所以你不能使用無法通過驗證規則的值作為預設值。

const getProducts = defineAPI({
    url: "https://fakestoreapi.com/products",
    payloadDef: {
        sort: {
            position: "query",
            defaultValue: () => "asc"
        }
    }
})

getProducts() // 無參數傳入,取用預設值,完整 `url` 會是 `https://fakestoreapi.com/products?sort=asc`

以上是這個章節的內容,主要講述套件最最基本的封裝概念,下一張開始會介紹到多支 API 的封裝~

不管是寫套件或文章,對我來說都是第一次挑戰,如果有可以改善的地方,還煩請各位大大多多鞭策,感謝閱讀到這邊的讀者,感謝大家!


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
DanSnow
iT邦好手 1 級 ‧ 2024-05-08 23:10:14

這給我的感覺還挺像 ts-rest 的概念的,不知道作者有接觸過這套嗎?

pure90719 iT邦新手 5 級 ‧ 2024-05-09 13:46:18 檢舉

真的耶! 我來研究看看,謝謝你~

我要留言

立即登入留言