公司动态

自定义组件封装:构建高复用性的业务组件库(24)

📅 2026/7/24 21:54:12
自定义组件封装:构建高复用性的业务组件库(24)
在 HarmonyOS 的 ArkUI 应用开发中构建高复用性的业务组件库是提升开发效率、降低代码冗余以及保障应用一致性的核心手段。基于官方最佳实践组件的封装与复用主要可以通过以下三个层级来实现一、 通用组件样式封装AttributeModifier适用场景当应用中多个不同页面需要共享相同的 UI 样式时例如登录页面的登录按钮和购物页面的结账按钮具有相同的颜色、圆角和尺寸推荐使用AttributeModifier提取公共样式。实现方式提供方创建一个实现系统AttributeModifier接口的自定义类在其中封装公共属性如宽高、字体颜色、背景色等。使用方创建该 Modifier 的实例并将其作为参数传递给系统组件的.attributeModifier()方法。注意AttributeModifier仅适用于系统组件无法直接修改自定义组件的属性且支持跨文件复用。1、 提供方定义公共样式修饰器独立文件在实际工程中通常将AttributeModifier的实现类抽取到公共文件中。通过构造函数传参可以实现样式的动态定制。// common/CommonButtonModifier.ets import { AttributeModifier, ButtonAttribute } from kit.ArkUI; export class CommonButtonModifier implements AttributeModifierButtonAttribute { // 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?: AttributeModifierTextAttribute; 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_castOH_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 的差异化应用除了AttributeModifierArkUI 还提供了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 动态共享包与组件导出当业务组件库需要跨模块甚至跨应用复用时推荐使用 HSPHarmony 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 接口封装在组件库底层通过 NAPINative API将 C 的核心算法封装为 ArkTS 可调用的接口。内存管理在使用 NDK 进行自定义绘制或数据处理时需特别注意ArrayBuffer与 C 指针之间的内存分配与释放避免内存泄漏。建议使用ArrayBuffer的零拷贝特性传递大数据块。十二、 工程化HSP 与版本管理在大型团队协作中组件库的维护和发布是核心痛点。HSP 版本语义化建立严格的语义化版本控制SemVer。对于 HSP 共享包主版本号变更代表不兼容的 API 修改次版本号变更代表向下兼容的功能性新增。依赖隔离在oh-package.json5中明确声明组件库的依赖范围。避免组件库将业务方的依赖传递污染确保组件库的纯净性和可移植性。自动化发布流水线配置 CI/CD 脚本当组件库代码合并到主分支时自动运行单元测试、构建 HSP 包并发布到私有仓库如 HarmonyOS 的 ohpm 仓库。