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:()voidthis.defaultContent type:info|warning|errorinfoBuilderdefaultContent(){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:Recordstring,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:Recordstring,string{info:#E3F2FD,warning:#FFF3E0,error:#FFEBEE,}returncolorMap[this.type]}}不传content的时候显示默认文案传了就覆盖掉。这个模式在组件库里特别常见。多插槽PageLayout 实战单个插槽解决不了复杂布局。实际项目里一个页面通常有 header、content、footer有时候还有个浮动按钮。这就需要多个插槽Componentexportstruct PageLayout{BuilderParamheader:()voidthis.emptyBuilderBuilderParamcontent:()voidthis.emptyBuilderBuilderParamfooter:()voidthis.emptyBuilderBuilderParamfloating:()voidthis.emptyBuilderStateshowHeader:booleantrueStateshowFooter:booleantruepaddingValue: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%)}}这里用了四个插槽每个都有默认的空实现。showHeader和showFooter控制是否显示对应区域给外部留了开关。浮动按钮放在 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:number0tabs:string[][推荐,热门,最新]// 用函数类型实现作用域插槽子组件把当前 tab 索引传给父组件BuilderParamtabContent:(index:number,name:string)voidbuild(){Column(){// Tab 栏Row(){ForEach(this.tabs,(tab:string,index:number){Text(tab).fontSize(16).fontWeight(this.activeTabindex?FontWeight.Bold:FontWeight.Normal).fontColor(this.activeTabindex?#2196F3:#666).padding({left:16,right:16,top:12,bottom:12}).onClick((){this.activeTabindex})})}.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(index0){RecommendList()}elseif(index1){HotList()}else{LatestList()}}build(){TabContainer({tabs:[推荐,热门,最新],tabContent:this.renderTab})}}依赖反转让组件只关心骨架这种插槽模式本质上是一种依赖反转。PageLayout 不知道 header 长什么样不知道 content 里放了什么它只提供一个布局骨架。具体的渲染逻辑全部由使用方注入。好处很明显——组件的复用性上去了。同一个 PageLayout首页能用详情页能用设置页也能用。改一个布局问题所有页面同步修复。坏处是代码分散了。出了问题你得在组件和使用方之间来回跳调试路径变长。我的经验是在组件里加好日志把插槽的渲染异常 catch 住别让一个插槽的崩溃拖垮整个页面。写在最后BuilderParam 是 ArkUI 里最被低估的特性之一。很多人写组件只想到用 Prop 传数据其实传渲染逻辑比传数据灵活得多。把长什么样的决策权交给使用方组件只做好放在哪里的事这才是组件库该有的分工。不过也别过度使用。如果一个组件有七八个插槽用起来反而比直接写页面还麻烦那说明这个组件的粒度不对应该拆开重新设计。

相关新闻