HI~大家好我是 VIC,不知道各位前端好朋友是否曾經遇過...
接手了一份專案,卻沒有 API 文件,或是文件內容東缺西漏,當今天接到一張與某支 API 相關聯的維護單,詢問了後端關於那支 API 詳細規格,他也只是苦笑地搖著頭跟你說他也不知道,然後順手貼給你那支 API 的程式碼叫你自己看,但為了完成這張單,你也只好硬著頭皮想辦法當「通靈王」。
又或是今天接到開發單,而要開發的新功能可能會使用到一些既有的 API,但今天文件缺失的狀態下,你根本不知道有什麼 API 可以用,只能去已完成開發的功能裡面慢慢尋找自己可用的 API,找到後也只是複製貼上到自己要開發的功能,頂多加個簡單的註解,殊不知數個月後接到這新功能的維護單,又得重新去通靈這些 API 的規格,如此惡性循環下去...
以上也只是簡單舉例一下前後端對接可能會遇到的鬼故事。然而,我就在想,是否能在專案中建置一個「API 服務」,而這個服務必須滿足以下條件:
requestFn(param[, config])
作為封裝後的 API 的使用介面,簡潔明瞭,並且 param
能夠隨著每支 API 需求參數不同而顯示不同規格,而在輸出(response)的部分,也需要讓 JS 的族群能夠對響應規格進行設定,以利後續使用 API 時,能夠獲得完整的語法提示。綜合以上條件,這份專案的靈感孕育而生...
版本:v1.2.2
Karman 是一款 JS 套件,用於建構 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"]
}
}
一般常見的 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`
上面的幾種參數中,
required
與rule
隸屬於驗證引擎,由於驗證引擎較為複雜,之後會花一個篇幅來專門講解,因此這邊先不贅述。
接下來我們來看 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 的封裝~
不管是寫套件或文章,對我來說都是第一次挑戰,如果有可以改善的地方,還煩請各位大大多多鞭策,感謝閱讀到這邊的讀者,感謝大家!
這給我的感覺還挺像 ts-rest 的概念的,不知道作者有接觸過這套嗎?
真的耶! 我來研究看看,謝謝你~