08|目录与依赖边界
第九章 / 08。目标:目录表达所有权,规则能在 CI 中判定,而不是靠“大家自觉”。
从允许方向开始
app → pages, features, shared
pages → features, entities, shared
features → entities, shared
entities → shared
shared → 外部包;不能反向导入业务层这是一个示例有向图。团队也可按领域垂直切分,但必须回答:谁拥有状态/请求、公共入口在哪、跨域依赖经什么接口、禁止方向是什么。
src/features/cart/
api/ 请求适配
model/ 状态和业务规则
ui/ 组件
index.ts 对外公共入口外部模块只从 features/cart 公共入口导入,不穿透 model/internal.ts。barrel 不是越多越好:无边界聚合会制造循环依赖、扩大初始化副作用和打包面。
状态与副作用放在哪里
- 只为一个组件服务的开关/输入留在组件。
- 可复用交互流程放 feature composable,并注入请求依赖。
- 后端实体形状在 API adapter 解析成领域模型,组件不直接依赖原始 JSON。
- 跨页面长期状态才进入 store;store 不是所有变量的默认家。
类型 import 也表达架构依赖。它可能被擦除而不进入运行时代码,但仍会让低层知道高层业务概念;边界规则应同时检查 value/type import。
把规则变成失败
ESLint no-restricted-imports、依赖图工具或自写扫描器都可以。关键输出要包含 importer、imported 和违反的允许边,且循环检测列出完整路径。只画架构图而 CI 不执行,目录会逐渐失真。
迁移时先添加观测模式生成现有违例清单,再按领域修复并逐步收紧;一次禁止全部历史例外会迫使团队关闭规则。
检测环要区分“以前走过”与“正在这条递归路径上”。从A走到B再到C,若C指向A,而A仍在当前路径,就找到了 A→B→C→A;若指向已经完整检查过且退出路径的D,则只是复用一个依赖,不是环。DFS(深度优先遍历)每次先走一条边到尽头,再返回,分别用 visiting/visited 两个集合记录这两种状态,答案展示完整实现。
allowed 的键定义全部已知层,包括没有出边的 shared,写成 shared: []。未知目标即便误填进某个允许列表也应报错,不能把规则拼错当成新增了一层。
练习:验证依赖图
实现 validateEdges(edges, allowed)。edge 为 [fromLayer,toLayer];返回全部非法边,不提前停止;层名未知也算非法。再实现 DFS 检测至少一条循环路径。用 shared→feature、feature→entity、entity→shared、feature→feature(同层允许与否由 allowed 明确)和三节点环写断言。
参考答案:第 9 章第 08 题。