鸿蒙大型项目高级架构:MVVM/MVI分层设计/DI容器依赖注入/模块化治理方案

📅 2026/7/26 17:12:58
鸿蒙大型项目高级架构:MVVM/MVI分层设计/DI容器依赖注入/模块化治理方案
一、前置思考随着应用从Demo级演进到企业级代码量膨胀、模块增多、职责混乱——这些问题在鸿蒙项目中同样适用且更加棘手因为ArkTS的类型系统限制了某些传统设计模式的直接应用无反射、无装饰器元数据、无动态代理。如何在ArkTS的约束下设计出可维护、可测试、可扩展的大型项目架构本文将分模块化治理、DI容器、MVVM分层、Repository模式四个维度展开。企业级架构要解决的核心问题改动隔离修改A功能B功能不受影响高内聚低耦合可测试性业务逻辑可以脱离UI在单元测试中验证团队协作多人并行开发不同模块不产生代码冲突演进能力新增功能、替换技术栈时成本可控二、文件结构治理2.1 按特性分层 vs 按类型分层按类型分层初创项目常用不适合大型项目entry/src/main/ets/ ├── models/ → 散落各处的数据类找不到归属 ├── pages/ → 页面文件50个时找文件像大海捞针 ├── components/ → 通用组件和业务组件混在一起 └── utils/ → 各种工具函数堆砌按特性分层推荐entry/src/main/ets/ ├── model/ # 数据模型Entity/Interface/Enum ├── viewmodel/ # 视图模型ObservedV2驱动UI ├── repository/ # 数据仓储本地/远程数据源抽象 ├── service/ # 业务服务层单例/工厂 ├── component/ # 通用UI组件与业务无关 │ ├── SmartCard/ │ ├── GradientButton/ │ └── ProgressRing/ ├── pages/ # 页面Entry薄薄一层 ├── di/ # 依赖注入容器 ├── theme/ # 主题管理 ├── router/ # 路由管理 └── utils/ # 纯工具函数 ├── LoggerUtil.ets ├── ToastUtil.ets └── DateUtil.ets两种分层的关键区别按类型分层文件找得快所有model放一起但改动要跨多个目录按特性分层单特性改动在一个目录内完成新增/删除特性也干净2.2 模块间依赖规则┌─────────────────────────────────────┐ │ PagesEntry薄薄一层组合 │ │ 依赖 → viewmodel component │ ├─────────────────────────────────────┤ │ ViewModelObservedV2UI状态逻辑 │ │ 依赖 → repository service │ ├─────────────────────────────────────┤ │ Repository数据抽象单一真相源 │ │ 依赖 → model │ ├─────────────────────────────────────┤ │ Service业务逻辑可跨Repository │ │ 依赖 → model repository │ ├─────────────────────────────────────┤ │ Model纯数据无依赖 │ └─────────────────────────────────────┘依赖铁律Model不依赖任何层最底层Repository不依赖ViewModel/PagePage只依赖ViewModel不直接调Repository所有跨层通信通过接口不通过具体实现三、依赖注入DI完整实现3.1 轻量DI容器设计ArkTS中无法使用reflect-metadata装饰器体系但可以通过工厂注册表实现轻量DIclassDIContainer{privatestaticregistry:Mapstring,ObjectnewMap();// 注册实例通常为单例staticregisterT(key:string,instance:T):void{DIContainer.registry.set(key,instanceasObject);}// 获取实例staticgetT(key:string):T|undefined{returnDIContainer.registry.get(key)asT;}// 检查是否已注册statichas(key:string):boolean{returnDIContainer.registry.has(key);}// 获取所有已注册的key用于调试staticgetAllKeys():string[]{constkeys:string[][];DIContainer.registry.forEach((_:Object,k:string){keys.push(k);});returnkeys;}// 移除注册staticremove(key:string):void{DIContainer.registry.delete(key);}}3.2 DI容器使用流程// 应用启动时app.ets 的 onCreate// 注册数据层constnoteRepo:NoteRepositorynewNoteRepository();DIContainer.register(NoteRepository,noteRepo);// 注册服务层constuserService:UserServicenewUserService();DIContainer.register(UserService,userService);// 页面/ViewModel 中使用 classNoteViewModel{privaterepo:NoteRepository;constructor(){// 从DI容器获取依赖consttemp:NoteRepository|undefinedDIContainer.getNoteRepository(NoteRepository);this.repotempasNoteRepository;}getNotes():Note[]{returnthis.repo.getAll();}}3.3 DI容器 vs 单例模式维度单例模式直接newDI容器依赖关系硬编码在代码中运行时配置单元测试难Mock单例本身就是具体类易Mock注册时注入Mock实例生命周期管理只有全局单例可控单例/工厂/作用域依赖发现散落各处不可见集中在注册点实际收益假设你有一个NoteRepository单元测试时需要Mock掉数据库调用。用单例你只能修改源码用DI你只需// 测试环境DIContainer.register(NoteRepository,newMockNoteRepository());// 生产环境DIContainer.register(NoteRepository,newNoteRepository());同一个ViewModel代码一行不改。3.4 DI最佳实践集中注册所有register调用集中在一个di/DISetup.ets文件中启动时调用命名规范key用类名‘NoteRepository’不要用魔法字符串‘nr’null检查get()返回undefined时必须处理不能假设一定有值注册时机在Ability.onCreate或首个aboutToAppear中注册保证使用时已就绪四、MVVM分层深度4.1 三层职责View (Page/ComponentV2) ┃ 职责UI渲染 事件分发 ┃ 不包含业务逻辑、数据获取 ┃ 依赖ViewModel通过 Param/Local/ObjectLink ↓ 用户操作 ViewModel (ObservedV2 类) ┃ 职责UI状态管理 业务逻辑调度 ┃ 不包含数据持久化、网络请求 ┃ 依赖Repository Service ↓ 数据请求 Repository ┃ 职责数据获取/存储的抽象 ┃ 不包含UI逻辑、业务校验 ┃ 依赖DataSource本地DB/云端API4.2 Model 数据层数据模型应该是纯数据结构不包含任何业务逻辑// 基础实体接口interfaceEntity{id:number;}// 业务实体interfaceNoteextendsEntity{title:string;content:string;createdAt:string;}// 应用配置独立模型interfaceAppConfig{appName:string;version:string;}模型设计原则用interface而非class定义数据结构ArkTS中interface更轻量继承基础接口Entity确保ID一致性不包含方法纯数据结构方法放在Repository/Service中4.3 Repository 数据仓储Repository是数据源的单一真相源classNoteRepository{privatedata:Mapnumber,NotenewMap();privatenextId:number1001;constructor(){// 预填充初始数据生产环境应从DB/API加载constinitNotes:Note[][{id:1001,title:架构设计文档,content:MVVM DI Repository 三层架构方案,createdAt:2026-07-20},{id:1002,title:API接口规范,content:RESTful API设计规范 v2.0,createdAt:2026-07-18}];for(leti:number0;iinitNotes.length;i){constnote:NoteinitNotes[i];this.data.set(note.id,note);}}getAll():Note[]{constresult:Note[][];this.data.forEach((note:Note){result.push(note);});returnresult;}getById(id:number):Note|undefined{returnthis.data.get(id);}add(title:string,content:string):Note{constnote:Note{id:this.nextId,title:title,content:content,createdAt:this.getCurrentDate()};this.data.set(this.nextId,note);this.nextId;returnnote;}delete(id:number):boolean{returnthis.data.delete(id);}privategetCurrentDate():string{constnow:DatenewDate();constyear:numbernow.getFullYear();constmonth:stringString(now.getMonth()1).padStart(2,0);constday:stringString(now.getDate()).padStart(2,0);returnyear-month-day;}}Repository设计要点对外暴露语义化方法getAll、getById、add、delete隐藏存储细节内部可以切换存储实现当前用Map未来可以切换到关系型数据库对外接口不变返回的是数据副本或直接引用取决于场景4.4 ViewModel 状态管理ObservedV2classNoteListViewModel{Tracenotes:Note[][];Traceloading:booleanfalse;Traceerror:string;privaterepo:NoteRepository;constructor(){consttemp:NoteRepository|undefinedDIContainer.getNoteRepository(NoteRepository);this.repotempasNoteRepository;this.loadNotes();}loadNotes():void{this.loadingtrue;this.error;// 生产环境这里可能是异步API调用this.notesthis.repo.getAll();this.loadingfalse;}deleteNote(id:number):void{this.repo.delete(id);this.notesthis.repo.getAll();// 刷新列表}}ViewModel设计要点用ObservedV2Trace让属性变更自动通知UI刷新通过DI容器获取Repository依赖而非直接new暴露加载/错误状态loading、error让UI层做兜底展示4.5 View 层View层Page/ComponentV2的代码应该极薄EntryComponentV2struct ArchitectureDemo{LocalviewModel:NoteListViewModelnewNoteListViewModel();LocalselectedNoteId:number-1;build(){Column(){// 加载指示器if(this.viewModel.loading){LoadingProgress().width(40).height(40)}// 错误提示if(this.viewModel.error!){Text(this.viewModel.error).fontColor(#FF5252).fontSize(14)}// 列表List(){ForEach(this.viewModel.notes,(note:Note){ListItem(){Text(note.title).fontSize(16).fontColor(#FFFFFF)}.onClick((){this.selectedNoteIdnote.id;})})}}}}View层铁律❌ 不包含数据库操作❌ 不包含网络请求❌ 不包含复杂业务逻辑✅ 只做三件事渲染UI、分发事件、订阅ViewModel状态五、错误处理架构5.1 统一错误处理策略用户操作 → ViewModel.foo() → try { Repository.bar() } catch (e) { → 记录日志LoggerUtil.error → 更新ViewModel.error状态 → toast提示用户 → 可选上报错误到监控平台 }5.2 错误分类错误类型处理策略用户提示网络错误自动重试3次 → 提示“网络连接失败请检查网络”数据校验失败阻断操作 → 高亮错误字段“标题不能为空”权限不足引导授权“需要相机权限请前往设置”未知异常记录堆栈 → 上报“出了点问题请稍后重试”六、代码规范与工具链6.1 命名规范文件命名PascalCase → UserRepository.ets、SmartCard.ets 接口/类PascalCase → NoteRepository、AppConfig 变量/方法camelCase → noteTitle、getAllNotes() 常量UPPER_SNAKE → MAX_RETRY_COUNT Entry页面以Demo/Page结尾 → ThemeArchitectureDemo6.2 文件内声明顺序按照workspace规范严格排序1. imports 2. interface / type / enum / class (按依赖顺序) 3. const 常量 4. Builder 函数 5. Entry / ComponentV2 struct: 5.1 状态变量 (Local / Param / Provide) 5.2 生命周期 (aboutToAppear / aboutToDisappear) 5.3 Builder 方法 5.4 build()七、避坑速查坑现象原因解决DI使用前未注册启动闪退get()返回undefined后直接使用在启动最早时机集中注册get()后判nullViewModel直接操作UI业务逻辑中出现UI引用违反了MVVM分层ViewModel只管理状态不引用组件Repository返回可变对象外部修改了内部数据Map/Array返回的是引用返回时做浅拷贝或使用不可变数据循环依赖编译报错或启动卡死A依赖BB依赖A引入中间接口解耦ViewModel臃肿单个ViewModel超过500行职责不单一按页面/功能拆分ViewModel八、总结大型项目的架构目标不是用的设计模式最多而是改动成本最低。核心原则依赖倒置DIP高层不依赖低层两者都依赖抽象DI容器就是那个抽象单一职责SRPPage只管渲染、ViewModel只管状态、Repository只管数据接口隔离ISP不强迫调用方依赖它不需要的方法一个文件只做一件事200行以内最优500行开始有异味MVVM DI Repository三层足够覆盖90%的企业级场景。剩下10%的复杂场景跨页面状态共享、复杂表单联动、数据流追溯可以在此基础上叠加Provide/Consume、EventHub等机制。对应Demo文件entry/src/main/ets/pages/ArchitectureDemo.ets