Skip to content
My Blog
Go back

第 07 课 · Profile 与 Bundle:组合即配置

前置:第 01、02 课(03–06 可并行学) | 预计时长:60–90 分钟 | 动手环节:必须完成

你将学会

问题引入

第 02 课说插件条目来自显式声明,第 01 课你见过 --dump-config 的组合树。这一课补上中间的机制:dsh-base 的 80 余行插件、你的自定义行为、应用层,是怎么叠成一棵树的? 一句话答案:Profile = 有序 Bundle 列表 + 补丁文件,启动时逐层叠加,last-write-wins。

正文

7.1 Bundle 与 Profile 的定义

7.2 关键纠正:组合全在补丁层

仓库根和各包都没有作为组合入口的 cordis.yml Profile 的根 cordis.yml 是启动时写入的空数组占位profile-boot.ts:80-84);现存 cordis.yml 只存在于示例、测试 fixture 与快照。全部组合都发生在 cordis.patch.yml 补丁层:

// packages/boot/app-boot/src/profile.ts:854-861 简化
composeEntries(profile) {
  layers = profile.layers.flat()   // 各 bundle 层 + 用户层 + home 层 + --patch 层
  return applyEntryPatches(EMPTY_ROOT, layers)   // 根是空数组占位
}

// vendor/include/src/index.ts:58-128 简化
applyEntryPatches(rows, patches) {
  for (patch of patches) {
    if (patch.insert) { rows.push(...patch.insert); buildMap(插入的行) }  // :96-101
    if (patch 命中某行 by id) 整段覆盖该行 config                                  // :121-124
    if (未命中) warn('patch: entry %s not found', id)                              // :112 不静默
  }
}

叠加顺序(docs/architecture.md:27):每个 bundle 按 bundles 列表顺序 → profile 的 cordis.patch.yml → home 级 → 任意 --patch overlay,最后加遥测开关补丁。dsh-base 的补丁主体就是对空根的一次大规模 insert(80 余行);dsh-web-app 在其上按 id 覆写 + insert Web 行。

7.3 补丁语义的三条规则

  1. 按行整体替换,非深合并:补丁按 id 命中行,config 等字段整段覆盖;insert 进去的行可以被后续补丁再命中;冲突解决 = last-write-wins。
  2. 行顺序不携带加载语义:激活由服务可用性驱动(第 02 课 epoch 机制)。补丁只决定「有哪些条目、各自配置」,不决定「谁先启动」。
  3. 未命中的补丁不静默:警告 + dsh --dump-config 逐层重放并打印每行来源(provenance)。这是「配置错误大声失败」原则在组合层的落点。

7.4 启动六步

// apps/cli/src/bin.ts → profile-boot.ts → packages/boot/app-boot/src/index.ts
1. parseDshArgs;runProfile
2. loadProfile:解析 bundle 层;重写空根配置 `[]`
3. composeProfile:修复模块回退链 → 收集 bundle 层 → 用户层 → home 层 → --patch → 遥测补丁
4. installFailLoud:注册 unhandledRejection → fatal + exit(1)
5. boot:new Context() → ctx.plugin(Loader) → prepare(ctx)   // 注入 environment/cmdlineArgs
   → mountRootInclude(...)                                   // Include 读文件→打补丁→事务挂载
   → ctx.get('loader').await() → assertEntriesActivated(ctx) // 逐个检查 fiber 状态,失败即抛
6. 若 patchReload='live':挂 watch-only HMR 监听用户补丁

「大声失败」实例集:

7.5 host / client 双编译面

Web 侧还有一个编译期事实:host 与 client 两侧都对 Context 做同名声明合并但类型不同,一个编译程序不能同时看见两侧tsconfig.host.json:1-4)。体现为两套检查聚合(tsconfig.host.json / tsconfig.client.json);组合上的桥是 dsh-web-app 补丁同时声明 host 行与 dsh.client 浏览器名册。第 12 课讲浏览器世界时会再用到。

动手环节

  1. 看来源pnpm dsh --dump-config,找到任意一行,确认输出标明了它来自哪个补丁层(provenance)。
  2. 做一个自定义 profile:复制 $DSH_HOME/profiles/webweb-tutorial,在其 cordis.patch.yml 里挑一个工具条目改成 disabled: true,然后 pnpm dsh --profile web-tutorial --dump-config 验证生效。
  3. 体验大声失败:在同一份补丁里故意把某个 patch 的 id 打错(指向不存在的条目),再 dump 一次——应看到 entry not found 警告;再把补丁文件改成非法 YAML,确认启动/dump 直接抛错而不是降级。
  4. 推演题:两个 bundle 的补丁都要改 tools 条目的 config,谁赢?如果把其中一个 bundle 在 bundles 列表里后移一位,结果怎么变?这和「行顺序」有什么区别?

自检清单

常见误解

延伸阅读


Share this post:

Previous Post
第 06 课 · 持久层、投影与「日志化状态」
Next Post
第 08 课 · 能力接缝:三角色与两种形态