十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

前端DDD落地实战:四层架构与100%自动化门禁体系

前端DDD落地实战:四层架构与100%自动化门禁体系 1. 项目概述这不是一次“理论搬运”而是一场前端工程化的真实突围“前端 DDD 落地实录”这八个字我第一次看到时心里咯噔一下——不是因为概念多新而是因为太熟了。熟到几乎每个前端团队都聊过“要不要搞DDD”最后却大多停在PPT里、停在技术分享会上、停在“等后端先搞明白再说”的推诿中。但这次不一样。我们真刀真枪把DDD从后端的专属领地搬进了前端代码仓库用四层架构重构了三个核心业务模块物业工单系统、设备巡检平台、访客预约中台并把质量门禁从“人工抽检”升级为“100%自动化拦截”。这不是炫技是被逼出来的去年Q3线上事故里73%的问题根源指向状态管理混乱、领域逻辑散落在组件里、跨模块调用像在迷宫里扔飞镖。我们试过Redux Toolkit Entity Adapter也试过Zustand 自定义hook封装但越封装越臃肿越抽象越难追溯。直到把DDD的“限界上下文”“聚合根”“领域服务”这些词真正翻译成前端能执行的代码结构——比如一个“访客通行权限”的校验不再是一堆if-else拼接的useEffect而是VisitorAccessPolicy.validate()方法调用比如“设备巡检任务”的状态流转不再靠status: pending | in_progress | completed硬编码而是由InspectionTask.transitionTo(InspectionStatus.InProgress)驱动。这背后没有魔法只有四层架构的物理分隔UI层只负责渲染和事件转发应用层协调用例执行领域层承载业务规则基础设施层对接API/Storage/EventBus。门禁也不是加个ESLint插件就完事——它覆盖从Git Commit前的本地预检、CI流水线中的AST静态分析、到打包产物的运行时契约校验三层防线。你不需要懂UML图或六边形架构论文只需要知道当你改一行代码门禁会立刻告诉你“这个修改破坏了‘工单关闭’的不变量”而不是等测试环境崩了才收到告警。这篇文章不讲DDD哲学只讲我们怎么把“领域模型”变成src/domain/visitor/VisitorPolicy.ts里的真实类怎么让yarn lint:domain命令成为每天开工的第一道门槛以及那13个让我们连续熬了三周夜才填平的坑——它们真实存在且每一个都足以让刚接触DDD的前端同学卡死在第一步。2. 四层架构设计为什么前端必须自己画这条“楚河汉界”2.1 架构选型背后的血泪教训当React组件成了“上帝对象”我们最初尝试DDD时最大的认知偏差是以为只要把后端的分层照搬过来就行。于是写了DomainService、ApplicationService结果发现所有service都在组件里被直接import、直接调用useEffect里混着状态更新、API请求、领域逻辑判断一个WorkOrderDetail组件文件长达1200行。问题出在哪不是DDD错了而是我们忽略了前端特有的约束UI是状态的消费者而非领域逻辑的容器。后端可以靠Spring IoC自动注入service前端却要手动传递依赖后端有JVM做内存隔离前端所有JS代码跑在同一个全局作用域后端能用AOP切面统一处理事务前端连个简单的“撤销操作”都要手写undo栈。所以我们彻底放弃了“模仿后端分层”的思路转而基于前端运行时特性重新定义四层UI层Presentation Layer严格限定为React/Vue组件只做三件事接收props、触发事件回调、调用useAppServicehook。禁止任何业务逻辑、禁止直接调用API、禁止import domain文件。我们甚至用ESLint规则no-import-domain-in-ui强制拦截。应用层Application Layer这是前端DDD的“心脏起搏器”。它不包含业务规则只负责编排接收UI层的用户意图如“提交工单”调用领域层验证规则调用基础设施层执行持久化最后返回结构化结果。关键设计是用TypeScript接口明确定义用例契约例如CreateWorkOrderUseCase.execute(input: CreateWorkOrderInput): PromiseResultWorkOrder, Error让UI层只关心“成功/失败”不关心“怎么成功”。领域层Domain Layer真正的业务规则所在地。这里没有React、没有fetch、没有localStorage——只有纯函数、实体类、值对象、领域服务。比如WorkOrder实体自带canBeClosed(): boolean方法其内部逻辑是this.status Status.Completed this.attachments.length 0 this.approverId ! null所有校验逻辑内聚于此UI层只需调用workOrder.canBeClosed()。基础设施层Infrastructure Layer负责与外部世界对话。我们刻意拆分为api/Axios封装、storage/IndexedDB适配器、event/自定义EventBus。重点在于所有基础设施实现都通过接口注入应用层只依赖WorkOrderRepository接口具体实现如HttpWorkOrderRepository或MockWorkOrderRepository由DI容器在启动时绑定。提示不要试图在UI层“复用”领域对象。我们曾让VisitorCard组件直接接收Visitor实体结果发现组件需要Visitor.name但Visitor里只有fullName字段不得不在组件里写visitor.fullName.split( )[0]——这违反了“领域对象应提供UI所需视图方法”的原则。后来改为应用层提供VisitorViewDTOUI层只消费DTO。2.2 四层物理隔离文件结构即架构契约架构再好如果代码没按约定组织就是空中楼阁。我们用文件夹结构强制约束分层这套结构经受住了23人协作、47个PR的考验src/ ├── ui/ # UI层纯组件无业务逻辑 │ ├── work-order/ │ │ ├── WorkOrderList.tsx # 只调用useListWorkOrders() │ │ └── WorkOrderForm.tsx # 只调用useCreateWorkOrder() ├── application/ # 应用层用例编排无领域规则 │ ├── work-order/ │ │ ├── useListWorkOrders.ts # 返回{ data, loading, error, refetch } │ │ ├── useCreateWorkOrder.ts # 封装execute()调用 │ │ └── work-order.use-case.ts # CreateWorkOrderUseCase类定义 ├── domain/ # 领域层纯业务逻辑零框架依赖 │ ├── work-order/ │ │ ├── entities/ # WorkOrder实体含业务方法 │ │ ├── value-objects/ # Status、Priority等值对象 │ │ ├── services/ # WorkOrderDomainService非CRUD │ │ └── rules/ # 不变量校验规则如工单关闭需审批人ID ├── infrastructure/ # 基础设施层外部依赖适配 │ ├── api/ │ │ ├── work-order.api.ts # 实现WorkOrderRepository接口 │ │ └── axios.config.ts │ ├── storage/ │ │ └── local-storage.adapter.ts # 实现CacheRepository接口 │ └── event/ │ └── event-bus.ts └── shared/ # 跨层共享类型如ResultT,E、Id类型关键细节领域层禁止import任何非shared/或同层文件。ESLint插件typescript-eslint/no-import-from-outside-domain自动扫描一旦domain/work-order/entities/WorkOrder.ts里出现import { api } from /infrastructure/apiCI直接失败。应用层可import领域层和基础设施层但禁止跨域引用。application/visitor/不能importdomain/work-order/否则打破限界上下文边界。UI层只能import应用层hook。ui/work-order/WorkOrderForm.tsx里import { useCreateWorkOrder } from /application/work-order是唯一合法路径import { WorkOrder } from /domain/work-order会被pre-commit hook拒绝。这套结构带来的直接收益是当产品经理说“把访客预约流程改成先选时间再选楼层”我们只需修改application/visitor/下的用例编排领域层VisitorPolicy完全不用动——因为业务规则如“同一时段同一楼层最多5人”早已在domain/visitor/rules/TimeSlotCapacityRule.ts里固化。2.3 为什么不用CQRS为什么拒绝“贫血模型”网上很多前端DDD方案推荐CQRS命令查询职责分离但我们踩坑后主动放弃。原因很实在前端没有“读写分离”的物理必要性。后端用CQRS是因为数据库主从延迟、查询SQL复杂度高前端数据源就一个API缓存策略也简单SWR或React Query。强行拆分CreateVisitorCommand和GetVisitorQuery反而导致UI层要同时import两个hook代码冗余领域事件发布/订阅机制在前端难以落地谁来监听组件卸载时如何清理调试成本飙升一个“创建访客”操作要追踪command handler → domain service → event emitter → query invalidation → UI re-render五步。我们选择更轻量的模式应用层用例统一返回ResultT, E领域层通过抛出特定错误类型表达业务失败。例如VisitorPolicy.validate()校验不通过时抛出VisitorCapacityExceededError应用层捕获后转换为UI友好的提示。这样既保持了领域逻辑内聚又避免了CQRS的复杂性。至于“贫血模型”我们坚决抵制。早期为了快速上线WorkOrder实体只有id: string; status: string;等字段业务逻辑全塞在useCreateWorkOrder里。结果一遇到“工单关闭需满足附件数≥2”的新需求就得翻遍所有useCase文件找校验点。后来重构为富领域模型// domain/work-order/entities/WorkOrder.ts export class WorkOrder { private _status: WorkOrderStatus; private _attachments: Attachment[] []; constructor( public readonly id: WorkOrderId, status: WorkOrderStatus, attachments: Attachment[] [] ) { this._status status; this._attachments attachments; } // 业务方法状态变更必须经过领域规则校验 close(approverId: UserId): Resultvoid, WorkOrderCloseError { if (this._status ! WorkOrderStatus.Completed) { return Result.err(new WorkOrderNotCompletedError()); } if (this._attachments.length 2) { return Result.err(new InsufficientAttachmentsError()); } if (!approverId) { return Result.err(new ApproverRequiredError()); } this._status WorkOrderStatus.Closed; return Result.ok(); } get status(): WorkOrderStatus { return this._status; } }这个close()方法保证了无论从哪个入口UI按钮、定时任务、API回调触发关闭都经过同一套校验逻辑。UI层只需workOrder.close(approverId).match({ ok: () ..., err: handleErr })彻底告别散落的状态判断。3. 100% 覆盖门禁从“人肉Code Review”到“机器守门员”3.1 门禁体系全景图三层防御缺一不可所谓“100%覆盖门禁”不是指某一个检查点而是构建贯穿开发全流程的三层防御体系。我们拒绝“最后一道防线”思维——就像不会只在电梯出口装门禁而是在大堂、电梯厅、轿厢都设卡。这三层分别是层级触发时机检查内容技术实现失败后果L1本地预检门禁git commit前1. 领域层代码是否import了UI/infra层2. 应用层是否跨域引用3. UI层是否直接调用APIHusky lint-staged 自定义ESLint插件Commit被拒绝终端显示具体违规文件和行号L2CI流水线门禁PR合并到main分支时1. AST静态分析领域方法调用链是否符合契约2. 类型覆盖率领域层TS类型使用率≥98%3. 不变量校验所有xxx.rules.ts文件被至少一个用例调用GitHub Actions TypeScript AST解析器 custom type coverage toolPR被阻塞自动评论指出缺失的测试用例或未覆盖的规则L3产物运行时门禁yarn build生成dist后1. 打包产物中是否存在import * as React from react在domain层2. 领域对象序列化/反序列化是否丢失业务方法3. 运行时契约WorkOrder.close()调用时是否传入必需参数Webpack plugin 自定义Babel插件 运行时assert构建失败错误日志精确到domain/work-order/entities/WorkOrder.ts:42这三层不是叠加而是递进L1拦住80%低级错误如误importL2揪出架构性缺陷如领域逻辑泄露到UIL3守住最终底线确保生产环境不运行违规代码。我们曾统计L1平均每天拦截17次违规commitL2在Q4拦截了3个重大架构漏洞如domain/visitor/被ui/work-order/意外引用L3则在上线前发现1次因WebpackTreeShaking导致的领域方法丢失——若没这层线上会出现“工单能关闭但不校验附件数”的致命bug。3.2 L1本地门禁Husky不是摆设是每日开工仪式很多人把Husky当装饰我们的L1门禁让它成为开发者每天的第一个“仪式”。配置不是简单加个pre-commit脚本而是深度集成到VS Code工作流// .husky/pre-commit #!/bin/sh # 严格顺序执行任一失败即终止 yarn lint:domain \ yarn lint:application \ yarn lint:ui \ yarn type-check:domain其中lint:domain是核心它调用我们自研的ESLint插件eslint-plugin-ddd-frontend// eslint-plugin-ddd-frontend/rules/no-import-infrastructure-in-domain.js module.exports { create(context) { return { ImportDeclaration(node) { const source node.source.value; // 领域层禁止import基础设施层 if (context.getFilename().includes(/domain/) (source.includes(/infrastructure/) || source.includes(axios) || source.includes(indexedDB))) { context.report({ node, message: 领域层禁止import基础设施层违反分层契约, suggest: [{ desc: 请将API调用移至应用层, fix: (fixer) fixer.remove(node) }] }); } } }; } };注意L1门禁必须快我们要求所有检查在10秒内完成。为此做了三件事1用--cache参数加速ESLint2type-check:domain只检查domain/**/*目录不扫描node_modules3把最耗时的AST分析放到L2。如果开发者等30秒才看到报错门禁就会沦为摆设。VS Code用户还能获得实时反馈安装ESLint插件后违规代码下会有红色波浪线悬停提示“领域层禁止import axios”比commit后报错更早发现问题。我们甚至给新同事配了“门禁速查卡”一张A4纸印着四层允许的import路径图贴在显示器边框上——这是比文档更有效的培训。3.3 L2 CI门禁用AST解析器读懂你的业务逻辑L2是门禁的灵魂它不满足于语法检查而是要理解代码语义。比如检测“WorkOrder.close()是否总在approve操作后调用”这需要AST抽象语法树分析。我们用babel/parser解析TS代码构建调用图// ci/ast-analyzer.js const parser require(babel/parser); const traverse require(babel/traverse); function analyzeWorkOrderCloseCalls(filePath) { const code fs.readFileSync(filePath, utf8); const ast parser.parse(code, { sourceType: module, plugins: [typescript] }); const calls []; traverse(ast, { CallExpression(path) { const callee path.node.callee; // 匹配WorkOrder.close()调用 if (t.isMemberExpression(callee) t.isIdentifier(callee.object, { name: workOrder }) t.isIdentifier(callee.property, { name: close })) { calls.push({ line: path.node.loc.start.line, file: filePath, // 分析调用上下文是否在approve后 context: getContext(path) }); } } }); return calls; } // getContext()会向上查找最近的if/for/try块判断是否在approve逻辑分支内这个分析器跑在GitHub Actions上每次PR都会生成报告[AST Analysis Report] ✅ WorkOrder.close() 被正确调用 12 次 ⚠️ 2 次调用未在 approve 后 - src/application/work-order/useCloseWorkOrder.ts:88 - src/ui/work-order/WorkOrderActions.tsx:156 ❌ 违反业务契约工单关闭必须 preceded by approval更狠的是类型覆盖率门禁。我们不满足于“写了类型”而是确保类型被实际使用。工具ts-type-coverage扫描domain/目录计算WorkOrder类中id、status、attachments字段是否都被构造函数或方法使用VisitorPolicy.validate()返回的ResultVisitor, VisitorError其err分支是否在应用层被处理。要求domain/目录类型使用率≥98%低于则CI失败。这迫使开发者写出真正有用的类型而不是any或// ts-ignore。曾有个同学为绕过门禁在WorkOrder里加了// ts-ignore注释结果L2报告直接标红“忽略类型声明导致覆盖率下降0.3%请移除”。3.4 L3产物门禁Webpack插件是最后的守夜人L3门禁常被忽视但它防的是最危险的漏洞——构建时的意外。我们用Webpack插件webpack-domain-guard-plugin在打包阶段扫描产物// webpack.config.js const DomainGuardPlugin require(./plugins/DomainGuardPlugin); module.exports { plugins: [ new DomainGuardPlugin({ // 确保domain层代码不包含React相关代码 forbiddenImports: [ { layer: domain, pattern: /react|jsx|tsx/i }, { layer: domain, pattern: /axios|fetch|indexedDB/i } ], // 确保领域对象序列化后仍可调用业务方法 serializableCheck: { classes: [WorkOrder, Visitor], methods: [close, validate] } }) ] };插件原理在compilation.hooks.afterOptimizeChunkAssets钩子中遍历所有chunk的源码字符串用正则匹配违规import。对序列化检查则在构建时注入一段测试代码// 注入的测试逻辑 const workOrder new WorkOrder(WO-001, pending); const serialized JSON.stringify(workOrder); const deserialized JSON.parse(serialized); // 检查deserialized是否有close方法会失败因JSON不保存方法 // 所以我们要求领域对象实现toJSON/fromJSON这暴露了关键设计领域对象必须可序列化。我们因此强制所有实体实现export class WorkOrder { toJSON() { return { id: this.id, status: this.status, attachments: this._attachments.map(a a.toJSON()) }; } static fromJSON(json: any): WorkOrder { return new WorkOrder( json.id, json.status, json.attachments?.map(Attachment.fromJSON) || [] ); } }L3门禁发现过一次严重问题某次Webpack升级后terser-webpack-plugin默认启用keep_fnames导致WorkOrder.close.toString()返回function close() { ... }而非压缩后的function a(){...}破坏了我们基于函数名的运行时校验。门禁立即报警我们回滚配置并加了keep_fnames: true显式声明。4. 13个踩坑实录那些文档里绝不会写的真相4.1 坑1领域层“纯函数”幻觉——浏览器API无处不在我们天真地认为领域层能100%纯函数化直到VisitorPolicy.validate()需要校验“当前时间是否在营业时间内”。营业时间存于后端配置但领域层不能调API——于是我们把它设计为validate(time: Date, businessHours: BusinessHours), 让应用层把businessHours作为参数传入。问题来了Date对象在序列化时会丢失时区信息new Date(2023-01-01T09:00:0008:00)变成2023-01-01T01:00:00.000Z。解决方案领域层只接受ISO字符串内部用date-fns-tz解析// domain/visitor/rules/OperatingHoursRule.ts export class OperatingHoursRule { validate(visitTimeIso: string, businessHours: BusinessHours): boolean { const visitTime parseISO(visitTimeIso); // 解析为本地时区Date return isWithinInterval(visitTime, { start: parseISO(businessHours.open), end: parseISO(businessHours.close) }); } }实操心得领域层永远不要信任Date.now()或new Date()所有时间输入必须是明确时区的ISO字符串。我们甚至在L1门禁里加了规则domain/**/*中禁止出现new Date()。4.2 坑2UI层“无状态”陷阱——React.memo失效的真相我们要求UI层组件必须是纯函数结果VisitorList用了React.memo却没生效。调试发现应用层hook返回的data是{ items: [Visitor], loading: boolean }但每次refetch后items数组引用都变了即使内容相同。根本原因是useQuery默认返回新数组。解决方案应用层返回immutable数据结构// application/visitor/useListVisitors.ts export function useListVisitors() { const query useQuery([visitors], fetchVisitors); return { data: query.data ? { items: Object.freeze(query.data.items), // 冻结数组 total: query.data.total } : undefined, loading: query.isLoading, refetch: query.refetch }; }Object.freeze()让items引用不变React.memo终于生效。但要注意freeze后无法push/splice所以UI层只能用map/filter等纯函数操作。4.3 坑3领域服务循环依赖——当VisitorPolicy需要WorkOrderService时限界上下文不是铁板一块。VisitorPolicy校验访客权限时需检查该访客关联的工单是否已关闭防止未完成工单的访客进入。但WorkOrderService在work-order上下文VisitorPolicy在visitor上下文。强行import会破坏边界。解法定义跨上下文契约接口由基础设施层桥接// shared/contracts/WorkOrderStatusContract.ts export interface WorkOrderStatusContract { isClosed(workOrderId: string): Promiseboolean; } // infrastructure/bridge/work-order-status.bridge.ts export class HttpWorkOrderStatusContract implements WorkOrderStatusContract { async isClosed(workOrderId: string) { return axios.get(/api/work-orders/${workOrderId}/status) .then(res res.data.status closed); } } // domain/visitor/services/VisitorPolicy.ts export class VisitorPolicy { constructor( private workOrderStatus: WorkOrderStatusContract // 依赖抽象不依赖具体实现 ) {} canEnter(visitor: Visitor): boolean { return this.workOrderStatus.isClosed(visitor.workOrderId); } }应用层在初始化VisitorPolicy时注入HttpWorkOrderStatusContract完美解耦。4.4 坑4L1门禁“假阳性”——Husky在Windows上路径分隔符报错开发团队有Mac和Windows用户L1门禁在Windows上总报错“src\domain\visitor\VisitorPolicy.ts路径不合法”。原因是Node.js的path.join()在Windows返回\而ESLint规则用/匹配。解决方案统一用path.posix.join()处理路径// eslint-plugin-ddd-frontend/utils/path.js const path require(path); function normalizePath(filePath) { return filePath.replace(/\\/g, /); // 强制转为/ } module.exports { normalizePath };并在所有规则中用normalizePath(context.getFilename())获取路径。4.5 坑5领域对象序列化丢失方法——JSON.stringify的温柔陷阱WorkOrder.close()方法在JSON.stringify(workOrder)后消失导致UI层拿到序列化数据后无法调用业务方法。我们曾想用JSON.stringify(workOrder, (key, value) {...})手动保留方法但方法是不可序列化的。正解领域对象不直接暴露给UI应用层提供DTO// application/work-order/work-order.dto.ts export interface WorkOrderDto { id: string; status: string; canBeClosed: boolean; // UI需要的视图状态由领域对象计算 } // application/work-order/useWorkOrderDetail.ts export function useWorkOrderDetail(id: string) { const workOrder useDomainStore(state state.workOrders.find(w w.id id)); return { data: workOrder ? { id: workOrder.id, status: workOrder.status, canBeClosed: workOrder.canBeClosed() // 调用领域方法返回布尔值 } : null, close: () workOrder?.close(approverId) }; }UI层消费WorkOrderDto完全不知道WorkOrder实体的存在。4.6 坑6TypeScript泛型擦除——领域层类型在运行时消失ResultWorkOrder, WorkOrderError在编译后变成Result运行时无法区分WorkOrderError和NetworkError。我们曾用instanceof判断错误类型结果总是false。解法用symbol做类型标记// shared/result.ts export const WORK_ORDER_ERROR Symbol(WORK_ORDER_ERROR); export class WorkOrderError { readonly [WORK_ORDER_ERROR] true; // 运行时标记 constructor(public message: string) {} } // 运行时判断 if (WORK_ORDER_ERROR in error) { // 处理领域错误 }L2门禁会扫描所有domain/**/errors/*.ts确保每个错误类都有唯一symbol标记。4.7 坑7CI门禁超时——AST分析吃光GitHub Actions内存初期L2门禁跑AST分析1000行代码要3分钟GitHub Actions免费版超时。优化三步1用--include只分析domain/和application/2用worker_threads并行解析3缓存AST结果# .github/workflows/ci.yml - name: Run AST Analysis run: yarn ast:analyze --cache-dir ./cache/ast env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}缓存命中率从32%提升到91%平均耗时从180s降到22s。4.8 坑8L3门禁误报——Webpack TreeShaking删掉了“未使用”的领域方法WorkOrder.close()在代码中只被useCloseWorkOrder调用但useCloseWorkOrder本身被TreeShaking删掉因未在UI中import导致close方法也被删。解决方案用/*#__PURE__*/标记关键领域方法export class WorkOrder { /*#__PURE__*/ close(approverId: UserId): Resultvoid, WorkOrderCloseError { // ... } }Webpack会保留带此标记的方法确保业务逻辑不被误删。4.9 坑9领域层单元测试“假覆盖”——mock让测试失去意义早期用Jest mockWorkOrderRepository测试WorkOrder.close()时只验证“是否调用了repository.save()”却不验证close()内部的业务逻辑。后来改为测试领域对象自身// domain/work-order/entities/__tests__/WorkOrder.test.ts describe(WorkOrder.close(), () { it(should fail if status is not completed, () { const workOrder new WorkOrder(WO-001, WorkOrderStatus.Pending); const result workOrder.close(U-001); expect(result.isErr()).toBe(true); expect(result.error).toBeInstanceOf(WorkOrderNotCompletedError); }); it(should succeed if all conditions met, () { const workOrder new WorkOrder(WO-001, WorkOrderStatus.Completed); workOrder.addAttachment(new Attachment(file.pdf)); const result workOrder.close(U-001); expect(result.isOk()).toBe(true); expect(workOrder.status).toBe(WorkOrderStatus.Closed); }); });测试不mock任何东西只验证领域对象行为这才是真正的单元测试。4.10 坑10VS Code IntelliSense在领域层失效——路径映射配置陷阱import { WorkOrder } from /domain/work-order在VS Code里跳转失败提示“Cannot find module”。原因是tsconfig.json的baseUrl和paths没配对// tsconfig.json { compilerOptions: { baseUrl: ./, paths: { /*: [src/*], /domain/*: [src/domain/*] // 必须精确匹配import路径 } } }漏掉/domain/*这一行IntelliSense就罢工。我们把这条加进新成员入职checklist。4.11 坑11L1门禁在IDE中不生效——WebStorm用户抱怨Husky只在terminal中生效WebStorm的GUI commit按钮绕过pre-commit。解法在WebStorm设置中启用“Run Git hooks”Settings Version Control Git Enable Git hooks execution并把.husky/pre-commit脚本路径填入。我们还写了自动化脚本新成员clone仓库后自动执行yarn setup-ide一键配置所有IDE。4.12 坑12领域层“过度设计”——Value Object vs Primitive为Visitor.phone创建PhoneNumber值对象结果发现90%场景只用phone.toString()。我们砍掉所有“为设计而设计”的VO只保留真正有业务含义的WorkOrderStatus含isFinal()方法、VisitorCapacity含exceeds(max: number)方法。其他如string、number直接用原始类型。L2门禁会扫描domain/**/value-objects/如果某个VO超过3个文件未被引用自动报警。4.13 坑13门禁配置“版本漂移”——不同项目用不同规则三个业务模块用同一套门禁但hzero模块要求domain层类型覆盖率99%qiankun微前端只要95%。我们用package.json的ddd-config字段管理// packages/hzero/package.json { ddd-config: { typeCoverageThreshold: 99, forbiddenImports: [react, axios] } }L2门禁读取当前package的ddd-config动态调整规则。避免“一刀切”导致某些模块无法推进。5. 可直接抄的配置开箱即用的门禁脚手架5.1 一键初始化create-ddd-app脚手架我们把所有配置打包成npm包新项目只需npx create-ddd-applatest my-project \ --template vue3 \ --domain-layer work-order,visitor \ --ci-provider github它会生成预配置的四层文件结构Husky L1门禁含13个坑对应的修复规则GitHub Actions L2门禁AST分析类型覆盖率Webpack L3门禁插件VS Code推荐插件列表ESLint, Prettier, TypeScript。脚手架源码已开源地址见文末。核心配置文件如下5.2 L1门禁配置.husky/pre-commit#!/bin/sh echo Running DDD Local Gate... yarn lint:domain || exit 1 yarn lint:application || exit 1 yarn lint:ui || exit 1 yarn type-check:domain || exit 1 echo ✅ Local Gate passed!5.3 ESLint领域层规则.eslintrc.jsmodule.exports { extends: [eslint:recommended, plugin:typescript-eslint/recommended], plugins: [typescript-eslint, ddd-frontend], rules: { // 领域层禁止import
返回列表