> 技术文档 > 深入探索鸿蒙自定义组件开发

深入探索鸿蒙自定义组件开发

引言
在鸿蒙(HarmonyOS)应用开发领域,UI 组件无疑是搭建用户界面的基石。鸿蒙系统慷慨地提供了一系列丰富的原生组件,诸如 Text、Button、Image 等,这些组件能够满足大部分常见的 UI 需求。然而,一旦涉足复杂的实际项目,开发者往往会遭遇诸多挑战。例如,在多个页面中频繁需要使用具有相同功能的 UI 模块,像是带图标的按钮、独具特色的自定义卡片或者结构复杂的表单控件等。倘若每次都选择重新编写代码来实现这些模块,不仅会极大地降低开发效率,而且后续的维护工作也将变得异常艰难,牵一发而动全身,极易出现遗漏或错误。
而自定义组件开发则成为化解这一困境的关键所在。通过将那些反复使用的 UI 逻辑与样式巧妙地封装成一个个独立的组件,开发者能够顺利达成代码复用、逻辑隔离以及维护便捷等多重目标。鸿蒙的 ArkUI 框架(基于 eTS/JS)全力支持自定义组件功能,并且提供了属性传递、事件回调、插槽(Slot)机制等一系列强大的高级特性,这使得开发者能够游刃有余地构建出高度可复用的 UI 模块,极大地提升了开发的灵活性与效率。
本文将引领大家深入剖析鸿蒙自定义组件的开发流程,并且结合多样化场景的代码示例,例如带图标的按钮、商品卡片、可配置表单控件等,帮助开发者全面掌握从基础到进阶的自定义组件设计技巧,在鸿蒙应用开发的道路上迈出坚实有力的步伐。
原生组件的局限性与自定义组件的核心价值
原生组件的局限性
鸿蒙系统提供的原生组件虽然在一定程度上能够满足常见的 UI 设计需求,但在面对复杂多变的业务场景时,其固有的局限性也逐渐显现出来。
1.功能单一性:以 Button 组件为例,它通常仅支持单纯地显示文本或者图标,在实际应用中,若开发者期望实现图文并排的复杂样式,原生的 Button 组件就显得力不从心,无法直接达成这一需求。
2.重复代码问题:当多个页面都需要运用相同样式的组件,比如带边框的卡片容器时,开发者不得不反复编写布局和样式代码。这种重复性的劳动不仅耗费大量时间和精力,还使得代码的冗余度大幅增加,降低了代码的可读性和可维护性。
3.维护困难:一旦业务需求发生变更,例如需要对按钮的样式进行调整,由于该按钮在多个页面中被广泛使用,开发者就必须在所有涉及该按钮的页面中逐一手动修改代码。这一过程不仅繁琐,而且极易因疏忽而遗漏某些页面,从而导致应用在不同页面上出现不一致的显示效果,严重影响用户体验。
自定义组件的核心价值
自定义组件通过封装与复用的理念,有效地解决了原生组件所面临的上述问题,为开发者带来了诸多显著的优势。
1.代码复用:开发者可以将具有通用性的 UI 逻辑,如 “带图标的按钮” 这一常见功能,精心封装成一个独立的组件。在后续的项目开发中,无论在哪个页面需要使用该功能,只需通过简单的引用操作,即可轻松实现复用,极大地提高了开发效率,避免了重复劳动。
2.逻辑隔离:自定义组件巧妙地将其内部的实现细节,包括样式计算、事件处理等复杂逻辑,对外进行隐藏。仅向外部暴露必要的属性和事件接口,这种做法有效地降低了不同模块之间的耦合度。当某个组件的内部逻辑需要进行修改或优化时,由于其与其他组件之间的低耦合性,不会对整个应用的其他部分产生过多的影响,从而使得代码的维护和升级变得更加容易和安全。
3.灵活配置:借助属性(Props)传递机制,自定义组件能够接收来自外部的动态参数,如图标名称、按钮文字等。同时,通过事件回调(Events)功能,组件可以及时通知父组件用户的操作行为,如按钮的点击事件。这种灵活的配置方式使得组件能够在不同的应用场景中根据实际需求进行个性化的定制,极大地增强了组件的通用性和适应性。
4.维护便捷:当某个自定义组件需要进行更新,比如优化样式或者修复逻辑错误时,开发者只需要专注于修改该组件的内部代码。由于组件的复用性,所有引用了该组件的页面会自动应用这些更新,无需在每个使用该组件的页面中逐一进行修改,大大减少了维护成本,提高了维护效率。
自定义组件开发详解
创建自定义组件的基本步骤
1.定义组件结构:在鸿蒙的 ArkTS 开发环境中,自定义组件是基于 struct 来实现的。通过 “struct + 自定义组件名 + {…}” 的组合形式来构建自定义组件,需要特别注意的是,自定义组件不能存在继承关系。同时,为了避免混淆,自定义组件名不能与系统组件名相同。例如:

struct MyCustomComponent { // 组件内部的成员变量和方法定义}

1.添加 @Component 装饰器:@Component 装饰器是赋予 struct 组件化能力的关键。这个装饰器只能用于修饰由 struct 关键字声明的数据结构。一旦 struct 被 @Component 装饰,它就具备了组件的特性,此时必须实现 build 方法,该方法用于详细描述组件的 UI 结构。需要强调的是,一个 struct 只能被一个 @Component 装饰。示例如下:

@Componentstruct MyCustomComponent { build() { // 在这里描述组件的UI结构 }}

1.实现 build () 函数:build () 函数在自定义组件中扮演着至关重要的角色,它承担着定义组件声明式 UI 描述的重任,是自定义组件不可或缺的部分。在这个函数中,开发者需要运用 ArkUI 框架提供的各种系统组件和布局方式,精心构建出组件的外观和结构。例如,创建一个简单的包含文本的自定义组件:

@Componentstruct MyCustomComponent { build() { return Text(\'这是一个自定义组件\').fontSize(16).color(Color.Black); }}

组件属性传递与事件处理
1.属性传递:在鸿蒙开发中,组件之间的数据传递主要依靠特定的装饰器来实现,这些装饰器就如同组件之间的通信桥梁,规范了数据在父子组件之间的流动方式。
◦@Prop 装饰器:@Prop 装饰器是最常用的用于接收父组件传递数据的方式。它的作用类似于函数的参数,子组件通过 @Prop 明确声明需要接收的数据类型和名称。例如,创建一个用户卡片组件,该组件需要接收用户名、头像和在线状态等信息:

@Componentstruct UserCard { @Prop username: string; @Prop avatar: string; @Prop online: boolean; build() { return Column() { Image(this.avatar).width(50).height(50); Text(this.username).fontSize(14); Text(this.online? \'在线\' : \'离线\').fontSize(12).color(this.online? Color.Green : Color.Red); }; }}

在父组件中使用该 UserCard 组件时,可以这样传递数据:

@Entry@Componentstruct ParentComponent { build() { return Column() { UserCard({ username: \'张三\', avatar: $r(\'app.media.avatar1\'), online: true }); }; }}

•@State 装饰器:@State 装饰器主要用于管理组件内部的状态数据。当组件内部的状态发生变化时,鸿蒙框架能够自动感知并重新渲染与该状态相关的 UI 部分,实现了数据与 UI 的实时同步更新。以一个计数器组件为例,展示如何使用 @State 管理内部状态:

@Componentstruct CounterComponent { @State count: number = 0; build() { return Column() { Text(`当前计数: ${this.count}`).fontSize(16); Button(\'增加计数\').onClick(() => { this.count++; }); }; }}

1.事件处理:自定义组件能够灵活地处理用户的交互事件,并通过事件回调将相关信息传递给父组件。例如,在上述的 UserCard 组件中,如果希望在用户点击卡片时执行某些操作,可以在组件中定义一个点击事件回调函数,并通过属性传递将该函数传递给父组件。

@Componentstruct UserCard { @Prop username: string; @Prop avatar: string; @Prop online: boolean; @Prop onClick: () => void; build() { return Column() { Image(this.avatar).width(50).height(50); Text(this.username).fontSize(14); Text(this.online? \'在线\' : \'离线\').fontSize(12).color(this.online? Color.Green : Color.Red); }.onClick(this.onClick); }}

在父组件中使用 UserCard 组件时,可以为其传递具体的点击事件处理逻辑:

@Entry@Componentstruct ParentComponent { handleCardClick() { console.log(\'用户卡片被点击了\'); } build() { return Column() { UserCard({ username: \'张三\', avatar: $r(\'app.media.avatar1\'), online: true, onClick: this.handleCardClick.bind(this) }); }; }}

插槽(Slot)机制的应用(扩展)
在一些较为复杂的高级场景中,插槽(Slot)机制能够发挥重要作用。通过 @Slot 装饰器定义内容插槽,允许父组件向子组件内部特定位置插入自定义的 UI 内容。例如,创建一个商品卡片组件,卡片底部需要根据不同的业务需求展示不同的按钮或其他 UI 元素。

@Componentstruct ProductCard { @Prop productName: string; @Prop price: number; @Slot content: () => void; build() { return Column() { Text(this.productName).fontSize(16).fontWeight(FontWeight.Bold); Text(`价格: ${this.price}`).fontSize(14); this.content(); }; }}

在父组件中使用 ProductCard 组件时,可以向其插槽中插入自定义的 UI 内容:

@Entry@Componentstruct ParentComponent { build() { return Column() { ProductCard({ productName: \'华为手机\', price: 3999, content: () => {  return Button(\'立即购买\').width(100).height(40).backgroundColor(Color.Blue); } }); }; }}

自定义组件使用示例
场景一:带图标的功能按钮
1.需求分析:在多个页面中,经常会需要使用到 “图标 + 文字” 形式的按钮,并且根据不同的功能和设计要求,图标和文字的间距、颜色等样式需要能够灵活配置。例如,在设置页面中,“设置” 按钮的图标可能是一个齿轮,文字颜色为白色;在搜索页面中,“搜索” 按钮的图标为放大镜,文字颜色为黑色。
2.实现步骤:
◦定义组件:创建一个自定义按钮组件,通过 @Prop 装饰器接收图标名称、文字内容、图标和文字的颜色以及间距等参数。

@Componentstruct IconTextButton { @Prop icon: string; @Prop text: string; @Prop iconColor: Color; @Prop textColor: Color; @Prop spacing: number; build() { return Row() { Image($r(`app.media.${this.icon}`)).width(20).height(20).margin({ right: this.spacing }).color(this.iconColor); Text(this.text).fontSize(14).color(this.textColor); }; }}

•在父组件中使用:在父组件的页面中,根据不同的需求创建多个 IconTextButton 组件实例,并传入相应的参数。

@Entry@Componentstruct HomePage { build() { return Column() { IconTextButton({ icon: \'gear\', text: \'设置\', iconColor: Color.White, textColor: Color.White, spacing: 5 }).backgroundColor(Color.Blue).width(100).height(40).margin({ top: 20 }); IconTextButton({ icon:\'magnifier\', text: \'搜索\', iconColor: Color.Black, textColor: Color.Black, spacing: 5 }).backgroundColor(Color.White).width(100).height(40).margin({ top: 10 }); }; }}

场景二:商品信息卡片
1.需求分析:在电商应用的商品列表页面中,需要展示商品卡片,卡片中应包含商品图片、标题、价格、标签等信息。同时,为了满足不同的视觉设计和用户体验需求,卡片的边框圆角、背景色、标签样式等都需要能够进行自定义设置。
2.实现步骤:
◦定义组件:创建一个商品卡片自定义组件,通过 @Prop 装饰器接收商品标题、价格、图片 URL、标签数组以及卡片的背景色、边框圆角等参数。

@Componentstruct ProductCard { @Prop title: string; @Prop price: number; @Prop imageUrl: string; @Prop tags: string[]; @Prop bgColor: Color; @Prop borderRadius: number; build() { return Column() { if (this.imageUrl) { Image(this.imageUrl).width(200).height(200).objectFit(ImageFit.Contain); } Text(this.title).fontSize(16).fontWeight(FontWeight.Bold).margin({ top: 10 }); Text(`价格: ${this.price}`).fontSize(14).margin({ top: 5 }); if (this.tags.length > 0) { Row() {  ForEach(this.tags, (tag) => { Text(tag).fontSize(12).backgroundColor(Color.Gray).padding(5).margin({ right: 5 }).borderRadius(3);  }) }.margin({ top: 10 }); } }.backgroundColor(this.bgColor).borderRadius(this.borderRadius).padding(15).shadow({ color: Color.Black, offsetX: 2, offsetY: 2, blurRadius: 4 }); }}

•在父组件中使用:在商品列表页面的父组件中,创建多个 ProductCard 组件实例,并传入不同的商品数据和样式参数。

@Entry@Componentstruct ProductListPage { build() { return Column() { ProductCard({ title: \'耳机\', price: 199, imageUrl: $r(\'app.media.headphone\'), tags: [\'热销\', \'包邮\'], bgColor: Color.White, borderRadius: 16 }).margin({ top: 20 }); ProductCard({ title: \'手表\', price: 999, imageUrl: $r(\'app.media.watch\'), tags: [\'新品\'], bgColor: Color.LightGray, borderRadius: 8 }).margin({ top: 20 }); }; }}

场景三:可配置的表单控件
1.需求分析:在用户注册页面等涉及表单输入的场景中,往往需要多个不同类型的输入框,如用户名输入框、密码输入框等。并且,这些输入框可能需要在前端展示一些特定的提示信息,例如输入框的前缀图标(如用户头像图标用于用户名输入框,锁图标用于密码输入框)、占位符文字以及根据用户输入的验证状态(正常 / 错误)来动态改变输入框的样式。
2.实现步骤:
◦定义组件:创建一个可配置的表单输入框自定义组件,通过 @Prop 装饰器接收占位符、前缀图标、密码模式(用于判断是否为密码输入框)、验证状态等参数。同时,组件内部使用 @State 装饰器维护输入框的当前值,并通过 onChange 事件监听用户的输入操作,实时更新输入框的值。

@Componentstruct CustomInput { @Prop placeholder: string; @Prop prefixIcon: string; @Prop isPassword: boolean; @Prop validationState: \'normal\' | \'error\'; @State inputValue: string = \'\'; build() { return Row() { if (this.prefixIcon) { Image($r(`app.media.${this.prefixIcon}`)).width(20).height(20).margin({ right: 5 }); } if (this.isPassword) { SecureInput(this.inputValue).placeholder(this.placeholder).onChange((value) => {  this.inputValue = value; }).width(2

https://developer.huawei.com/consumer/cn/training/classDetail/13f68a5f423e497d8ced35beabe05b1e?type=1?ha_source=hmosclass&ha_sourceId=89000248