6. 组合与 HMR(热模块替换)
到目前为止构建的每项能力都是插件;组合文档则选择应用的插件树。本章把组合移到 YAML、 热重载一个插件,并诊断始终无法加载的插件。
组合项不只有名称
Cordis.Loader 的组合文档(cordis.yml 的对应物)接受 id / name / config / disabled:
plugins:
- id: greeter # 稳定标识:patch 按 id 区分"修改现有行"与"先删除再添加"
name: greet
config:
targets: [alpha, beta]
- id: consumer
name: consumer
disabled: true # 保留行但不挂载;改回后插件连同依赖方一起重新加载
加载并挂载:
var catalog = new PluginCatalog();
catalog.Register<GreetConfig>("greet", config => Plugin.From(c =>
{
foreach (var target in config.Targets)
Console.WriteLine($"{config.Greeting}, {target}!");
}));
// ... 其余插件注册
var loader = new CompositionLoader(catalog);
await using var mounted = await loader.LoadFileAsync(root, "cordis.yml");
LoadFileAsync 先 RenderFileAsync 渲染出不可变的 RenderedComposition(include 深度优先展开、
环境变量替换、patch 层按序应用),再由 MountedComposition.CreateAsync 事务式挂载:
任一行失败,整份候选回滚。返回的 MountedComposition 可以用 ReplaceAsync(candidate) 原子地
换成新候选,也可以整体 DisposeAsync()。
热模块替换
卸载释放 effect(第 2 章),加载遵循依赖关系
(第 3 章)。Cordis.Hmr 的 HotReloadWatcher
(src/Cordis.Hmr/HotReloadWatcher.cs)把两者组织成 last-good swap:
- 旧插件仍运行时先编译 candidate;编译失败就保留旧版本。
- 编译成功后 dispose 旧
PluginHandle,但暂留旧CompiledScript作为 rollback material。 - apply candidate;成功才请求卸载旧 ALC。
- candidate apply 失败时清理 candidate,并从仍加载的旧 assembly 重建 last-good 插件。
因此它能回滚失败版本,但不承诺零空窗的原子替换。HotReloadWatcher 暴露
ReloadAsync(file) / UnmountAsync(file) / Error 事件,宿主(见
Hot Reload Watcher)负责接线。
第 2 章的纪律是它能工作的唯一保障:任何活引用(事件处理器、静态缓存、timer 回调)都会阻止 ALC 卸载。
诊断始终无法加载的插件
依赖驱动加载的另一面:inject 指定了无人提供的服务时,插件一直等待,不输出任何内容。 这不是错误——PENDING 是合法状态,提供方可能稍后才挂载。
代码内可以直接查看这些状态:
// mounted.Handles 是 MountedComposition 的全部 PluginHandle
foreach (var handle in mounted.Handles)
{
if (!handle.IsActive && !handle.Completion.IsCompleted)
Console.WriteLine($"{handle.Plugin.GetType().Name} is PENDING — a required service is missing");
}
向 catalog 注册对应的提供方(或取消该行的 disabled)后,插件就会加载。插件既不执行任何操作
也不报告任何内容时,先检查它的 handle 状态。
下一章:进入 harness:把相同模式用于真实的 harness 服务。