【Karman.js】建立卡门树来批次封装 API - 02

在上一章节有提到,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 的返回值 FinalAPIroute?: 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 当中会有两个可继承的物件属性 headersauth,这两个属性在继承时,不是引用物件的址,而是会在子节点上複製一个新的物件,再以子节点的物件进行複写,因此假设程式码如下:

// ...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 快取存放的位置。

请求设定:

headersauthtimeouttimeoutErrorMessageresponseTypeheaderMapwithCredentialsand 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 的核心功能之一——「参数验证引擎」。


关于作者: 网站小编

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

热门文章