【Karman.js】一款专为建构 API 抽象层的前端套件 - 01

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`

上面的几种参数中,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 的封装~

不管是写套件或文章,对我来说都是第一次挑战,如果有可以改善的地方,还烦请各位大大多多鞭策,感谢阅读到这边的读者,感谢大家!


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章