Files
TrueGrowth/docs/CYCLE_DEPENDENCY_LESSONS.md

142 lines
7.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 循环依赖治理经验
更新日期2026-04-27
## 背景
本轮目标是继续收敛项目里的循环依赖:先把运行时静态 import、type-only import 清到 0再单独收敛 dynamic import 形成的懒加载 SCC。最终 `runtime-static``static-all``dynamic-inclusive` 三层扫描均为 0。
## 本轮结论
### 1. 循环依赖要分层看
推荐至少区分三层:
- `runtime-static`:运行时静态 import/export必须作为失败项。
- `static-all`:运行时静态 + type-only import/export适合作为架构洁净度检查。
- `dynamic`:动态 import 造成的懒加载图回边,适合单独专题治理。
经验不要只看“有没有环”要先判断它会不会影响初始化顺序、chunk 分包、首屏边界或类型层耦合。
### 2. type-only 环也值得收敛
TypeScript 的 type-only import 不会直接进入运行时代码,但它会暴露模块职责不清的问题。本轮剩余问题主要来自:
- workflow converter 同时承载类型和转换逻辑。
- settings manager 同时承载配置类型、常量和运行时管理器。
- model adapter barrel 带有默认注册副作用,被类型引用误触。
经验:类型环通常不需要大改逻辑,优先抽出中立纯类型模块,让业务模块都向下依赖。
### 3. 桶文件不是类型边界
`index.ts` barrel 很方便,但如果 barrel 同时 re-export 有副作用的运行时模块,就不适合作为纯类型 import 来源。
推荐做法:
- 类型引用直连 `types` 或低层纯类型模块。
- 有副作用的 barrel 只给运行时功能入口用。
- 公共 API 需要兼容时,在旧入口 re-export 类型,但内部新代码不要再从旧入口回引。
本轮例子:
- `audio-api-service` 的结果类型改为直连 `model-adapters/types`
- `gemini-api/types` 改为直连 `provider-routing/types`
- `settings-manager` 继续 re-export `settings-types`,但 provider-routing 内部不再回引它。
### 4. 中立类型模块要保持“轻”
抽出的类型模块只应依赖更底层的纯类型或常量类型,不应引入服务、单例、注册表、存储读写或 UI 组件。
本轮新增的边界:
- `workflow-types`:只承载 workflow 数据形状。
- `settings-types`:只承载 settings 数据形状和纯常量。
经验:中立模块一旦混入运行时逻辑,很快会变成新的公共泥球。
### 5. 检查脚本默认要保守
`pnpm check:cycles` 继续只检查运行时静态环,避免突然扩大 CI 失败面。新增 `pnpm check:cycles:types` 专门检查 type-only 环,适合在架构收敛或重构后复跑。
经验:检查项升级要分阶段,让默认门禁稳定,再逐步把更严格模式纳入常规流程。
### 6. dynamic import 环要先找“元数据拉注册表”
本轮 dynamic-inclusive 大 SCC 最初有 97 个文件,第一刀不是改组件,而是切断内置工具元数据对工具注册表的依赖:
- `built-in-tools` 只读取 manifests不读取 `toolRegistry`
- `ToolPluginModule` 移到纯类型文件,工具插件不再 type-import registry。
- 模型测试启动器只读内置 manifests不为查一个 tool manifest 拉入组件注册表。
结果97 文件大 SCC 立即拆成 `22 + 5 + 5`。经验是:如果“常量/元数据”反向 import 了“注册表/组件加载器”,懒加载图很容易被拖成大团。
### 7. 通用 UI 组件不要默认回拉业务能力
剩余 22 文件 SCC 的关键边是 `MediaViewport -> canvas-operations -> card -> MarkdownEditor -> media-library -> shared`。处理方式:
- 媒体预览组件只依赖轻量 image/video 插入 helper不直接 import 全量 `canvas-operations`
- `canvas-board-ref` 独立承载 board 引用,避免轻量插入能力为了拿 board 反向拉入文本/Card 插入链。
- media-library 内部使用 leaf import不从 `shared/index` 桶文件拿 `HoverTip``ContextMenu`
经验:通用展示组件可以提供兜底能力,但兜底能力必须是窄入口;否则一个“插入到画布”按钮就会把 Card、Markdown、素材库一起拉回预览组件。
### 8. 运行时单例互访用 bridgeTransform 和组件分离
两个 5 文件小 SCC 分别来自 service 互访和插件组件互访:
- GitHub sync 需要刷新 workspace但不再静态 import `workspace-service`;改为 `workspace-runtime-bridge` 注册运行时能力。
- `sw-capabilities` 只需要 WorkZone transforms不需要 WorkZone React 组件;将 transforms 拆到中立模块,`with-workzone` 保持 re-export 兼容。
经验:服务单例之间需要互相调用时,优先抽一个无副作用 bridge插件的纯 transform/API 不要和 React 渲染组件放在同一个依赖入口。
## 防回归规则
新增或调整 import 前,先按下面规则判断方向是否正确:
| 场景 | 推荐方向 | 避免方向 |
| --- | --- | --- |
| 类型共享 | 业务模块 -> `types` / 纯类型模块 | `types` -> service / UI / registry |
| 元数据读取 | 常量模块 -> manifests / 纯数据 | constants -> registry / 组件加载器 |
| 通用 UI 兜底能力 | UI -> 窄 helper | UI -> 全量业务服务 barrel |
| 插件能力复用 | service -> transform-only 模块 | service -> plugin React 入口 |
| 服务单例互访 | service -> runtime bridge | service A -> service B -> service A |
| 组件间复用 | 叶子组件直连叶子组件 | 叶子组件 -> `shared/index` 大桶 |
提交前重点检查三类高风险信号:
1. 一个 `index.ts` 同时 re-export 类型、服务单例、组件和注册副作用。
2. 一个通用组件为了小功能 import 了完整业务 service。
3. 一个“纯数据/纯类型”文件 import 了 React、registry、storage 或网络请求模块。
如果确实需要跨层调用,先抽窄接口:`*-types``*-transforms``*-bridge``*-ref``*-helper`。这些模块必须保持无 UI、无存储副作用、无注册副作用。
## 验证清单
基础检查:
```bash
NPM_TOKEN=${NPM_TOKEN:-dummy} pnpm check:cycles
NPM_TOKEN=${NPM_TOKEN:-dummy} pnpm check:cycles:types
NPM_TOKEN=${NPM_TOKEN:-dummy} pnpm typecheck
```
如果改动触及 Vite manual chunk、入口预取或重包懒加载再跑
```bash
NPM_TOKEN=${NPM_TOKEN:-dummy} pnpm exec nx build web
NPM_TOKEN=${NPM_TOKEN:-dummy} pnpm verify:startup
```
## 后续建议
1. 新增核心类型时,优先放到纯类型模块,不要顺手放进服务实现文件。
2. 从 barrel import 前先确认该 barrel 是否包含运行时副作用。
3. `dynamic import` SCC 不要和静态环一起治理应按工具窗口、SW、媒体插入等边界拆专题治理时优先切“元数据 -> 注册表”“通用 UI -> 业务服务”“服务单例互访”三类边。
4. 保持旧导出兼容,但内部依赖逐步迁到低层入口。
5. 每次拆环都先跑 `check:cycles:types`,再跑 `typecheck`,避免只修图不修类型。
## 一句话结论
循环依赖治理最稳的方式是先分清运行时、类型层和动态懒加载,再用纯类型模块、窄 helper、runtime bridge 打断高风险边界。✅