单一上下文、单一注册表,以及何时止步

发布: (2026年3月4日 GMT+8 22:28)
9 分钟阅读
原文: Dev.to

I’m happy to translate the article for you, but I’ll need the full text you’d like translated. Could you please paste the content (or the portion you want translated) here? I’ll keep the source line exactly as you provided and preserve all formatting, markdown, and code blocks.

上一篇,首次运行时和适配器已就绪

类型系统一直在反击——跨包的声明合并、构建工具的切换——但说实话?我差点把这个问题忘了。它仍然存在,只是现在没有阻塞任何东西。有时你前进,旧的争斗仍在等待。😄

这些单元已经存在。它们可以注册。但 “在配置文件中声明的单元”“实际运行的单元” 之间的管道尚未建立。

情况已经改变。这一轮工作比上一次更安静,但涉及面更广:

  • 配置解析
  • 共享基础上下文
  • 可工作的暴露与查询系统
  • 一些小工具以保持注册表的整洁

首先:加载单元

在其他任何工作能够进行之前,内核需要知道它正在处理哪些单元。这正是 config.resolve.units 的作用——它接受配置文件中平铺的单元列表,并将其解析为内核实际可以使用的东西。

这些单元以引用的形式进入——字符串、工厂函数、导入的对象——并输出为已加载、已验证、准备启动的定义,内核知道如何对其进行分类和初始化。

这就是 “我在配置中声明了单元”“内核知道该怎么处理它们” 之间的桥梁。
没有它,单元数组只是一段停留在文件中的数据。有了它,内核可以:

  • 按类型对单元进行分类,
  • 按依赖关系进行拓扑排序,
  • 按正确顺序调用生命周期钩子。

这并不是光鲜的工作,但它是让其他一切成为可能的管道。

一个上下文共享所有单元

单元解析完毕后,下一个问题是:它们可以使用什么?在 Velnora 中,每种单元类型都会获得一个 上下文对象——即传入 configure()build() 等生命周期钩子的东西。上下文是单元与系统其余部分交流的方式:暴露 API、查询其他单元、读取配置。

到目前为止,每个上下文都是独立定义的:

  • IntegrationConfigureContext 有自己的 ctx.query()
  • IntegrationBuildContext 也有自己的。
  • AdapterDevContext —— 同样,从头重新声明。

这导致了到处都是重复的表面,并且对共享行为的任何更改都必须在所有上下文中镜像。对于三种单元类型还能勉强管理,但如果增至十种就不可能了。

解决办法很直接——创建一个 BaseContext,承载所有共享内容,让具体的上下文从它继承。

  • 集成上下文继承基础并添加集成特定的能力。
  • 适配器上下文同理。
  • 运行时上下文同理。

共享部分集中在一个地方;专用部分仍保留在各自所属的位置。这并不是一个巧妙的点子,只是第一次真正实现它。

暴露、查询、完成

BaseContext 现在完整实现了暴露和查询机制——每个单元无论类型都需要的两个操作。ctx.expose()ctx.query() 现在真正 工作,不仅仅是类型声明,而是端到端连通。

  • 单元可以在 configure() 期间通过键暴露一个 API。
  • 任何声明依赖该键的其他单元都可以 query 并获得真实实现。

无需强制转换、any 或手动查找。整个单元系统设计的基础现在已经落地。

支撑 BaseContext 的是 GlobalRegistry——一个在运行时保存所有已暴露 API 的中心注册表对象。

  • 当单元调用 ctx.expose("docker", dockerApi) 时,它会写入 GlobalRegistry
  • 当另一个单元调用 ctx.query("docker") 时,它会从同一位置读取。

每个注册表条目都有键,键的类型经过严格检查并得到强制执行。编译时 TypeScript 检查的键就是运行时查找的键。如果类型声明 "docker" 存在,运行时注册表就会有 "docker" 的槽位;如果不存在,则没有该槽位。

Source:

ist and the query fails loudly.

有了这些,BaseContext 不再只是一个共享接口。它成为了所有集成、适配器和运行时都构建在其上的工作层。两篇帖子前还只是类型的东西,现在已经在运行代码了。

为懒人准备的助手

在这项工作中衍生出的一个小助手——makeRegistryObject。它是为懒人准备的便利工具。你给它一个名称和一个结构,它会为你构建一个嵌套对象,其中 每一级都是可用的字符串键

makeRegistryObject("kernel", {
  config: {
    units: "units"
  }
})

结果:

  • result"kernel"
  • result.config"kernel:config"
  • result.config.units"kernel:config:units"

每一级都可以作为注册表键使用。无需手动拼接字符串,不会出现拼写错误,拥有完整的自动补全。

这看似微不足道,但在一个拥有大量注册表键的单体仓库中,它能避免很多低级错误。

暂停

在完成基础上下文和配置解析的接线后,我开始深入运行时实现——即 NodeBun 等运行时如何启动其工具链、解析包并执行代码的核心部分。随后我停了下来。

并不是因为出现了错误,而是我意识到自己即将做出一些以后很难撤销的决定。运行时层涉及所有内容:

  • 单元如何获取其工具链,
  • 执行计划如何构建,
  • 包管理抽象如何接入,
  • 线程模式和进程模式的执行如何分叉。

这里的一个错误抽象会让每个适配器、每个集成、每个未来的运行时都继承这个错误。

于是我回到研究阶段:重新阅读之前的设计笔记,草拟替代方案,重新审视 Toolchain API 和 Adapter Protocol,看看在最近几轮的所有变动之后,这些边界是否仍然合理……

有时,最有成效的事情就是停止编码,去思考。这正是其中的时刻。

接下来是什么

一旦运行时设计确定下来,下一步就是实现它:一个真实的 Node 运行时通过 BaseContext 启动其工具链,通过注册表解析包,并生成适配器可以使用的执行计划,而无需了解其背后的运行时。这就是验证整个堆栈端到端工作的测试。

0 浏览
Back to Blog

相关文章

阅读更多 »