在上一章节有提到,Karman 会用 defineAPI(option)
进行单一支 API 的封装,并返回一个可以基于 option
内的配置发起请求的函式(FinalAPI
),而这章节会开始提到如何建立一个可以批次管理多支 API 的方法—— defineKarman
。
卡门树 Karman Tree
「卡门树 Karman Tree」其实顾名思义就是一种树状结构,树上的每个节点会有 0 到 * 个子节点指标,也会存放着该节点的 0 到 * 个 FinalAPI
,还会管理着从祖父节点所继承下来的共同设定。
在建立 Karman Tree 时,需要使用到 defineKarman
这支 API,这支 API 所返回的物件就是一个节点,排除一些共同设定(headers, interceptors, .etc)以外的参数,defineKarman
会有以下几个可设定的选项,这些参数通常会影响到最后卡门树的结构或拥有一些特殊的行为:
root?: boolean
:当前节点是否为根节点。url?: string
:当前节点所管理的 URL 或 URL 片段,有特殊的继承规则。api?: Record<string, FianlAPI>
:此节点上所封装的 APIs,为一个物件,key 是 API 名称,value 是 defineAPI
的返回值 FinalAPI
。route?: Record<string, Karman>
:子节点们,为一个物件,key 是子节点名称,value 是 defineKarman
的返回值。以下先不以实际可发送请求的 API 演示如何建构卡门树,以及如何访问节点及节点上的 FinalAPI
:
import { defineKarman, defineAPI } from "@vic0627/karman"const karman = defineKarman({ root: true, url: "...", api: { /** * 根节点上的 Final API */ api01: defineAPI({ // ... }) }, route: { /** * 子节点 */ node01: defineKarman({ url: "...", api: { /** * 子节点上的 Final API */ node01api01: defineAPI({ // ... }) } }) }})const [resPromise01] = karman.api01() // 从根节点访问根节点上的 Final APIresPromise01.then((res) => console.log(res))const [resPromise01] = karman.node01.node01api01() // 从根节点访问子节点,在从子节点访问子节点上的 Final APIresPromise01.then((res) => console.log(res))
路径管理
若还记得上一章所介绍的 defineAPI
,会发现它与 defineKarman
一样都有 url
参数,且一样都为非必须参数,那么在 FinalAPI 该怎么取得请求的 URL 呢?
这部分就必须提到 Karman 的其中的一项特色「路径管理」,路径会由父节点往下继承、拼接,若是 FinalAPI 没有设定 url
,就会参考它所属节点的 url
,若是当前的节点并没有设定 url
,就会参考父层节点的 url
,若是以前端最熟悉的 XML 来举例节点与 FinalAPI 的路径的继承关係:
<karman root url="https://fakestoreapi.com" base-url="https://fakestoreapi.com"> <final-api url="auth/login" base-url="https://fakestoreapi.com/auth/login" /> <karman url="products" base-url="https://fakestoreapi.com/products"> <final-api base-url="https://fakestoreapi.com/products" /> <final-api url=":id" base-url="https://fakestoreapi.com/products/:id" /> <final-api url="categories" base-url="https://fakestoreapi.com/products/categories" /> <!-- and more... --> </karman> <karman url="carts" base-url="https://fakestoreapi.com/carts"> <final-api url=":id" base-url="https://fakestoreapi.com/carts/:id" /> <final-api url="../other/path" base-url="https://fakestoreapi.com/other/path" /> <!-- and more... --> </karman></karman>
你会在在最后一个
<final-api />
的url
看到有相对路径存在,并且 Karman 不但容许其存在,而且是会生效的,这是因为 Karman 在设计之初的目的,就是要能够给开发人员最大的弹性去管理路由,因为在一些 legacy 的专案中,后端 API 可能不会按照 Restful 风格设计。
若是将上面的 XML 以程式实作,範例如下:
import { defineKarman, defineAPI } from "@vic0627/karman"export default defineKarman({ root: true, url: "https://fakestoreapi.com", api: { login: defineAPI({ url: "auth/login" }) }, route: { product: defineKarman({ url: "products", api: { getAll: defineAPI(), getById: defineAPI({ url: ":id" }), getCategories: defineAPI({ url: "categories" }) } }), cart: defineKarman({ url: "carts", api: { getById: defineAPI({ url: ":id" }), outOfParadigm: defineAPI({ url: "../other/path" }) } }) }})
继承事件
「继承事件」是一个会发生在当节点的 root
属性被设置为 true
时所触发的事件,事件触发时,会由根节点开始将可继承的设定往子节点传播,若是继承的设定被子节点给複写,複写后的设定将作为新的继承设定继续往子孙节点传播。
卡门树只能有一个根节点,且必须是顶层的节点,否则 Karman 将抛出错误。
FinalAPI 同样会有继承,但并不是发生在初始化时,也就是 FinalAPI 的继承事件不会跟节点的继承事件同时发生。
比较要注意的事情是,属性覆写的行为同样会发生在物件型别的属性,这是什么意思呢?
在 defineKarman
当中会有两个可继承的物件属性 headers
与 auth
,这两个属性在继承时,不是引用物件的址,而是会在子节点上複製一个新的物件,再以子节点的物件进行複写,因此假设程式码如下:
// ...export default defineKarman({ // ... headers: { "Content-Type": "application/json", Token: "S2FybWFuIGlzIHRoZSBiZXN0IQ==" }, api: { api01: defineAPI({ // ... headers: { Hello: "Karman" } }) }, route: { node01: defineKarman({ headers: { "Content-Type": "text/plain", }, api: { node01api01: defineAPI({ // ... headers: { Token: "SSBsb3ZlIEthcm1hbiE=" } }) } }) }})
我们可以得到各个节点与 FinalAPI 的 headers
如下:
root
{ "Content-Type": "application/json", "Token": "S2FybWFuIGlzIHRoZSBiZXN0IQ=="}
root.api01
{ "Content-Type": "application/json", "Token": "S2FybWFuIGlzIHRoZSBiZXN0IQ==", "Hello": "Karman"}
root.node01
{ "Content-Type": "text/plain", "Token": "S2FybWFuIGlzIHRoZSBiZXN0IQ=="}
root.node01.node01api01
{ "Content-Type": "text/plain", "Token": "SSBsb3ZlIEthcm1hbiE="}
可继承的设定
在卡门树中常见的可继承的设定还有下列这部分,这边以功能性进行分类,并在 Karman 特有的功能上简单说明一下:
一般功能性设定:
validation
使否启用验证引擎。快取设定:
cache
是否启用响应快取。cacheExpireTime
快取有效时间。cacheStrategy
快取存放的位置。请求设定:
headers
auth
timeout
timeoutErrorMessage
responseType
headerMap
withCredentials
and more...拦截器:
onRequest
请求发起前呼叫,可以对请求物件进行拦截,写入动态设定等。onResponse
请求完成时呼叫,可定义请求成功时的条件。根节点
在 Karman 中,会有一些特殊的功能或设定只能透过根节点触发,这些设定或功能通常也不具备继承的特性。
排程任务
是的,卡门树也具备排程任务的机制,但这机制目前只负担快取的清除任务而已,而排程任务採被动触发的机制,所以我们只能设定每次执行排程的时间间隔 scheduleInterval
。
当今天排程管理器接收到任务,会把任务加入一个队列,当队列内存在一个以上(含)的任务时,就会自动启用排程任务机制,并在固定的时间点上执行队列中所有任务,而当有任务完成时,该任务就会从队列中剃除,直到队列为空,排程管理器就会自动关闭。
另外,目前 Karman 只有一个排程管理器,意思就是说,当今天存在两个以上的卡门树时,这两个卡门树会共用同一个排程管理器,并且只会以第二个卡门树所设定的 scheduleInterval
为準。
依赖外挂插件
后续的章节里,会陆陆续续提到 Karman 的其中一个功能 Middleware,这些 Middleware 都是函式型别,并且会绑定当前的节点作为 this
所指向的上下文。
通常在这些 Middleware 内,可能会进行一些基本的资料处理,资料处理的共通逻辑就能以外挂的形式注册到卡门树上,再透过 Middleware 内的 this
访问外挂的共通逻辑或常数等,而注册必须统一由根节点进行,否则 Karman 将会抛出错误。
Karman 本身已经有内建的外挂模组,包含以下两个:
Karman._typeCheck
:包含各种型别验证的方法,为 Karman 验证引擎所使用的基本模组。Karman._pathResolver
:为路径字串的操作模组,本身也被 Karman 广泛使用。而要在卡门树上注册外挂需要使用 Karman.$use(plugins)
这个方法,而外怪本身需要有一个 install(karman)
具体方法,假设我们有一个函式 _add()
要注册为外挂:
// /karman/plugins/add.jsexport default function _add(a, b) { return a + b}// install() 的具体实现Object.defineProperty(_add, "install", { value: (karman) => { Object.defineProperty(karman, "_add", { value: _add }) }})
接下来就能在根节点上尝试注册此外挂,并且假设根节点上有一个子节点 product
:
// karman/index.jsimport { defineKarman, defineAPI } from "@vic0627/karman"import product from "./karman/routes/product.js"import _add from "./plugins/add.js"const karman = defineKarman({ // ... route: { /** * ## 商品管理 API */ product }})// 注册外挂函式kaman.$use(_add)export default karman
后续我们就能在这座卡门树上的任一 Middleware 用 this
访问这个外挂:
// karman/routes/product.js// ...export default defineKarman({ // ... onRequest() { const sum = this._add(2, 4) console.log(sum) }, api: { getAll: defineAPI({ // ... onSuccess(res) { const sum = this._add(9, 8) console.log(sum) } }) }})
让外挂的语法支援自动完成
透过上述方式注册好外挂后,的确能从 Middleware 中调用到外挂,但美中不足的是,注册外挂并没有支援语法提示,因此可以透过外挂的声明文件,对 Karman 进行模组声明,扩展 KarmanDependencies
介面:
// /karman/plugins/add.d.tsinterface Add { (a: number, b: number): number; (a: string, b: string): string;}export default function _add: Add;declare module "@vic0627/karman" { interface KarmanDependencies { /** * 相加 */ _add: Add; }}
今天这个章节主要介绍了什么是卡门树,以及卡门树是如何去做路由管理以及卡门树的各项基本设定,下一个章节会会开始介绍 Karman 的核心功能之一——「参数验证引擎」。