Skip to content
My Blog
Go back

声明即授权:dsh Web 客户端的 slot 体系

dsh 的浏览器端同样是插件系统,但 UI 组合有个额外难题:谁有权把组件渲染进谁的洞里?前端插件化项目在这里常常退化成约定俗成,dsh 把它做成了加载期的硬约束。

一个 API,外加「声明即授权」

组合的唯一入口是 ctx.slots.register({ name, children?, store?, inject? }, Component)。底座是 SlotCore——一个零运行时依赖的纯注册表,外面包一层 Cordis Service(服务名 slots)。

规则的核心:你的组件能渲染哪些子 slot,恰好等于注册时 children 对象里声明的键。渲染一个没声明的 slot,或声明了别人已声明的 slot,都会在加载期直接抛错:

// packages/client/ui-slots/src/index.ts
throw new Error(
  `slot "${options.name}" is not declared ` +
  `(a parent entry's children table must declare it)`
)

重复声明的错误信息还会点名是谁先声明的:already declared (by ${childRec.declaredBy})。客户端规约里把这叫「children = declaration + authorization」——冲突不是实现疏漏,是设计在说话。

props 不是手写的,是推导出来的

组件的 props 由若干个「份额」交叉而成,业务代码不许手写其中任何一个成员。客户端的规约文档里说是四个份额,但代码已经跑到前面了——ui-slots/src/index.ts 里的 ComposedProps 实际是六个的交叉:PropsRuntime(运行时上下文)、PropsRenderSlots(子 slot 的渲染函数)、PropsStore(store 工厂)、InjectFace(注入面)、MatchedShare(链式 slot 的匹配数据)和 PropsLocale(翻译函数 t)。

hook 同理:组件能用的只有框架给的常设席位(useSessionuseSessionsuseWorkspacesuseStorerenderSlot),加上渲染器从注入面绑出来的 use<Name>。业务代码自己造 hook 当 prop 传是禁止的——组件连 ctx 都看不到,一切数据和回调都从 props 份额来。

slots.inject:往别人的洞里注册

想把自己的组件注册进另一个包声明的 slot,用 ctx.slots.inject(name, () => ...):它会等那个声明真正出现再执行,声明塌缩时自动撤走贡献,重新声明后重跑,生命周期跟着调用方的插件一起走。跨包注册不靠加载顺序的巧合,靠等待。

崩溃了怎么办:让位,不连坐

某个 entry 渲染期崩溃,不会拖垮整棵树。SlotCore.reportEntryError 让出位置给下一优先级的影子条目,而崩溃的注册条目仍留在账本上——恢复是可能的,记录不丢。

还有一层更底的:模块图在 DI 之下

动态插件在浏览器里要先解决「代码从哪来」,才谈得上「服务从哪来」。启动代码里有一行很能说明问题:

// packages/client/web/src/boot.ts
await ctx.plugin(Loader)
const loader = ctx.loader
loader.internal = this.modules as never

Cordis 要通过 Loader 才能触达插件代码,所以每个模块请求必须先可满足,DI 才排得了激活顺序。静态模块表 PLATFORM_MODULES 里就八个名字(react、react-dom、cordis 加几个静态客户端库),用 satisfies 钉死和常量一致。客户端规约里专门有一张表区分这两种依赖声明:Cordis 的 inject 以服务名为单位、运行时等待、可被任何提供者满足、允许环;模块图的 external 以模块标识为单位、物化时同步不可等、身份唯一不可替换、遇环即拒。

两种看起来像依赖边的声明,语义完全不能互换——这大概是整个客户端架构里最容易想错的地方。

小结

把「谁能渲染谁」变成加载期可判定的结构问题,把「组件拿什么数据」变成类型可推导的份额问题,把「插件崩溃」变成可恢复的让位问题——slot 体系没有发明新框架,它只是把每个容易腐化的地方都换成了机器说了算。

本系列


Share this post:

Previous Post
Capability Seam:让 shell、文件系统和沙箱都可替换
Next Post
第 03 课 · 五种事件分发模式:协作的语法