在 HarmonyOS 的 ArkUI 应用开发中,构建高复用性的业务组件库是提升开发效率、降低代码冗余以及保障应用一致性的核心手段。基于官方最佳实践,组件的封装与复用主要可以通过以下三个层级来实现:
一、 通用组件样式封装:AttributeModifier
适用场景:当应用中多个不同页面需要共享相同的 UI 样式时(例如:登录页面的登录按钮和购物页面的结账按钮具有相同的颜色、圆角和尺寸),推荐使用AttributeModifier提取公共样式。
实现方式:
- 提供方:创建一个实现系统
AttributeModifier接口的自定义类,在其中封装公共属性(如宽高、字体颜色、背景色等)。 - 使用方:创建该 Modifier 的实例,并将其作为参数传递给系统组件的
.attributeModifier()方法。
注意:AttributeModifier仅适用于系统组件,无法直接修改自定义组件的属性,且支持跨文件复用。
1、 提供方:定义公共样式修饰器(独立文件)
在实际工程中,通常将AttributeModifier的实现类抽取到公共文件中。通过构造函数传参,可以实现样式的动态定制。
// common/CommonButtonModifier.ets import { AttributeModifier, ButtonAttribute } from '@kit.ArkUI'; export class CommonButtonModifier implements AttributeModifier<ButtonAttribute> { // 1. 定义私有变量,支持外部动态修改 private bgColor: ResourceColor = '#007DFF'; private fontSize: number = 16; private radius: number = 24; // 2. 构造函数支持传参,方便使用方按需定制 constructor(bgColor?: ResourceColor, fontSize?: number) { this.bgColor = bgColor ?? this.bgColor; this.fontSize = fontSize ?? this.fontSize; } // 3. 实现核心方法:应用默认状态下的属性 applyNormalAttribute(instance: ButtonAttribute): void { instance .backgroundColor(this.bgColor) .fontSize(this.fontSize) .borderRadius(this.radius) .fontColor('#FFFFFF') .height(48); } }2、 使用方:在页面中跨文件引入并应用
使用方只需导入对应的 Modifier 类,实例化后通过.attributeModifier()绑定到系统组件上,即可享受清爽的链式调用体验。
// pages/LoginPage.ets import { CommonButtonModifier } from '../common/CommonButtonModifier'; @Entry @Component struct LoginPage { // 实例化修饰器,并传入特定业务所需的样式参数 private loginBtnModifier = new CommonButtonModifier('#E60012', 18); build() { Column({ space: 20 }) { // 使用定制样式的按钮 Button('立即登录') .attributeModifier(this.loginBtnModifier) .width('80%') // 使用默认规范样式的按钮 Button('游客访问') .attributeModifier(new CommonButtonModifier()) .width('80%') } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } }3、结合状态变量实现动态样式刷新
AttributeModifier支持结合状态装饰器(如@State),当关联的状态变量发生变化时,会自动触发applyNormalAttribute重新执行,从而实现 UI 的动态刷新。
@Entry @Component struct DynamicStylePage { // 使用 @State 修饰 Modifier,使其具备状态感知能力 @State modifier: CommonButtonModifier = new CommonButtonModifier('#007DFF'); build() { Column({ space: 20 }) { Button('切换主题') .attributeModifier(this.modifier) .width('80%') .onClick(() => { // 修改状态,触发 UI 刷新并重新应用样式 this.modifier = new CommonButtonModifier('#707070'); }) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } }二、 自定义组件封装:@Component 组合
适用场景:当需要复用的不仅是 UI 样式,还包含特定的布局结构和业务逻辑时(例如:一个由 Image + Text 垂直排列组成的图文复合组件),应将其封装为自定义组件。
实现方式:
- 使用
@Component装饰器定义组件,在内部实现不变的布局结构。 - 将可能变化的部分(如文本内容、图片资源、点击事件等)作为参数变量暴露出来。
- 进阶复用:为了让外部使用者能够灵活定制内部子组件的样式,可以在封装组件的变量中添加
AttributeModifier类型的参数。这样,外部调用方既可以直接传入数据,也可以通过传入Modifier实例来修改内部子组件的样式。
1、 提供方:封装图文复合组件
在封装组件时,我们将不变的 UI 结构(如垂直排列、间距)固化在build()函数中,同时将可变的数据(文本、图片)和样式修饰器(AttributeModifier)作为参数暴露给外部。
// components/FeatureCard.ets import { AttributeModifier, TextAttribute } from '@kit.ArkUI'; // 1. 定义外部传入参数的接口,保持组件属性整洁 export interface FeatureCardParams { title: string; icon: Resource; // 支持外部传入修饰器,用于定制内部子组件样式 titleModifier?: AttributeModifier<TextAttribute>; onCardClick?: () => void; // 暴露业务事件回调 } @Component export struct FeatureCard { // 2. 接收外部传入的参数 private params: FeatureCardParams; build() { Column({ space: 10 }) { // 固定的布局结构 Image(this.params.icon) .width(60) .height(60) // 内部子组件,支持通过外部传入的 Modifier 进行样式定制 Text(this.params.title) .fontSize(16) // 默认样式 .attributeModifier(this.params.titleModifier) // 应用外部传入的修饰器 } .padding(16) .borderRadius(12) .backgroundColor('#F5F5F5') .onClick(() => { // 触发业务逻辑回调 this.params.onCardClick?.(); }) } }2、 使用方:灵活组装与样式定制
在页面中使用时,调用方只需关注业务数据的传递。如果默认的样式不满足当前页面的需求,可以直接传入AttributeModifier实例进行覆盖,无需修改组件源码。
// pages/HomePage.ets import { FeatureCard } from '../components/FeatureCard'; import { CommonTextModifier } from '../common/CommonTextModifier'; // 假设我们有一个公共的文本修饰器 @Entry @Component struct HomePage { build() { Column({ space: 20 }) { // 场景 1:使用默认样式 FeatureCard({ title: '基础功能', icon: $r('app.media.icon_basic'), onCardClick: () => { console.log('点击了基础功能'); } }) // 场景 2:深度定制内部样式 FeatureCard({ title: '高级特性', icon: $r('app.media.icon_advanced'), // 传入自定义修饰器,覆盖组件内部的默认字体大小和颜色 titleModifier: new CommonTextModifier(20, '#E60012'), onCardClick: () => { console.log('点击了高级特性'); } }) } .padding(20) } }三、 跨文件组件复用与架构规范
适用场景:随着业务组件增多,需要将组件抽取到公共组件库中,供整个工程的不同模块复用。
实现方式与规范:
- 导出与导入:提供方在公共组件库中定义好组件后,必须使用
export关键字导出;使用方在需要的页面中通过import引入即可。 - 模块化设计:遵循 HarmonyOS 推荐的模块化设计架构,将公共样式、复合组件、组件工厂类等进行合理的目录划分,提升项目的可维护性和团队协作效率。
- 跨 Ability 迁移(API 24+ 新特性):从 API version 24 开始,支持自定义组件跨 Ability 迁移。开发者只需在
module.json5配置文件的metadata标签中配置enableCustomComponentCrossAbility为true,即可实现组件在 UIAbility 间的无缝流转。
1、 提供方:规范导出公共组件
在公共组件库中,组件定义完成后必须使用export关键字显式导出,以便外部模块能够访问。
// common/components/UserProfileCard.ets import { Component } from '@kit.ArkUI'; // 使用 export 关键字导出组件 @Component export struct UserProfileCard { userName: string = '默认用户'; avatar: Resource = $r('app.media.default_avatar'); build() { Row({ space: 10 }) { Image(this.avatar) .width(50) .height(50) .borderRadius(25) Text(this.userName) .fontSize(18) .fontWeight(FontWeight.Bold) } .padding(15) .backgroundColor('#FFFFFF') .borderRadius(12) } }2、 模块化设计:推荐的公共组件库目录结构
遵循 HarmonyOS 推荐的模块化设计架构,将公共样式、复合组件、服务类等进行合理的目录划分,可以显著提升项目的可维护性和团队协作效率。
common/ ├── components/ // 复合组件库 │ ├── UserProfileCard.ets // 用户资料卡片组件 │ ├── FeatureCard.ets // 功能卡片组件 │ └── Index.ets // 统一导出入口文件 ├── modifiers/ // 公共样式修饰器 │ ├── CommonButtonModifier.ets │ └── CommonTextModifier.ets ├── services/ // 业务服务类(如分布式消息总线等) │ └── DistributedMessenger.ets └── utils/ // 工具类 └── Logger.ets统一导出入口文件示例:
// common/components/Index.ets export { UserProfileCard } from './UserProfileCard'; export { FeatureCard } from './FeatureCard';3、 使用方:跨文件引入并使用组件
在业务页面中,通过import语句从公共组件库中引入所需组件,即可像使用本地组件一样无缝调用。
// pages/HomePage.ets // 从公共组件库中导入组件 import { UserProfileCard } from '../common/components'; @Entry @Component struct HomePage { build() { Column({ space: 20 }) { // 直接使用跨文件引入的公共组件 UserProfileCard({ userName: '张三', avatar: $r('app.media.zhangsan_avatar') }) } .padding(20) } }4、 跨 Ability 迁移配置(API 24+)
从 API version 24 开始,自定义组件支持跨 Ability 迁移。要实现这一特性,需要在应用工程的module.json5配置文件中进行如下配置:
// entry/src/main/module.json5 { "module": { // ...其他配置 "metadata": [ { "name": "enableCustomComponentCrossAbility", "value": "true" // 使能自定义组件跨 Ability 迁移 } ] } }四、 底层自定义渲染:NDK 自绘制能力
适用场景:当系统组件和常规组合无法满足特殊的视觉表现(如独特的按钮形状、复杂的文字图像混合图标、游戏引擎接入等)时,可以使用底层的自定义绘制能力。
实现方式:ArkUI 提供了基于 NDK 的自定义绘制节点能力。开发者可以创建ARKUI_NODE_CUSTOM类型的节点,并注册自定义绘制事件(如内容背景层、内容层、前景层等)。在回调函数中获取 Canvas 画布指针,使用 C/C++ 代码进行精细化的图形绘制,从而实现高度定制化的 UI 效果。
1、 创建自定义节点并注册绘制事件
在 C++ 侧,首先需要创建ARKUI_NODE_CUSTOM类型的节点,并为其注册内容层绘制事件(ARKUI_NODE_CUSTOM_EVENT_ON_DRAW)。
// NativeDrawPageSample.cpp #include <arkui/native_node.h> #include <arkui/native_type.h> #include <native_drawing/drawing_canvas.h> #include <native_drawing/drawing_path.h> #include <native_drawing/drawing_pen.h> #include <native_drawing/drawing_color.h> // 1. 创建自定义节点 auto customNode = nodeAPI->createNode(ARKUI_NODE_CUSTOM); // 2. 注册自定义绘制事件 // 将自定义节点、事件类型、事件ID和UserData作为参数传入 nodeAPI->registerNodeCustomEvent( customNode, ARKUI_NODE_CUSTOM_EVENT_ON_DRAW, // 注册内容层绘制事件 1, nullptr // UserData );2、 编写事件回调与 Canvas 绘制逻辑
在事件回调函数中,通过传入的event获取绘制上下文,将其转换为OH_Drawing_Canvas指针后,即可使用 Native Drawing API 进行图形绘制。
// 3. 编写事件回调函数 nodeAPI->registerNodeCustomEventReceiver([](ArkUI_NodeCustomEvent *event) { // 获取自定义事件绘制的上下文 auto *drawContext = OH_ArkUI_NodeCustomEvent_GetDrawContextInDraw(event); // 获取 Canvas 指针并转换为 OH_Drawing_Canvas auto *canvas1 = OH_ArkUI_DrawContext_GetCanvas(drawContext); OH_Drawing_Canvas *canvas = reinterpret_cast<OH_Drawing_Canvas *>(canvas1); // --- 以下为 Native Drawing 精细化绘制逻辑 --- int32_t width = 1000; int32_t height = 1000; // 创建路径并绘制一条对角线 auto path = OH_Drawing_PathCreate(); OH_Drawing_PathMoveTo(path, width / 4, height / 4); OH_Drawing_PathLineTo(path, width * 3 / 4, height * 3 / 4); OH_Drawing_PathClose(path); // 设置画笔属性 auto pen = OH_Drawing_PenCreate(); OH_Drawing_PenSetWidth(pen, 10); OH_Drawing_PenSetColor(pen, OH_Drawing_ColorSetArgb(0xFF, 0x00, 0x4A, 0x4F)); // 将画笔附加到画布并执行绘制 OH_Drawing_CanvasAttachPen(canvas, pen); OH_Drawing_CanvasDrawPath(canvas, path); });3、 进阶:使用 RenderNode 进行渲染节点树操作
除了基础的自定义绘制,从 API version 20 开始,ArkUI NDK 还支持直接构建渲染节点树(RenderNode)。这种方式可以绕过常规的测量和布局过程,直接绘制节点并调整其大小、位置和属性。
// 创建渲染节点及其子节点 auto renderRootNode = OH_ArkUI_RenderNodeUtils_CreateNode(); auto firstChildRenderNode = OH_ArkUI_RenderNodeUtils_CreateNode(); // 将渲染节点挂载到 ARKUI_NODE_CUSTOM 类型的自定义节点上 auto result = OH_ArkUI_RenderNodeUtils_AddRenderNode(customNode, renderRootNode); OH_ArkUI_RenderNodeUtils_AddChild(renderRootNode, firstChildRenderNode); // 直接设置渲染节点的大小、位置和背景颜色 OH_ArkUI_RenderNodeUtils_SetSize(renderRootNode, 500, 500); OH_ArkUI_RenderNodeUtils_SetPosition(renderRootNode, 300, 100); OH_ArkUI_RenderNodeUtils_SetBackgroundColor(firstChildRenderNode, 0xFFFF0000); // 红色五、 核心机制:Props 与 Events 的灵活通信
自定义组件的灵活性很大程度上依赖于属性定义与参数传递机制。通过合理设计组件属性,可以打造出高度可复用的通用组件。
- Props(属性传递):父组件向子组件传递的只读数据。对于必选属性(无默认值),外部使用时必须传入;对于可选属性,通过设置默认值增强灵活性。同时,支持基本数据类型、复杂对象及可选链操作符(
?)来避免空指针错误。 - Events(事件回调):子组件向父组件通信的机制。通过事件回调,子组件可以将用户的操作(如点击卡片、收藏按钮)通知给父组件,从而实现交互逻辑的解耦。
通过合理定义 Props 和 Events,可以实现父子组件间数据与交互的完美解耦。
// 子组件:定义属性与事件回调 @Component export struct ProductCard { // Props:接收父组件传递的数据,支持可选链操作符避免空指针 title: string = '默认商品'; price?: number; // 可选属性 // Events:定义事件回调,将用户的交互通知给父组件 onFavoriteClick?: (title: string) => void; build() { Column({ space: 10 }) { Text(this.title).fontSize(18) Text(`价格: ${this.price ?? '面议'}`).fontSize(14) Button('收藏') .onClick(() => { // 触发事件回调,将当前商品标题传给父组件处理 this.onFavoriteClick?.(this.title); }) } } } // 父组件:传递数据与监听事件 @Entry @Component struct ShoppingPage { build() { ProductCard({ title: '鸿蒙开发指南', price: 59.9, // 监听子组件的收藏事件 onFavoriteClick: (title) => { console.log(`用户收藏了: ${title}`); } }) } }六、 样式扩展:@Styles 与 @Extend 的差异化应用
除了AttributeModifier,ArkUI 还提供了@Styles和@Extend装饰器来封装通用样式:
- @Styles:支持在组件内部或全局定义,用于封装重复公用的属性。其弊端是只能写通用样式,不支持传参。
- @Extend:仅支持全局定义,但功能更强大。它支持封装指定组件的私有属性和事件,并且支持传参,开发者可以在调用时传递参数,调用遵循 TS 方法传值调用。
@Styles适合封装静态的通用样式,而@Extend适合封装特定组件的私有属性并支持动态传参。
// 1. @Styles 封装:仅支持通用属性,不支持传参 @Styles function commonCardStyle() { .padding(16) .borderRadius(12) .backgroundColor('#F5F5F5') } // 2. @Extend 封装:支持指定组件的私有属性,且支持传参 @Extend(Text) function dynamicTextStyle(fontSize: number, color: ResourceColor) { .fontSize(fontSize) .fontColor(color) .fontWeight(FontWeight.Bold) } @Entry @Component struct StylePage { build() { Column({ space: 20 }) { // 使用 @Styles Text('基础卡片文本') .commonCardStyle() // 使用 @Extend,动态传入字体大小和颜色 Text('高级定制文本') .dynamicTextStyle(24, '#E60012') } } }七、 架构扩展:HSP 动态共享包与组件导出
当业务组件库需要跨模块甚至跨应用复用时,推荐使用 HSP(Harmony Shared Package)动态共享包:
- 按需加载与体积控制:多个 HAP/HSP 共用的代码和资源放在同一个 HSP 中,可以提高代码的可重用性。HSP 在运行时按需加载,有助于提升应用性能并控制应用包大小。
- 组件与接口导出:在 HSP 中,可以通过
export关键字导出 ArkUI 组件、类和方法。使用方引入后,即可在工程中像使用本地组件一样调用共享包中的自定义组件。
在 HSP 模块中,组件的导出与导入机制与常规模块一致,但 HSP 在运行时按需加载,能有效控制包体积。
// 【HSP 模块】common_hsp/src/main/ets/components/HspButton.ets // 提供方:使用 export 导出组件 @Component export struct HspButton { label: string = 'HSP按钮'; build() { Button(this.label) .backgroundColor('#007DFF') } } // 【HAP 业务模块】pages/Index.ets // 使用方:从 HSP 模块中引入并使用 import { HspButton } from '@ohos/common_hsp'; @Entry @Component struct Index { build() { Column() { HspButton({ label: '来自共享包的按钮' }) } } }八、 高级范式:@Builder 与自定义弹窗封装
对于复杂的 UI 结构(如自定义弹窗),推荐使用@Builder函数进行封装:
- 实现原理:提供方可以封装一个工具类,通过
UIContext获取promptAction对象。使用方将自定义弹窗结构的@Builder函数作为参数传入,结合ComponentContent定义弹窗内容,最终调用openCustomDialog实现自定义弹窗的展示。 - 开发范式优势:采用声明式开发范式构建 UI,开发者只需直观地描述“界面应该是什么样”,无需关心底层 UI 绘制和 DOM 管理。相比类 Web 开发范式,其渲染更新链路更为精简,占用内存更少,应用性能更佳。
使用@Builder封装复杂的 UI 结构,结合promptAction可以优雅地实现自定义弹窗。
// 1. 使用 @Builder 定义弹窗的 UI 结构 @Builder function customDialogBuilder() { Column({ space: 15 }) { Text('自定义弹窗标题').fontSize(20).fontWeight(FontWeight.Bold) Text('这是通过 @Builder 封装的弹窗内容,渲染链路更精简,性能更佳。') Button('我知道了') .onClick(() => { // 关闭弹窗 promptAction.closeCustomDialog(); }) } .padding(20) } @Entry @Component struct DialogPage { build() { Button('打开自定义弹窗') .onClick(() => { // 2. 调用 openCustomDialog 展示 @Builder 定义的内容 promptAction.openCustomDialog(customDialogBuilder(), { alignment: DialogAlignment.Center, offset: { dx: 0, dy: 0 } }); }) } }九、 性能优化:懒加载与渲染控制
在列表或复杂页面中,组件的复用必须考虑性能开销。
- LazyForEach 与数据懒加载:对于长列表中的组件复用,不应直接使用
ForEach遍历全量数据。应使用LazyForEach配合IDataSource,仅渲染屏幕可见区域的组件。当组件滑出屏幕时,系统会自动回收复用,极大降低内存占用。 - @BuilderParam 与条件渲染:在封装通用容器组件(如卡片)时,使用
@BuilderParam装饰器接收子组件结构。结合if/else或switch进行条件渲染时,确保逻辑判断轻量级,避免在build函数中执行复杂的计算逻辑。 - reuseId 机制:在
LazyForEach中,为组件指定唯一的reuseId。这有助于框架更精准地识别组件实例,在数据源变更时执行最小化的 UI 更新,而不是重建整个组件树。
十、 状态管理:跨组件与跨页面的状态同步
随着组件复用层级的加深,Prop 逐层传递(Prop Drilling)会导致代码难以维护。
- AppStorage 与 LocalStorage:对于全局共享的状态(如用户信息、主题配置),应使用
AppStorage进行管理。组件库中的组件可以通过@StorageLink或@StorageProp直接响应全局状态的变化,实现“一处修改,处处更新”。 - CustomEvent 与全局事件总线:对于非状态数据的通信(如通知刷新、埋点上报),可以封装基于
EventHub的全局事件总线。组件库内部触发事件,业务层订阅事件,实现完全解耦。
十一、 跨语言交互:ArkTS 与 C++ 的高效通信
当组件涉及高性能计算或图形处理(如图像处理滤镜组件、音视频编解码组件)时,单纯的 ArkTS 可能无法满足性能要求。
- NAPI 接口封装:在组件库底层,通过 NAPI(Native API)将 C++ 的核心算法封装为 ArkTS 可调用的接口。
- 内存管理:在使用 NDK 进行自定义绘制或数据处理时,需特别注意
ArrayBuffer与 C++ 指针之间的内存分配与释放,避免内存泄漏。建议使用ArrayBuffer的零拷贝特性传递大数据块。
十二、 工程化:HSP 与版本管理
在大型团队协作中,组件库的维护和发布是核心痛点。
- HSP 版本语义化:建立严格的语义化版本控制(SemVer)。对于 HSP 共享包,主版本号变更代表不兼容的 API 修改,次版本号变更代表向下兼容的功能性新增。
- 依赖隔离:在
oh-package.json5中明确声明组件库的依赖范围。避免组件库将业务方的依赖传递污染,确保组件库的纯净性和可移植性。 - 自动化发布流水线:配置 CI/CD 脚本,当组件库代码合并到主分支时,自动运行单元测试、构建 HSP 包并发布到私有仓库(如 HarmonyOS 的 ohpm 仓库)。