大型前端项目的目录结构演进:从 feature-based 到 domain-driven 的架构变迁
大型前端项目的目录结构演进从 feature-based 到 domain-driven 的架构变迁目录结构是前端架构的物理映射。一个混乱的目录结构意味着混乱的模块边界、混乱的依赖关系、混乱的团队分工。本文梳理三个阶段的目录结构演进路径从传统的 type-based 到 feature-based再到 domain-driven每一阶段的驱动力和适用场景。一、三种目录范式的基础对比Type-based按技术类型分层最经典的结构按文件类型组织src/ components/ hooks/ utils/ services/ pages/ styles/优势学习成本为零任何新人都能快速找到文件位置。劣势功能分散在多个目录中修改一个功能需要跨 6 个目录跳转。Feature-based按功能模块分层将同一功能的所有文件聚合到同一目录src/ features/ auth/ components/ hooks/ utils/ services/ types.ts dashboard/ components/ hooks/ ...优势修改隔离功能边界清晰。劣势公共依赖的归属模糊跨功能引用容易形成隐式耦合。Domain-driven按业务域分层以业务领域为边界每个域包含完整的功能闭环src/ domains/ user/ auth/ # 子域认证 profile/ # 子域用户资料 shared/ # 域内共享 domain.ts # 域的公共接口 order/ cart/ # 子域购物车 payment/ # 子域支付 shared/ domain.ts shared/ # 跨域共享仅基础工具优势与业务边界对齐团队可按域分工。劣势需要业务建模能力前期投入大。二、从 Type-based 到 Feature-based 的迁移路径迁移的驱动力通常是团队规模增长。5 人团队用 type-based 没有问题15 人团队就会出现改一个功能要协调 3 个目录负责人的问题。迁移步骤第一步识别功能边界。以路由为线索每个路由对应一个 feature 目录的候选。第二步迁移低耦合功能。先迁移那些依赖最少的功能如关于页、设置页验证目录结构可行性。第三步逐步迁移核心功能。核心功能依赖复杂需要先梳理依赖图。/** * 目录结构迁移工具依赖图分析 * 扫描源码中的 import 关系生成功能间的依赖矩阵 */ interface DependencyNode { feature: string; imports: Mapstring, number; // 依赖的其他功能及引用次数 internalFiles: number; // 功能内部的文件数 } async function analyzeFeatureDependencies( srcPath: string, routeMap: Mapstring, string ): PromiseDependencyNode[] { try { const nodes: DependencyNode[] []; for (const [route, featureName] of routeMap) { const featurePath ${srcPath}/${featureName}; const imports new Mapstring, number(); // 扫描该功能目录下所有文件的 import 语句 const files await glob(${featurePath}/**/*.{ts,tsx,js,jsx}); for (const file of files) { const content await readFile(file, utf-8); const importMatches content.matchAll( /import\s.*?\sfrom\s[](\/|src\/)(.*?)[]/g ); for (const match of importMatches) { const importPath match[2]; // 解析 import 路径对应的功能名称 const targetFeature resolveFeatureFromPath(importPath, routeMap); if (targetFeature targetFeature ! featureName) { imports.set( targetFeature, (imports.get(targetFeature) || 0) 1 ); } } } nodes.push({ feature: featureName, imports, internalFiles: files.length, }); } return nodes; } catch (error) { console.error( 依赖分析失败: ${error instanceof Error ? error.message : String(error)} ); return []; } }迁移中的依赖治理迁移时最常见的问题是循环依赖。两个 feature 互相引用对方的组件意味着边界划分有误。解决方案有三种将共享部分提取到shared/目录重新划分功能边界可能两个 feature 应合并用事件总线替代直接引用仅在运行时解耦场景使用实测数据一个 120 个文件的 Vue3 项目从 type-based 迁移到 feature-based 花费 2 周。迁移后修改一个功能的平均文件跳转数从 4.2 降低到 1.3。三、从 Feature-based 到 Domain-driven 的跨越条件不是所有项目都需要 domain-driven 结构。跨越的触发条件有三个团队按业务域分工——每个域有独立的 Product Owner 和交付节奏功能数量超过 20 个——feature-based 目录开始出现一个 feature 里塞了 8 个子功能的问题跨域集成是主要复杂度来源——feature 之间的依赖关系比 feature 内部更复杂如果以上三个条件都不满足feature-based 就是合理的终态。不要为了架构先进性强行升级。Domain 接口设计的关键原则每个域对外暴露一个domain.ts文件作为域的公共接口。域内的其他文件不直接被外部引用。/** * user 域的公共接口定义 * 外部只能通过此文件访问 user 域的功能 * 域内的组件、hooks、utils 一律不直接导出 */ // 认证相关 export { useAuth, AuthProvider, LoginForm } from ./auth; // 用户资料相关 export { useUserProfile, UserProfileCard } from ./profile; // 域内共享的类型定义 export type { User, UserProfile, AuthState } from ./shared/types; // 域的配置 export const USER_DOMAIN_CONFIG { apiBase: /api/user, cacheTimeout: 5 * 60 * 1000, // 缓存超时 5 分钟 } as const;域之间的引用必须通过domain.ts禁止直接引用域内文件。这条规则可以用 ESLint 自定义规则强制执行/** * ESLint 自定义规则禁止跨域直接引用 * 仅允许引用其他域的 domain.ts 文件 */ const noCrossDomainDirectImport: Rule.RuleModule { meta: { type: error, docs: { description: 禁止跨域直接引用内部文件, }, }, create(context) { const domains [user, order, inventory, analytics]; const domainPattern new RegExp( src/domains/(${domains.join(|)})/(?!domain\\.ts) ); return { ImportDeclaration(node) { const importPath node.source.value as string; // 检查是否引用了其他域的内部文件 if (domainPattern.test(importPath)) { const currentFile context.filename; const currentDomain getCurrentDomain(currentFile, domains); // 提取被引用的域名称 const match importPath.match(domainPattern); const targetDomain match?.[1]; if (targetDomain currentDomain targetDomain ! currentDomain) { context.report({ node, message: 跨域引用必须通过 domain.ts: ${currentDomain} 域不应直接引用 ${targetDomain} 域的内部文件, }); } } }, }; }, };四、三阶段演进中的成本与收益数据指标Type-basedFeature-basedDomain-driven新人上手时间0.5 天1 天2~3 天单功能修改涉及的目录数4~61~21跨功能依赖可见性低中高通过 domain.ts迁移成本120 文件项目—2 周4 周适用团队规模3~5 人8~15 人15 人适用功能数量 1010~20 20关键发现从 type-based 到 feature-based 的投入产出比最高2 周迁移修改效率提升 60%从 feature-based 到 domain-driven 的边际收益递减额外 2 周效率提升约 20%Domain-driven 的主要收益不在开发效率在团队分工清晰度和长期架构可控性结论目录结构的演进是团队规模与项目复杂度共同驱动的结果而非架构师的个人偏好。核心结论有三点第一type-based 是合理的起点不是落后。5 人团队强行用 domain-driven只会增加不必要的认知负担。第二feature-based 是大多数项目的最优终态。它平衡了修改隔离和上手成本迁移投入产出比最高。第三domain-driven 的触发条件是业务域分工不是文件数量。只有当团队按域分工交付时domain-driven 才能发挥价值。演进路径不是线性的升级而是约束条件变化后的适应性调整。不要追求最先进的目录结构追求最适合当前约束的结构。