可逆是 Koishi 插件开发哲学中影响最深远的一个。
一、定义
可逆的 Koishi 是指,对于任何一个 Koishi 实例,任意进行加载和卸载插件操作后,实例行为仅与最终启用的插件相关;与中间是否重复加载过插件、插件之间的加载或卸载顺序都无关。你也可以简单理解为「路径无关」。这里的相关和无关具体包括:
- 多次加载的插件不会重复注册任何行为
- 多次加载并卸载一个插件后,内存占用不会增加
- 加载并卸载任何插件不会残留对其他插件的影响
- 如果插件之间有依赖关系,依赖的插件会自动在被依赖的插件之后加载,并自动在被依赖的插件之前卸载,即确保插件的生命周期由依赖关系而非加载顺序决定
Il 称之为「零熵」,意思是一样的,只是他发明了这个词而已。
二、设计动机
实现了「可逆的 Koishi」的项目将会获得以下优点:
2.1 热重载
Koishi 的所有插件都将可以在运行时加载、卸载和重载,减少了用户的更新成本。
2.2 动态加载
由于插件的加载顺序由依赖关系决定,因此插件的代码可以被异步地加载,而不需要担心加载顺序对可用性的影响。
2.3 可追踪
可以对 Koishi 输出的任何内容追踪插件来源(尽管目前暂无下游实现)。
三、生态现状
目前的 Koishi 生态普遍依赖此模式。
3.1 依赖服务的插件
Koishi 存在大量依赖服务的插件。任何插件可以声明自身依赖某些服务,由 Koishi 确保插件只在服务加载完成后加载,并在卸载开始前卸载。
3.2 @koishijs/plugin-market
@koishijs/plugin-market 提供了多个页面:
- 插件管理:允许用户在运行时启用、停用、修改插件配置,而不用重启 Koishi
- 插件市场:允许用户在运行时安装并启用新插件,而不用重启 Koishi
- 依赖管理:允许用户在运行时更新版本、卸载插件(需要重启 Koishi)
3.3 @koishijs/plugin-hmr
@koishijs/plugin-hmr 允许用户在开发过程中直接通过保存代码来按需重载插件源码和插件配置。这是非常少见的后端 HMR(Hot Module Replacement,模块热替换)实现。
3.4 @koishijs/plugin-console
值得一提的是,由于可逆性是在 Cordis 架构中提供的,因此不仅 Koishi 的后端是可重载的,前端也是可重载的。
四、实现原理
4.1 基本概念
由于缺乏合适的翻译,这里直接用英文概念来表达。
- 我们称一个代码片段是 可回收 (disposable) 的,如果其返回某个回收函数,执行该函数会清除代码的副作用 (包括注册的行为、占用的内存、依赖该代码片段的其他代码片段的加载)
- 我们称一个函数是 自回收 (self-disposable) 的,如果其包含的代码片段是可回收的,并且回收函数会在这个函数所在的代码片段被回收时调用
在这个定义下,「可逆的 Koishi」可以被等价表达为 所有 Koishi 插件都是可回收的。
4.2 插件实现
要实现一个代码片段的可回收需要确保代码片段中的每一条语句都满足以下三个性质之一:
- 无副作用 (含副作用可以被运行时 GC 的情况)
- 有副作用,但是副作用被其他语句手动回收
- 是自回收的函数的调用
目前绝大部分 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 并且有副作用的例子即可,因此并没有明显的额外心智负担。
4.3 服务实现
服务方法的副作用通常需要服务自己进行回收,也就是说需要设计为自回收的。需要通过 ctx.scope.collect()
进行实现,目前普遍使用的服务均支持此功能,但由于此部分没有收入官方文档,社区中的服务开发者可能并未熟知相关的技术。
五、对比
5.1 可逆与正交
为了确保服务 API 的可逆性,通常需要对服务进行正交化设计。
5.2 可逆与零占用
可逆本身与零占用无关,但是可逆与零占用共同组成了热更新的基石。
5.3 可逆与 0dt
可逆并未实现 0dt,但热更新最大限度地减少了用户在生产环境下的重启次数,客观上增加了 SLA。