今天也在 GitHub 搜 Koishi,搜到了这个,看起来也不是官方发的,但是感觉好好玩,就转载过来了。
然后我感觉这篇文章里讲的东西有很多其实是不合理或者难以实现的,所以只是图一乐。
今天也在 GitHub 搜 Koishi,搜到了这个,看起来也不是官方发的,但是感觉好好玩,就转载过来了。
然后我感觉这篇文章里讲的东西有很多其实是不合理或者难以实现的,所以只是图一乐。
可逆是 Koishi 插件开发哲学中影响最深远的一个。
可逆的 Koishi 是指,对于任何一个 Koishi 实例,任意进行加载和卸载插件操作后,实例行为仅与最终启用的插件相关;与中间是否重复加载过插件、插件之间的加载或卸载顺序都无关。你也可以简单理解为「路径无关」。这里的相关和无关具体包括:
Il 称之为「零熵」,意思是一样的,只是他发明了这个词而已。
实现了「可逆的 Koishi」的项目将会获得以下优点:
Koishi 的所有插件都将可以在运行时加载、卸载和重载,减少了用户的更新成本。
由于插件的加载顺序由依赖关系决定,因此插件的代码可以被异步地加载,而不需要担心加载顺序对可用性的影响。
可以对 Koishi 输出的任何内容追踪插件来源(尽管目前暂无下游实现)。
目前的 Koishi 生态普遍依赖此模式。
Koishi 存在大量依赖服务的插件。任何插件可以声明自身依赖某些服务,由 Koishi 确保插件只在服务加载完成后加载,并在卸载开始前卸载。
@koishijs/plugin-market 提供了多个页面:
@koishijs/plugin-hmr 允许用户在开发过程中直接通过保存代码来按需重载插件源码和插件配置。这是非常少见的后端 HMR(Hot Module Replacement,模块热替换)实现。
值得一提的是,由于可逆性是在 Cordis 架构中提供的,因此不仅 Koishi 的后端是可重载的,前端也是可重载的。
由于缺乏合适的翻译,这里直接用英文概念来表达。
在这个定义下,「可逆的 Koishi」可以被等价表达为 所有 Koishi 插件都是可回收的。
要实现一个代码片段的可回收需要确保代码片段中的每一条语句都满足以下三个性质之一:
目前绝大部分 Koishi 提供的 API 均实现了无副作用或者自回收:
ctx.any()
ctx.database.get()
:可能会创建一个连接,但连接有超时机制,因此可以视为无副作用ctx.command()
:含链式调用中的 .option()
等,会创建多个回收函数ctx.on()
ctx.middleware()
ctx.plugin()
:如果插件是可回收的,那么它就是自回收的ctx.i18n.define()
ctx.component()
ctx.setInterval()
ctx.router.get()
:通过修改了 Koa Router 的行为实现ctx.model.extend()
:尽管有副作用,但认为影响不大,且保留此副作用有利于优化,故不进行回收所以对于插件开发者来说,只需要通过 dispose
事件处理非 Koishi 官方 API 并且有副作用的例子即可,因此并没有明显的额外心智负担。
服务方法的副作用通常需要服务自己进行回收,也就是说需要设计为自回收的。需要通过 ctx.scope.collect()
进行实现,目前普遍使用的服务均支持此功能,但由于此部分没有收入官方文档,社区中的服务开发者可能并未熟知相关的技术。
为了确保服务 API 的可逆性,通常需要对服务进行正交化设计。
可逆本身与零占用无关,但是可逆与零占用共同组成了热更新的基石。
可逆并未实现 0dt,但热更新最大限度地减少了用户在生产环境下的重启次数,客观上增加了 SLA。
本文将以 指令管理 为例,分析 Koishi 中的可逆插件开发。
该功能由独立的官方插件 @koishijs/plugin-commands 实现,插件的源代码位于 此目录 下。
从文档中可以看到,指令管理插件提供了以下功能:
与此同时,该插件也允许其他插件通过 ctx.schema
服务注册更多可以被该插件所配置的功能。
上述所有行为都通过配置文件进行持久化,即:当用户在控制台中做出任何改动时,插件本身的配置项发生变化,并同步至配置文件中;当整个实例重启时这些改动会依然生效。
该插件要实现可逆性,需要满足以下具体的性质:
可逆的 Koishi 要求 commands 插件回收自身产生的一切副作用,即:当插件被停用时,用户对指令系统做出的一切改动应当复原:已修改的配置项、指令的别名和显示名称全部恢复为未启用此插件时的状态;创建的新指令被移除;被调整的父子指令关系被复原。
commands 插件的功能可以分为两个部分:
其中 1 不依赖控制台插件,2 依赖控制台插件。可逆的 Koishi 要求该插件被启用时,1 会立即生效,而 2 则仅当 console 插件启用时生效;当 console 插件停用时,2 会立即失效并回收相关副作用,1 保持生效。
可逆的 Koishi 要求插件的加载顺序可以是任意的。而 commands 插件会改动某些指令的行为,因此其不能假定其在加载时对应的指令已存在。具体而言:
以上三个部分使用了不同的实现原理。
使用 dispose
事件配合 ctx.command()
本身的自回收实现。
使用 using
实现。
对于未加载的指令,维护一个等待中的改动队列,并同时监听 command/added
事件以实现。
commands 插件的设计充分体现了 Koishi 的可逆性。尽管该插件需要考虑非常多边界情况,但得益于自回收、生命周期、服务依赖、事件系统等特性,所有的情况都能够被妥善处理。事实上,大部分插件并不会遇到如此复杂的情况;对于可逆性的考量也更多地存在于架构设计上,并不会显著增加插件开发者的心智负担。