Koishi v5 发布预告

3.2 CJS-ESM 多例

多例问题很多人都遇到过,不过这里说的是一种特殊的多例情况。

首先想象一下,如果 Koishi 仅提供 ESM 版本,那么使用 ESM 编写的插件能够加载 Koishi,但使用 CJS 分发的插件在加载 Koishi 时会直接报错失败。那么如果 Koishi 同时提供 CJS 和 ESM 版本,又会发生什么呢?此时,使用 CJS 分发的插件能够正确地加载 Koishi 的 CJS 导出,使用 ESM 分发的插件同样能够加载 Koishi 的 ESM 导出。然而 CJS 和 ESM 导出本质上是两个文件,这种加载方式意味着内存中同时加载了两份 Koishi 源码,即所谓的「CJS-ESM 多例」。这既浪费了资源,也会带来种种不一致问题。

如你所见,无论是仅导出 ESM 还是同时导出 CJS+ESM 都会存在可行性问题。唯一的解决方案似乎是强制要求所有插件也都改为 ESM 分发,但我们绝对不会放弃现有的 1000 余个插件的生态。这便是任何框架迁移到 ESM 都会遇到的一大阻碍。万幸的是,Koishi 从技术上解决了这一问题。

注:Node.js 22 提供了 --experimental-require-module,这将允许 CJS 模块导入同步的 ESM 导出。不过 Koishi 目前的最低目标版本是 Node.js 18,无法使用这项特性。

4 个赞

3.3 ESM 无法 HMR

HMR 即模块热替换(Hot Module Replacement)。在 Koishi 插件的开发过程中,任意保存一个插件的源码文件,都可以在其他插件不停机的情况下,重载这个插件的代码。这项技术在前端非常普及,但是在后端几乎没有实现,本质原因是后端项目难以确定重载边界(HMR Boundary)。

Koishi Loader 基于 Cordis 插件系统,能够准确分析出任何模块的重载边界,因此成为了为数不多的(在我的认知里是唯一的)支持后端 HMR 的框架。这套 HMR 基于 require.cache 实现。这套 API 一方面提供了不同文件的依赖关系,作为 HMR 分析的基础;另一方面允许 Koishi 从缓存中删除已经加载的模块,并回收所有的运行时副作用,确保了重载过程中不会出现内存泄漏。

然而,require.cache 仅仅在 CJS 环境下可用。在 ESM 环境下加载的模块根本不会写入这个对象。这意味着我们即无法追踪依赖,也无法在重载后回收泄露的内存。你可以在许多 issue 中看到人们对此的抱怨和请求:

ESM 作为标准被推行已经有很多年了,然而直到今天也没有看到任何成熟的解决方案。我们也一度认为这是绝无可能做到的,不过最终我们还是成功实现了:互联网上首个无需打包器的 ESM HMR 完整技术

7 个赞

3.4 问题的解决

最终,我们设计了两套完整的算法来解决上述问题。目前在 Cordis 仓库中使用的是其中一套,它基于一些 Node.js 内部 API,可以在守护进程中使用。后续如果有时间,我们也可能切换到另一套实现,它不依赖任何内部 API 就能安全地解决 CJS-ESM 多例和 ESM HMR 问题。

以下是一些相关代码:

有了这套技术,我们终于可以在 v5 迎来一个纯 ESM 的 Koishi 了。

5 个赞

第四部分:FAQ

以下是一些大家可能好奇的问题的统一回答。

4 个赞

4.1 关于变更

Q:插件要如何从 v4 迁移到 v5?

v5 相比 v4-latest 不会引入任何破坏性 API 变更。

因此,所有可以在 v4-latest 下运行的插件依然可以在 v5 运行。

Q:没有破坏性变更为什么要提升主版本号?

尽管没有 API 破坏性变更,但是 Koishi v5 在使用上依然引入了足量的称得上破坏性的更新,例如纯 ESM、全新的配置文件格式、Cordis 生态带来的依赖更新。其中的每一个对于特定使用者都可能称得上是破坏性的。

此外,尽管我们会认真评估和测试所有潜在的问题,在如此巨大的更新规模下也难免出现疏漏。为了迎接这些未知的问题,提升主版本号足以给准备升级的用户一些警示。

Q:v4 在历史上经历过 Node.js 最低版本提升和插件拆分,即便是这些改动也没有提升主版本号,为什么这次上 v5 了?

首先我认为纯 ESM、新版配置文件、Cordis 生态算得上是更大的变化(未来如果要提升 Node.js 最低版本限制或者拆分插件也会放在 v5 版本内完成);

其次 v4 已经独立发展了 2 年多了,本身 koishi 也没有很多可改动的空间了。转换视角,迁移到 cordis 迎接更多生态是我认为全新的一步。

Q:旧版 v4 插件能否运行在 v5 实例中?

完全可以。

按照现有的依赖管理工具,在 v5 实例安装 v4 插件时会提示“版本不兼容”。这个问题将会在未来解决,新的依赖管理工具仍然会提示低版本,但不会以红色示警或阻止用户安装。

不过,即便没有更新 API 的必要,我们也建议插件开发者将依赖的版本更新至 v5(在 v5 步入稳定版本后)。

Q:我要如何迁移到新版配置文件和新插件?

v5 会自动检测旧格式并完成迁移。你不需要进行任何操作。

6 个赞

4.2 关于时间表

Q:v5 的第一个版本计划于何时发布?

目前仅仅是跑通了技术流程,考虑到上述变更存在较大的迁移工作量,第一个 v5 版本的发布可能还需要数个月的时间。不过在此之前,我们会先发布适用于 Satori 生态的整合包。如果对新架构感兴趣,可以先用 Satori 整合包体验 v5 的诸多特性。

Q:遇到预料外的破坏性变更要怎么做?

同其他大版本一样,v5 会有若干个 alpha / beta / rc 版本,这些版本会着手处理未知的问题。

在最终稳定版本到来前,建议大家备份后再尝试升级。

11 个赞

好!!

2 个赞

今天是 202x年 5 月 14 日,也是 Koishi 之日,将这条消息转发到koishi.js相关论坛,就可以用小火箭把 Koishi 升级到 v5。我试过了,是真的,成功更新到了5.1.4版本()但今天真的是 Koishi 之日 :heart_eyes_cat:

2 个赞

也就是说需要canvas和pupp的插件可以直接分别和这2个依赖组个整合包,这样就不会有啥字体显示还有什么渲染问题了是吧

2 个赞

(鼓掌鼓掌鼓掌鼓掌)

1 个赞

给 puppeteer 配置一个 canvas 隔离域即可。

如果你是整合包作者,可以直接在整合包中就写好这个隔离域,用户在安装的时候就不会出问题了。

6 个赞

好好好。

1 个赞

好,那么好!

3 个赞

好耶!梦中情框!

1 个赞

好好,好

1 个赞

还没时间研究v4,就马上步入v5了。希望学习成本不要太高,越用越方便那样就好了。

我对 v5 有些自己的小小的期待…例如:

  1. 适配器能简化某些操作吗,例如 qqbot 通过安装某个插件就能发送音频,无需修改对应插件代码

我挺希望在开发插件时候只需要 sisson.send(h.audio(xx)) 就能兼容所有平台,一些基础类型的消息搞特殊真的挺不方便的,虽然我知道是 qqbot 某些机制是个坑

1 个赞

这个 v4 可以做吧,可以请求 adapter-qq 支持一下就可以了

(鼓掌)