组件化设计原则:从 8 个业务组件看模块复用
一、引言
在 HarmonyOS 应用开发中,组件化是应对业务复杂度增长的核心手段。当项目从单模块向多模块演进时,如何设计高内聚、低耦合的业务组件,如何管理组件的构建与发布,成为架构师必须回答的问题。
本文以一个包含 11 个模块的 HarmonyOS 英语学习 App 为案例,深入分析其components/目录下 8 个独立业务组件的设计模式,涵盖独立模块配置、接口导出、状态边界控制和 HAR 包构建等关键主题。这 8 个组件分别是:
| 组件名称 | 功能定位 | 依赖关系 |
|---|---|---|
| base_select | 基础选择器 | 无依赖 |
| select_category | 分类选择 | 无依赖 |
| answer_questions | 答题引擎 | 无依赖 |
| login_info | 登录模块 | 无依赖 |
| aggregated_payment | 聚合支付 | 无依赖 |
| feedback | 意见反馈 | 依赖 axios、mock |
| search | 搜索 UI | 无依赖 |
| search_question | 题目搜索 | 无依赖 |
其中 7 个组件零外部依赖,体现了纯粹的组件化设计思想。
二、独立模块的 build-profile.json5 配置
每个组件都是一个独立的 HAR(Harmony Archive)模块,拥有自己的build-profile.json5文件。以base_select为例:
{ "apiType": "stageMode", "buildOption": {}, "buildOptionSet": [ { "name": "release", "arkOptions": { "obfuscation": { "ruleOptions": { "enable": false, "files": ["./obfuscation-rules.txt"] }, "consumerFiles": ["./consumer-rules.txt"] } } } ], "targets": [ { "name": "default" }, { "name": "ohosTest" } ] }组件模块的配置要点如下:
1. apiType 固定为 stageMode
HarmonyOS 从 API 9 开始全面推行 Stage 模型,所有组件模块必须统一使用stageMode,确保生命周期管理和上下文传递的一致性。
2. consumerFiles 混淆规则
作为 HAR 包,组件需要提供consumer-rules.txt,将混淆规则传递给最终消费者。这确保了组件内部的代码经过混淆保护,同时对外暴露的接口保持可用。
3. 极简的 buildOption
组件模块无需关心签名、权限等应用级配置,因此buildOption通常为空。配置的简化也是组件化的优势之一——聚焦业务逻辑而非工程配置。
4. 项目级模块注册
在根目录build-profile.json5中,所有组件模块需要注册到modules数组:
{ "modules": [ { "name": "base_select", "srcPath": "./components/base_select" }, { "name": "select_category", "srcPath": "./components/select_category" }, { "name": "answer_questions", "srcPath": "./components/answer_questions" }, { "name": "login_info", "srcPath": "./components/login_info" }, { "name": "aggregated_payment", "srcPath": "./components/aggregated_payment" }, { "name": "feedback", "srcPath": "./components/feedback" }, { "name": "search", "srcPath": "./components/search" }, { "name": "search_question", "srcPath": "./components/search_question" } ] }三、Index.ets 导出组件能力的模式
每个组件模块的Index.ets是模块对外的唯一接口门面(Facade),这是模块化设计的第一原则:接口先行,实现隐藏。
3.1 简单导出:直接暴露组件
最简单的模式如search组件,仅导出一个页面组件:
// components/search/Index.etsexport{SearchPage}from'./src/main/ets/components/SearchPage';search_question同样遵循此模式:
// components/search_question/Index.etsexport{SearchQuestionPage}from'./src/main/ets/components/SearchQuestionPage';3.2 复合导出:组件 + 数据模型
更常见的做法是同时导出组件和数据模型,让消费者既能使用 UI 组件,也能引用类型定义。以base_select为例:
// components/base_select/Index.etsexport{BaseSelect,ContentItemModel,SelectItemModel,ThirdItemModel,SecondItemModel,ItemDetail,SecondParam}from'./src/main/ets/components/MainPage';这里导出了 1 个组件和 6 个数据模型类型。消费者可以这样使用:
import{BaseSelect,SelectItemModel}from'base_select';3.3 全量导出:复杂业务模块
对于像answer_questions这样功能丰富的组件,其 Index.ets 采用了全量导出策略:
// components/answer_questions/Index.etsexport*from'./src/main/ets/components/AnswerQuestionsPage';export*from'./src/main/ets/components/AddNotePage';export*from'./src/main/ets/components/AnswerSheetPage';export{TopicItemModel,AnswerItem,TopicItemModelPreference,AnswerItemPreference}from'./src/main/ets/viewModel/TopicItemModel';export{TopicItemType,AnswerItemType,QUESTION_DATA,WORD_QUESTION_DATA,LISTENING_QUESTION_DATA,READING_QUESTION_DATA,GRAMMAR_QUESTION_DATA,QuestionsRouterModel}from'./src/main/ets/viewModel/TopicPageModel';export{AnswerSheetDialog}from'./src/main/ets/dialog/AnswerSheetDialog';export{AddNoteDialog}from'./src/main/ets/dialog/AddNoteDialog';这里导出了 3 个页面组件、2 个对话框、4 个数据模型和 7 个常量/枚举。特别值得注意的是QuestionsRouterModel——这是一个导航路由模型,表明该组件具备独立的路由能力,可以在内部管理自己的页面栈。
3.4 能力导出:服务 + 组件
aggregated_payment组件展示了服务能力的导出模式:
// components/aggregated_payment/Index.etsexport{PaymentCallback,PaymentType}from'./src/main/ets/common/CustomTypes';export{WechatPaymentInfo}from'./src/main/ets/models/WechatPaymentInfo';export{HuaweiPaymentInfo}from'./src/main/ets/models/HuaweiPaymentInfo';export{AlipayPaymentInfo}from'./src/main/ets/models/AlipayPaymentInfo';export{CashierPicker}from'./src/main/ets/CashierPicker';export{aggregatedPaymentService}from'./src/main/ets/AggregatedPaymentService';不仅导出了 UI 组件CashierPicker,还导出了聚合支付服务aggregatedPaymentService和多种支付模型。这种模式下,组件不仅是 UI 的复用,更是业务能力的复用。
四、组件内部状态与外部参数的边界
良好的组件化设计需要清晰划分"内部状态"和"外部参数"的边界。本项目使用 ArkTS V2 装饰器体系来实现这一划分。
4.1 @Param:外部传入的参数
以login_info组件的QuickLogin为例:
@ComponentV2exportstruct QuickLogin{// 外部参数:由父组件传入@ParamisBindContentCover:boolean=false;@Param@Requireicon:ResourceStr;@Param@RequireloginBtnBgColor:string|ResourceStr;@Param@RequireappName:string='';// 事件回调:通知父组件@EventonLoginWithHuaweiID:(flag:boolean,phone?:string)=>void=()=>{};@EventonPrivacyPolicy:()=>void=()=>{};@EventonServicePolicy:()=>void=()=>{};@EventshouldDismiss:()=>void=()=>{};// 内部状态:组件自己管理@LocalisSelected:boolean=false;@LocalquickLoginAnonymousPhone:string='';}设计原则清晰:
@Param标注外部注入的参数,使用@Require标记必填项@Event标注事件回调,组件通过回调通知父组件状态变化@Local标注内部状态,外部不可直接干涉
4.2 父组件如何使用
在LoginPage中,组件的使用方式如下:
@ComponentV2exportstruct LoginPage{isBindContentCover:boolean=false;build(){Column(){QuickLogin({isBindContentCover:this.isBindContentCover,icon:$r('app.media.exam_icon_login'),loginBtnBgColor:'#4B5CC4',appName:'备考',onLoginWithHuaweiID:(flag:boolean,phone?:string)=>{// 处理登录成功逻辑letprop:UserInfo=AppStorageV2.connect(UserInfo,()=>newUserInfo())!;prop.isLogin=true;// ...},shouldDismiss:()=>{RouterModule.pop();}});}}}父组件通过传参控制组件的行为,通过回调监听组件的事件,两者之间没有直接的状态共享,实现了彻底的解耦。
4.3 参数设计最佳实践
- 必填参数用 @Require:编译期检查,防止遗漏
- 提供合理的默认值:非必填参数给出默认值,降低使用成本
- 回调用 @Event:语义清晰,区别于 @Param
- 内部状态用 @Local:外部不可修改,保持封装性
- 复杂参数使用接口类型:提高可读性和类型安全
五、HAR 包的构建与引用
5.1 构建 HAR 包
HAR(Harmony Archive)是 HarmonyOS 的静态共享包格式,类似于 Android 的 AAR。当使用hvigor构建时,组件模块默认输出.har文件:
components/base_select/build/default/outputs/default/base_select.harHAR 包的特点:
- 包含编译后的字节码和资源文件
- 不包含签名信息,由最终应用签名
- 支持混淆(通过 consumer-rules.txt)
- 支持资源合并
5.2 引用方式
在消费者模块的oh-package.json5中通过文件路径引用:
// product/entry/oh-package.json5 { "dependencies": { "login_info": "file:../../components/login_info", "answer_questions": "file:../../components/answer_questions", "feedback": "file:../../components/feedback" } }而homePagefeature 模块则引用了更多组件:
// features/homePage/oh-package.json5 { "dependencies": { "commonlib": "file:../../commons/commonLib", "login_info": "file:../../components/login_info", "search": "file:../../components/search", "search_question": "file:../../components/search_question", "answer_questions": "file:../../components/answer_questions", "base_select": "file:../../components/base_select", "select_category": "file:../../components/select_category" } }5.3 独立组件的依赖策略
值得关注的是,8 个组件中仅有feedback组件具有外部依赖:
// components/feedback/oh-package.json5 { "dependencies": { "@ohos/axios": "2.2.6", "@hw-agconnect/axios-mock-adapter": "1.0.0", "@hw-agconnect/util-log": "1.0.0", "@hw-agconnect/ui-toast": "1.0.0" } }而base_select、answer_questions、login_info等 7 个组件的依赖为空:
// components/base_select/oh-package.json5 { "dependencies": {} }这种零依赖设计是组件复用的最高境界——组件不依赖任何外部库或框架,可以嫁接到任何项目中。实际开发中应尽可能保持组件的纯净性,将外部依赖降到最低。
六、最佳实践总结
1. 独立配置,独立构建
每个组件拥有自己的build-profile.json5、oh-package.json5、hvigorfile.ts,实现构建层面的完全独立。
2. Index.ets 作为唯一出口
所有对外暴露的能力通过 Index.ets 导出,消费者禁止引用模块内部路径。
3. 清晰的装饰器语义
@Param:外部传入参数@Event:事件回调@Local:内部状态@Require:必填标记
4. 最小依赖原则
组件应尽可能零外部依赖,必须的依赖通过oh-package.json5声明并由消费者传递。
5. HAR 包的版本管理
每个组件维护独立的CHANGELOG.md和LICENSE,支持独立的版本迭代和发布。
通过以上设计,本项目实现了组件的最大化复用——同一个组件可以服务于多个 feature 模块,甚至可以跨项目复用。当新业务需要类似功能时,只需要引入对应的 HAR 包并传入参数即可,无需重复开发。
七、参考源码路径
| 文件 | 路径 |
|---|---|
| 根模块配置 | d:\HarmonyOS\WorkSpace\Exam1.0.4\build-profile.json5 |
| base_select 模块 | d:\HarmonyOS\WorkSpace\Exam1.0.4\components\base_select\ |
| answer_questions 模块 | d:\HarmonyOS\WorkSpace\Exam1.0.4\components\answer_questions\ |
| login_info 模块 | d:\HarmonyOS\WorkSpace\Exam1.0.4\components\login_info\ |
| feedback 模块 | d:\HarmonyOS\WorkSpace\Exam1.0.4\components\feedback\ |
| aggregated_payment 模块 | d:\HarmonyOS\WorkSpace\Exam1.0.4\components\aggregated_payment\ |