当前位置: 首页 > news >正文

HarmonyOS7 RenderSlot 为什么越用越香?可插拔组件设计一次讲明白

文章目录

    • 前言
    • @BuilderParam 基础
    • 默认内容:插槽不传就用兜底
    • 多插槽:PageLayout 实战
    • 实际使用 PageLayout
    • 作用域插槽:子组件给父组件传数据
    • 依赖反转:让组件只关心骨架
    • 写在最后

前言

写组件库的时候经常遇到这种场景:一个 PageLayout 组件,有的页面顶部放搜索栏,有的放导航按钮,有的什么都不放。内容区域更是千变万化,底部可能有 TabBar 也可能没有。如果把所有可能性都写成参数,这个组件得有三四十个属性,谁用谁崩溃。

ArkUI 里的@BuilderParam就是为了解决这个问题。它允许父组件往子组件里塞自定义的 UI 内容,效果上跟 Vue 的 slot 或者 React 的 children 差不多。但用法上有些自己的特点,用好了能大幅提升组件的灵活性。

@BuilderParam 基础

先搞清楚@BuilderParam的工作方式。子组件声明一个@BuilderParam属性,父组件使用的时候传一个@Builder函数进去:

@Componentstruct Card{@BuilderParamcontent:()=>voidbuild(){Column(){this.content()}.padding(16).borderRadius(12).backgroundColor('#FFFFFF').shadow({radius:4,color:'rgba(0,0,0,0.1)',offsetY:2})}}

父组件使用:

@Componentstruct HomePage{@BuilderuserCard(){Row(){Image($r('app.media.avatar')).width(48).height(48)Text('张三').fontSize(18).margin({left:12})}}build(){Card(){content:this.userCard}}}

这就是最简单的插槽模式。父组件控制塞什么内容,子组件只负责容器样式和布局。

默认内容:插槽不传就用兜底

很多时候希望插槽有个默认内容,不传的时候也能正常显示。给@BuilderParam一个默认值就行:

@Componentstruct AlertBanner{@BuilderParamcontent:()=>void=this.defaultContent type:'info'|'warning'|'error'='info'@BuilderdefaultContent(){Text('这是一条提示信息').fontSize(14).fontColor('#666')}build(){Row(){Image(this.getIcon()).width(20).height(20).margin({right:8})this.content()}.width('100%').padding(12).borderRadius(8).backgroundColor(this.getBgColor())}privategetIcon():Resource{consticonMap:Record<string,Resource>={info:$r('app.media.ic_info'),warning:$r('app.media.ic_warning'),error:$r('app.media.ic_error'),}returniconMap[this.type]}privategetBgColor():string{constcolorMap:Record<string,string>={info:'#E3F2FD',warning:'#FFF3E0',error:'#FFEBEE',}returncolorMap[this.type]}}

不传content的时候显示默认文案,传了就覆盖掉。这个模式在组件库里特别常见。

多插槽:PageLayout 实战

单个插槽解决不了复杂布局。实际项目里一个页面通常有 header、content、footer,有时候还有个浮动按钮。这就需要多个插槽:

@Componentexportstruct PageLayout{@BuilderParamheader:()=>void=this.emptyBuilder@BuilderParamcontent:()=>void=this.emptyBuilder@BuilderParamfooter:()=>void=this.emptyBuilder@BuilderParamfloating:()=>void=this.emptyBuilder@StateshowHeader:boolean=true@StateshowFooter:boolean=truepaddingValue:Edges={top:0,bottom:0,left:16,right:16}@BuilderemptyBuilder(){// 空占位,不渲染任何内容}build(){Stack({alignContent:Alignment.BottomEnd}){Column(){// Header 区域if(this.showHeader){Column(){this.header()}.width('100%')}// Content 区域,自动填充剩余空间Column(){this.content()}.layoutWeight(1).width('100%').padding(this.paddingValue)// Footer 区域if(this.showFooter){Column(){this.footer()}.width('100%')}}.width('100%').height('100%')// 浮动层,覆盖在内容上方Column(){this.floating()}.margin({right:20,bottom:20})}.width('100%').height('100%')}}

这里用了四个插槽,每个都有默认的空实现。showHeadershowFooter控制是否显示对应区域,给外部留了开关。浮动按钮放在 Stack 里,不影响主布局流。

实际使用 PageLayout

看一个完整的页面组装:

@Componentstruct OrderPage{@Stateorders:OrderItem[]=[]@BuilderpageHeader(){Row(){Image($r('app.media.ic_back')).width(24).height(24)Text('我的订单').fontSize(18).fontWeight(FontWeight.Bold).layoutWeight(1)Image($r('app.media.ic_search')).width(24).height(24)}.width('100%').padding({left:16,right:16,top:12,bottom:12}).backgroundColor('#FFFFFF')}@BuilderorderList(){List(){ForEach(this.orders,(order:OrderItem)=>{ListItem(){OrderCard({order:order})}})}.width('100%').divider({strokeWidth:8,color:'#F0F0F0'})}@BuilderbottomBar(){Row(){Text('共 3 个订单').fontSize(14).fontColor('#999')Blank()Button('全部已读').fontSize(14).backgroundColor('#4CAF50')}.width('100%').padding(12).backgroundColor('#FFFFFF')}@BuilderfabButton(){Button({type:ButtonType.Circle}){Image($r('app.media.ic_add')).width(24).height(24)}.width(56).height(56).backgroundColor('#2196F3').shadow({radius:8,color:'rgba(33,150,243,0.4)',offsetY:4})}build(){PageLayout(){header:this.pageHeader,content:this.orderList,footer:this.bottomBar,floating:this.fabButton}}}

一个完整的订单页面就这么拼出来了。header 放导航栏,content 放订单列表,footer 放底部操作栏,浮动按钮用于新建订单。每个部分的逻辑都在页面组件里,PageLayout 只管布局。

作用域插槽:子组件给父组件传数据

有时候父组件需要拿到子组件内部的数据来渲染。比如列表组件内部维护了选中状态,父组件需要根据选中状态渲染不同的内容。

ArkUI 没有像 Vue 那样直接的 scoped slot 语法,但可以通过回调函数模拟:

@Componentstruct TabContainer{@StateactiveTab:number=0tabs:string[]=['推荐','热门','最新']// 用函数类型实现作用域插槽:子组件把当前 tab 索引传给父组件@BuilderParamtabContent:(index:number,name:string)=>voidbuild(){Column(){// Tab 栏Row(){ForEach(this.tabs,(tab:string,index:number)=>{Text(tab).fontSize(16).fontWeight(this.activeTab===index?FontWeight.Bold:FontWeight.Normal).fontColor(this.activeTab===index?'#2196F3':'#666').padding({left:16,right:16,top:12,bottom:12}).onClick(()=>{this.activeTab=index})})}.width('100%')// 内容区域,把当前 tab 信息传出去Column(){this.tabContent(this.activeTab,this.tabs[this.activeTab])}.layoutWeight(1).width('100%')}}}

父组件拿到 tab 索引后自己决定渲染什么:

@Componentstruct HomePage{@BuilderrenderTab(index:number,name:string){if(index===0){RecommendList()}elseif(index===1){HotList()}else{LatestList()}}build(){TabContainer({tabs:['推荐','热门','最新'],tabContent:this.renderTab})}}

依赖反转:让组件只关心骨架

这种插槽模式本质上是一种依赖反转。PageLayout 不知道 header 长什么样,不知道 content 里放了什么,它只提供一个布局骨架。具体的渲染逻辑全部由使用方注入。

好处很明显——组件的复用性上去了。同一个 PageLayout,首页能用,详情页能用,设置页也能用。改一个布局问题,所有页面同步修复。

坏处是代码分散了。出了问题你得在组件和使用方之间来回跳,调试路径变长。我的经验是,在组件里加好日志,把插槽的渲染异常 catch 住,别让一个插槽的崩溃拖垮整个页面。

写在最后

@BuilderParam 是 ArkUI 里最被低估的特性之一。很多人写组件只想到用 @Prop 传数据,其实传渲染逻辑比传数据灵活得多。把"长什么样"的决策权交给使用方,组件只做好"放在哪里"的事,这才是组件库该有的分工。

不过也别过度使用。如果一个组件有七八个插槽,用起来反而比直接写页面还麻烦,那说明这个组件的粒度不对,应该拆开重新设计。

http://www.jsqmd.com/news/1096519/

相关文章:

  • COMSOL后处理实战:精准提取动态接触面积
  • 算法:删除有序数组的重复项
  • Web身份验证漏洞攻防实战:从暴力破解到MFA绕过的全面防御指南
  • 从CT灰度到力学模型:Mimics中股骨多材料属性赋予的完整实践
  • STM32F407ZET6 SysTick延时:从寄存器配置到传感器精准触发的实战解析
  • 抖音直播录制神器:3步快速部署40+平台自动录制完整指南
  • VMware运维工具箱:从RVTools到PowerCLI的实战利器盘点
  • TinyML 推理引擎:从模型量化到 MCU 级部署的极致内存优化
  • 你玩的游戏,可能正在帮外国军队扫描你的国家
  • 【万字文档+源码】基于springboot+vue茶叶商城管理系统-可用于毕设-课程设计-练手学习-学习资料分享
  • Delphi 实战:从阻塞到流式,解锁OpenAI API异步调用与实时响应
  • 英雄联盟Akari助手:3分钟快速上手的游戏效率工具终极指南
  • 一行命令让 AI Agent 看遍全网:Agent-Reach 全平台数据源扩展实战
  • 从 1 台到 10 台:无人售货柜的规模化复制
  • Windows 11 系统盘越用越小怎么办?存储感知 DISM Compact OS 等专属工具详解
  • 论文AI写作软件推荐哪个好?2026年度榜单
  • WWW 2024 | 图嵌入新范式:从LINE到大规模动态网络的表示学习
  • 在Java中,如何使用break和continue关键字来控制循环?
  • 记录redis学习
  • 别再硬编码密钥了!Spring Boot项目实战:用配置文件安全管理AES256加解密密钥
  • 大模型 AGI 开发模式:从概念到落地的系统性技术解构
  • STC16F40K128单片机驱动4路红外循迹模块实战指南
  • HarmonyOS7 泛型组件怎么写才不废?TypeScript 类型安全通用列表实战
  • 终极指南:如何用Python免费下载B站大会员4K高清视频
  • 网络基础入门与实战操作指南
  • 终极指南:如何用MPC-HC打造专业级Windows媒体播放体验 [特殊字符]
  • 一键下载中小学电子课本:国家中小学智慧教育平台PDF下载工具完全指南
  • 海量简历筛选太痛苦?实测AI智能体批量归档黑科技,猎头效能提升10倍
  • 解锁B站缓存视频:m4s-converter工具完整使用指南
  • 同步与异步通信:从概念到实战,如何为你的系统选择最佳通信模式?