7.9 KiB
Agent Note: 客户端壳分层与动态包边界
Status: implemented
English | 中文
Client 插件装载模型负责模块到达、Cordis 生命周期和 HMR。本 Note 负责包归属、构建 face、共享模块请求及 npm 依赖声明;这些决定取代装载 Note 中较早的包分类和 import 边规则。
Problem
Client npm 依赖区段描述安装和开发关系,但不能可靠描述 bundle 内容。把 dependencies、peerDependencies 或 devDependencies 当作隐式 bundler 指令,可能内联本应共享的 React 或 workspace 身份,也可能让构建后的库携带未解析子 import,却没有交给预期的宿主组装。
浏览器应用还包含不同角色:HTML/Vite 编译入口、不依赖框架的 Cordis 启动内核、静态装配库,以及由 Loader 治理的插件。HTML 提前执行属于到达策略,不定义包类别。Runtime 和 modules 需要先于 Vite 主模块到达,同时继续使用普通 lib/client.js 产物和动态图 row。
共享 UI 库仍向大量消费者暴露同步 TypeScript 与 React 实体。在这些实体进入 service 或 slot 前,形式上把库改为动态 entry 只会保留实体耦合,并模糊外壳必须共享的模块身份。
Decision
分层与构建形态
| 层 | 成员 | 职责 | 构建与加载形态 |
|---|---|---|---|
| Web 编译壳 | apps/web |
拥有 index.html、Vite 配置、dist chunk 和静态资源 |
从已构建 package export 组装最终浏览器产物 |
| 启动内核 | packages/client/web |
拥有纯 DOM 启动页、模块系统接线、Cordis settle 和 renderer handoff | staticLinked lib/index.js;无 dsh.client row |
| 静态装配库 | Cordis、ui-primitives、ui-slots |
提供共享模块身份和直接实体 API | ESM lib/index.js,由 Vite 合并拆分;不是 Loader entry |
| 模块自举包 | packages/client/modules |
提供 client 模块表及其 Cordis wrapper | 带一个普通 lib/client.js 的动态包;host 提前送达其 factory |
| 动态 client 包 | runtime、ui-renderer、主题和功能插件 |
通过 Cordis service、slot 和 effect 参与应用 | 声明 dsh.client,产出自注册 lib/client.js,并保留 host graph entry |
packages/client/web 把 Cordis 保持为 matching peer 与开发依赖,并把 modules 和静态 UI 包作为开发期编译输入。apps/web 消费已构建 package export,不通过 alias 读取 workspace 源码。
staticLinked 预设让 lib/index.js 中每个 bare specifier 保持 external import,并在旁边输出相对 CSS 资产。Vite 宿主负责解析和去重这些 import,并决定最终 chunk 边界。静态库不会把宿主打包策略复制进自身产物。
共享模块请求
动态浏览器 bundle 会隐式 external 统一基座:PLATFORM_MODULES 命名由外壳播种的 React、Cordis 和静态 UI 身份,PRELOADED_CLIENT_EXTERNALS 命名由 HTML parser 预载的 runtime 动态身份。包只在精确请求基座外实体时使用 dsh.client.external。纯类型 import 会被擦除,不产生请求;允许的第三方实现库保留为 bundle 私有内容。
请求只有两种提供方:
- 请求所命名的 dynamic package row;末尾
/client会别名到该 package row。 - 外壳静态模块表中的精确 key。
不存在通用 dsh.client.provide 别名机制。动态 row 和静态 key 已穷尽实际提供方,Cordis service provide 与此相互独立。图组合会拒绝畸形或缺失请求、自请求和同步请求环,并把动态提供方排在消费者之前。ClientModuleSystem.import() 与 prefetch() 会在消费者能够物化前递归登记这些动态提供方的 factory,因此网络时序无法破坏同步请求图。
Parser 预载与 React 移交
Modules Node 半按以下顺序向实际返回的 HTML 注入启动协议:
- 以 queue 模式安装
window.__ModuleLoader__,包含pendingQueue、load()与create()。 - 以阻塞式 classic script 执行 modules graph row 的普通
lib/client.js。 - 以相同方式执行 runtime 的普通
lib/client.js。 - 赋值
window.__DSH_BOOT__。 - 执行 Vite 主模块。
两个提前执行的脚本都只注册 factory。启动内核把原始图与外壳 seed 传给 __ModuleLoader__.create()。Facade 移除 modules registration,用拒绝全部 external 的 require 函数将其物化,再调用其 createClientModuleSystem 导出。Modules bundle 解析图、构造 ClientModuleSystem、把自身 exports 缓存为 modules row,并在模块闭包中保留该系统。构造过程先把同一 facade 切换到 live 模式,再排空 runtime 的 pending factory。因此 modules client face 必须满足零 runtime external 的自举要求。
immediately 层级完成 factory 注册后,内核创建全部 Loader entry,等待 Cordis 静止,并要求每个 fiber 都进入 ACTIVE。随后调用 ctx.uiRenderer.mount(container)。动态 ui-renderer 包拥有 React、slot 渲染、已有启动 DOM 的 hydrate 和 React root 生命周期;启动内核与失败页保持 React-free。
依赖声明
每个 client 包都把 Cordis 保持为 matching peerDependencies 和 devDependencies。动态包若 import、re-export、augment 内部动态包,或在 dsh.client.inject 中命名它,就把该包保持为 matching peer 与开发依赖。静态 client 输入和 React 模块对动态包只是开发依赖,因为外壳提供其运行期身份。
普通安装库仍放在 dependencies:动态构建可以内联私有实现,而 staticLinked 库会保留 bare import 交给最终宿主。各构建 face 独立决定 external,不由 npm 区段推导。发布文件列表覆盖产物实际可达的每个运行期入口、相对资产和声明文件。
verify-client-packages 会检查这些分类、依赖区段、构建形态、parser preload 对齐、共享模块请求和模块图无环性。仓库 publint pass 负责检查发布闭包。该验证器的 --fix 模式只修复无歧义的 manifest 漂移。
Alternatives considered
立即把所有 client 包改为动态插件。 ui-primitives 与 ui-slots 仍提供同步实体,且没有独立 service 或 slot 生命周期;只加 manifest 声明不会移除这些 import。
为 modules 或 runtime 生成单独的 client-static.js。 两个包仍是动态图 row 和 Cordis 插件,只有 factory 提前到达。第二份产物会把宿主策略编码进文件名,并让同一源码产生两个运行期产品。
把全部共享模块编进 Vite entry。 这会让业务插件失去部署组合与插件级替换能力,包括 renderer 和主题。
保留通用模块 provider 声明。 Package row 和精确静态 key 已命名全部提供方;别名会增加另一套归属协议,却没有第三种供给来源。
在 apps/web/index.html 中硬编码预载 URL。 URL 与 rev 属于 host 当前 graph。改写实际返回的 HTML 才能让 queue、bundle URL 和 manifest 使用同一 graph revision。
Consequences
Npm 依赖在 peer 与开发区段间移动时,bundle 内容保持稳定,因为每个构建 face 都直接声明 external。静态库继续由宿主装配,动态包则保留统一产物与生命周期治理。
启动协议依赖 modules 和 runtime 的 package id,modules 还必须保持运行期自包含。缺少 bootstrap registration 会在 Cordis 启动前失败;后续插件 import、apply 与 service 等待失败仍由启动页的 ACTIVE 扫描呈现。
外壳消费已构建 lib/ 产品,因此在相关 build 或 watcher 运行前,源码与浏览器产物可能漂移。仅源码 typecheck 通过不能证明实际服务的应用使用同一份代码。
两个静态 UI 库仍是明确例外。把其中任一项转换为动态包时,必须在同一变更中把全部实体消费者迁移到 service 或 slot,并从静态 seed 删除对应身份。