零基础学 ArkUI 自定义组件专题六从 Builder 到构建自己的组件库博主说当你的 App 需要 10 个长得一样的输入框、20 个风格统一的卡片、5 个不同颜色的按钮时——「复制粘贴改颜色」就成了效率杀手。自定义组件就是解决这个问题的终极方案。今天一篇讲清楚 ArkUI 组件复用的全部技巧。 4 种复用层级的演进Builder 函数 → 复用 UI 片段 BuilderParam 插槽 → 让调用方自定义片段内部 自定义 Component → 完整的可复用组件单元 组件库多文件 → 项目级/企业级组件资产⚙️ 运行环境项目要求DevEco Studio5.0.3.800HarmonyOS SDKAPI 12️ 从 Builder 到组件库 — 4 个演进阶段 阶段 1Builder — UI 片段复用场景多个列表项有相同的卡片样式。EntryComponentstruct Demo_Builder{Stateitems:{title:string;desc:string}[][{title:标题1,desc:描述文字1},{title:标题2,desc:描述文字2},{title:标题3,desc:描述文字3},];// Builder 和 BuilderParam 相关功能封装可复用的 UI 片段BuilderCard(title:string,desc:string){Column(){Text(title).fontSize(18).fontWeight(FontWeight.Bold)Text(desc).fontSize(14).fontColor(#888).margin({top:4})}.width(90%).padding(16).backgroundColor(#FFF).borderRadius(12).shadow({radius:4,color:#20000000,offsetY:2})}build(){Column(){Text(Builder 复用的卡片列表).fontSize(20).fontWeight(FontWeight.Bold).margin(16)ForEach(this.items,(item){this.Card(item.title,item.desc)// 调用 Builder})}.width(100%)}} 阶段 2BuilderParam — 插槽模式自定义内容场景卡片框架固定但中间的内容区域由调用方决定。Componentstruct CardFrame{// BuilderParam 定义插槽BuilderParamcontent:()voidthis.defaultContent;BuilderdefaultContent(){Text(默认内容).fontSize(14).fontColor(#999)}build(){Column(){Text(卡片标题).fontSize(18).fontWeight(FontWeight.Bold)Divider().margin({top:8,bottom:8})this.content()// 渲染插槽}.width(90%).padding(16).backgroundColor(#FFF).borderRadius(12).shadow({radius:4,color:#20000000,offsetY:2})}}EntryComponentstruct Demo_BuilderParam{BuildercustomContent(){Row(){Circle().width(40).height(40).fill(#007AFF)Column(){Text(自定义内容).fontSize(16).fontWeight(FontWeight.Bold)Text(由调用方传入).fontSize(13).fontColor(#888)}.margin({left:12})}}build(){Column(){Text(BuilderParam 插槽示例).fontSize(20).fontWeight(FontWeight.Bold).margin(16)// 使用默认内容CardFrame()// 传入自定义内容CardFrame({content:this.customContent})}.width(100%)}} 阶段 3自定义 Component — 完整组件单元场景一个带标签、必填校验、错误提示的完整输入框组件。// 1. 定义组件的对外接口Componentstruct FormInput{// 对外暴露的属性privatelabel:string;privateplaceholder:string;privaterequired:booleanfalse;// 对外暴露的状态调用方能读取Linkvalue:string;// 对内状态StatehasError:booleanfalse;StateerrorMsg:string;// 对外暴露的方法validate():boolean{if(this.required!this.value.trim()){this.hasErrortrue;this.errorMsg${this.label}不能为空;returnfalse;}this.hasErrorfalse;this.errorMsg;returntrue;}build(){Column(){Row(){Text(this.label).fontSize(15).fontWeight(FontWeight.Bold)if(this.required){Text( *).fontSize(15).fontColor(#FF3B30)// 必填标识}}.width(100%)TextInput({placeholder:this.placeholder,text:this.value}).width(100%).height(44).backgroundColor(this.hasError?#FFF0F0:#F8F8F8).borderRadius(8).borderColor(this.hasError?#FF3B30:transparent).borderWidth(1).padding({left:12}).onChange((val:string){this.valueval;if(this.hasError){this.validate();// 重新校验}})if(this.hasError){Text(this.errorMsg).fontSize(12).fontColor(#FF3B30).width(100%).margin({top:4})}}.width(100%)}}// 2. 使用自定义组件EntryComponentstruct Demo_CustomComponent{Stateusername:string;Statepassword:string;Stateemail:string;build(){Column(){Text(自定义 FormInput 组件).fontSize(20).fontWeight(FontWeight.Bold).margin(16)FormInput({label:用户名,placeholder:请输入用户名,required:true,value:this.username}).margin({bottom:16})FormInput({label:密码,placeholder:请输入密码,required:true,value:this.password}).margin({bottom:16})FormInput({label:邮箱,placeholder:选填,required:false,value:this.email}).margin({bottom:24})Button(提交).width(100%).height(48).backgroundColor(#007AFF).fontColor(#fff).borderRadius(8).onClick((){// 调用自定义组件的 validate 方法需要 ref// 这里简化处理if(!this.username.trim()||!this.password.trim()){AlertDialog.show({message:请填写必填项});}else{AlertDialog.show({message:提交成功});}})}.width(90%).padding(16)}} 阶段 4组件库 — 企业级复用目录结构components/ ├── FormInput.ets ← 带校验的输入框 ├── CardLayout.ets ← 卡片容器 ├── LoadingSpinner.ets ← 加载动画 ├── EmptyState.ets ← 空状态占位 ├── ConfirmDialog.ets ← 确认弹窗 ├── Badge.ets ← 角标 ├── index.ets ← 统一导出 └── README.md ← 使用文档// components/index.ets — 统一导出export{FormInput}from./FormInput;export{CardLayout}from./CardLayout;export{LoadingSpinner}from./LoadingSpinner;export{EmptyState}from./EmptyState;export{ConfirmDialog}from./ConfirmDialog;// 使用时只需一行导入// import { FormInput, CardLayout } from ../components; 组件设计规范设计原则说明示例单一职责一个组件只做一件事FormInput 只管输入校验属性驱动通过属性控制行为required: boolean状态提升数据由父组件管理用Link而非State插槽扩展BuilderParam 让调用方自定义内容CardFrame 的 content 插槽默认值所有属性都有合理默认值required: false类型安全用 interface 定义属性类型interface FormInputProps⚠️ 避坑指南坑原因正确做法Builder 中不能用this.xxx调其他 Builder作用域限制在 Component 外定义纯函数 Builder自定义组件属性变了但 UI 不刷新忘了加Prop/Link/State根据数据流向选对装饰器BuilderParam 不渲染忘了在 build 中调用this.content()必须显式调用组件太臃肿一个文件塞了太多逻辑拆分成多个小组件循环引用组件 A 引用组件 BB 引用 A用export解耦避免交叉引用 最佳实践命名规范组件文件名大写驼峰FormInput.ets单一职责每个组件只做一件事功能复杂就拆分默认值所有属性都给默认值让调用方最省事文档注释每个属性和方法都要写 JSDoc 注释Demo 页面开发时配套 Demo 页面展示所有使用方式版本管理组件库用独立版本号通过 oh-package.json5 发布 扩展挑战主题系统用Styles定义主题变量组件自动响应主题切换单元测试用ohos/hypium为每个组件写单元测试Storybook搭建组件预览页面像 Storybook 一样展示所有状态按需加载通过动态import()实现组件懒加载官方文档HarmonyOS 应用开发文档开发者社区华为开发者论坛欢迎加入开源鸿蒙跨平台社区https://openharmonycrossplatform.csdn.net/
零基础学 ArkUI 自定义组件(专题六):从 @Builder 到构建自己的组件库
发布时间:2026/6/10 0:33:11
零基础学 ArkUI 自定义组件专题六从 Builder 到构建自己的组件库博主说当你的 App 需要 10 个长得一样的输入框、20 个风格统一的卡片、5 个不同颜色的按钮时——「复制粘贴改颜色」就成了效率杀手。自定义组件就是解决这个问题的终极方案。今天一篇讲清楚 ArkUI 组件复用的全部技巧。 4 种复用层级的演进Builder 函数 → 复用 UI 片段 BuilderParam 插槽 → 让调用方自定义片段内部 自定义 Component → 完整的可复用组件单元 组件库多文件 → 项目级/企业级组件资产⚙️ 运行环境项目要求DevEco Studio5.0.3.800HarmonyOS SDKAPI 12️ 从 Builder 到组件库 — 4 个演进阶段 阶段 1Builder — UI 片段复用场景多个列表项有相同的卡片样式。EntryComponentstruct Demo_Builder{Stateitems:{title:string;desc:string}[][{title:标题1,desc:描述文字1},{title:标题2,desc:描述文字2},{title:标题3,desc:描述文字3},];// Builder 和 BuilderParam 相关功能封装可复用的 UI 片段BuilderCard(title:string,desc:string){Column(){Text(title).fontSize(18).fontWeight(FontWeight.Bold)Text(desc).fontSize(14).fontColor(#888).margin({top:4})}.width(90%).padding(16).backgroundColor(#FFF).borderRadius(12).shadow({radius:4,color:#20000000,offsetY:2})}build(){Column(){Text(Builder 复用的卡片列表).fontSize(20).fontWeight(FontWeight.Bold).margin(16)ForEach(this.items,(item){this.Card(item.title,item.desc)// 调用 Builder})}.width(100%)}} 阶段 2BuilderParam — 插槽模式自定义内容场景卡片框架固定但中间的内容区域由调用方决定。Componentstruct CardFrame{// BuilderParam 定义插槽BuilderParamcontent:()voidthis.defaultContent;BuilderdefaultContent(){Text(默认内容).fontSize(14).fontColor(#999)}build(){Column(){Text(卡片标题).fontSize(18).fontWeight(FontWeight.Bold)Divider().margin({top:8,bottom:8})this.content()// 渲染插槽}.width(90%).padding(16).backgroundColor(#FFF).borderRadius(12).shadow({radius:4,color:#20000000,offsetY:2})}}EntryComponentstruct Demo_BuilderParam{BuildercustomContent(){Row(){Circle().width(40).height(40).fill(#007AFF)Column(){Text(自定义内容).fontSize(16).fontWeight(FontWeight.Bold)Text(由调用方传入).fontSize(13).fontColor(#888)}.margin({left:12})}}build(){Column(){Text(BuilderParam 插槽示例).fontSize(20).fontWeight(FontWeight.Bold).margin(16)// 使用默认内容CardFrame()// 传入自定义内容CardFrame({content:this.customContent})}.width(100%)}} 阶段 3自定义 Component — 完整组件单元场景一个带标签、必填校验、错误提示的完整输入框组件。// 1. 定义组件的对外接口Componentstruct FormInput{// 对外暴露的属性privatelabel:string;privateplaceholder:string;privaterequired:booleanfalse;// 对外暴露的状态调用方能读取Linkvalue:string;// 对内状态StatehasError:booleanfalse;StateerrorMsg:string;// 对外暴露的方法validate():boolean{if(this.required!this.value.trim()){this.hasErrortrue;this.errorMsg${this.label}不能为空;returnfalse;}this.hasErrorfalse;this.errorMsg;returntrue;}build(){Column(){Row(){Text(this.label).fontSize(15).fontWeight(FontWeight.Bold)if(this.required){Text( *).fontSize(15).fontColor(#FF3B30)// 必填标识}}.width(100%)TextInput({placeholder:this.placeholder,text:this.value}).width(100%).height(44).backgroundColor(this.hasError?#FFF0F0:#F8F8F8).borderRadius(8).borderColor(this.hasError?#FF3B30:transparent).borderWidth(1).padding({left:12}).onChange((val:string){this.valueval;if(this.hasError){this.validate();// 重新校验}})if(this.hasError){Text(this.errorMsg).fontSize(12).fontColor(#FF3B30).width(100%).margin({top:4})}}.width(100%)}}// 2. 使用自定义组件EntryComponentstruct Demo_CustomComponent{Stateusername:string;Statepassword:string;Stateemail:string;build(){Column(){Text(自定义 FormInput 组件).fontSize(20).fontWeight(FontWeight.Bold).margin(16)FormInput({label:用户名,placeholder:请输入用户名,required:true,value:this.username}).margin({bottom:16})FormInput({label:密码,placeholder:请输入密码,required:true,value:this.password}).margin({bottom:16})FormInput({label:邮箱,placeholder:选填,required:false,value:this.email}).margin({bottom:24})Button(提交).width(100%).height(48).backgroundColor(#007AFF).fontColor(#fff).borderRadius(8).onClick((){// 调用自定义组件的 validate 方法需要 ref// 这里简化处理if(!this.username.trim()||!this.password.trim()){AlertDialog.show({message:请填写必填项});}else{AlertDialog.show({message:提交成功});}})}.width(90%).padding(16)}} 阶段 4组件库 — 企业级复用目录结构components/ ├── FormInput.ets ← 带校验的输入框 ├── CardLayout.ets ← 卡片容器 ├── LoadingSpinner.ets ← 加载动画 ├── EmptyState.ets ← 空状态占位 ├── ConfirmDialog.ets ← 确认弹窗 ├── Badge.ets ← 角标 ├── index.ets ← 统一导出 └── README.md ← 使用文档// components/index.ets — 统一导出export{FormInput}from./FormInput;export{CardLayout}from./CardLayout;export{LoadingSpinner}from./LoadingSpinner;export{EmptyState}from./EmptyState;export{ConfirmDialog}from./ConfirmDialog;// 使用时只需一行导入// import { FormInput, CardLayout } from ../components; 组件设计规范设计原则说明示例单一职责一个组件只做一件事FormInput 只管输入校验属性驱动通过属性控制行为required: boolean状态提升数据由父组件管理用Link而非State插槽扩展BuilderParam 让调用方自定义内容CardFrame 的 content 插槽默认值所有属性都有合理默认值required: false类型安全用 interface 定义属性类型interface FormInputProps⚠️ 避坑指南坑原因正确做法Builder 中不能用this.xxx调其他 Builder作用域限制在 Component 外定义纯函数 Builder自定义组件属性变了但 UI 不刷新忘了加Prop/Link/State根据数据流向选对装饰器BuilderParam 不渲染忘了在 build 中调用this.content()必须显式调用组件太臃肿一个文件塞了太多逻辑拆分成多个小组件循环引用组件 A 引用组件 BB 引用 A用export解耦避免交叉引用 最佳实践命名规范组件文件名大写驼峰FormInput.ets单一职责每个组件只做一件事功能复杂就拆分默认值所有属性都给默认值让调用方最省事文档注释每个属性和方法都要写 JSDoc 注释Demo 页面开发时配套 Demo 页面展示所有使用方式版本管理组件库用独立版本号通过 oh-package.json5 发布 扩展挑战主题系统用Styles定义主题变量组件自动响应主题切换单元测试用ohos/hypium为每个组件写单元测试Storybook搭建组件预览页面像 Storybook 一样展示所有状态按需加载通过动态import()实现组件懒加载官方文档HarmonyOS 应用开发文档开发者社区华为开发者论坛欢迎加入开源鸿蒙跨平台社区https://openharmonycrossplatform.csdn.net/