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.jsexport default { optimizeDeps: { exclude: ["@vic0627/karman"] }}
封装一支 API
一般常见的 HTTP Client 通常在使用当下,马上就会依照传入的配置发起请求,例如说下面这支 API:
// Get all productsfetch("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 的封装~
不管是写套件或文章,对我来说都是第一次挑战,如果有可以改善的地方,还烦请各位大大多多鞭策,感谢阅读到这边的读者,感谢大家!