HarmonyOS7 跨层购物车页实战:@Provide 与 @Consume 不只是会用,还要用得顺手
文章目录
- 共享状态只放真正共享的东西
- 判断是否已加购很关键
- `@Provide` 和 `@Consume` 的使用边界
- 页面结构怎么看
- 完整代码
- 写在最后
购物车是最适合讲跨层状态的场景之一。顶部角标要知道购物车数量,商品列表要能加购,底部结算区还要算合计。如果每一层都手动传cartItems,页面一复杂,参数会传得很难看。
这个 HarmonyOS7 案例用@Provide和@Consume把购物车状态放在祖先组件里,后代组件按需消费。它解决的不是“少写两行代码”,而是让跨层数据有一个稳定来源。
共享状态只放真正共享的东西
页面里的核心数据结构是CartEntry,包含商品 id、名称、价格、数量和图标。这个结构会同时被角标、商品区和购物车区域使用,所以适合放进跨层共享状态。
但不是所有状态都应该这么做。比如某个按钮的按下态、某个局部弹窗的显隐,就没必要放进@Provide。共享状态越多,后面越难判断谁改了它。
判断是否已加购很关键
isInCart()很短,但它决定了商品按钮展示“加入购物车”还是“已加入”。这种判断建议单独封装,不要在 UI 表达式里重复写。
isInCart(id:number):boolean{returnthis.cartItems.some((e:CartEntry)=>e.id===id)}配合getQty()和addToCart(),商品区就能在不关心父组件层级的情况下读到购物车状态。角标组件CartBadge也一样,它只消费cartItems,然后自己计算总数量。
@Provide和@Consume的使用边界
这个案例适合跨层共享,不适合跨页面全局共享。也就是说,它更像页面内部的上下文,而不是应用级状态管理。如果购物车要跨多个页面同步,后面还需要配合持久化或全局状态方案。
在一个页面内部,@Provide('cartItems')提供数据,深层组件用@Consume('cartItems')拿数据。中间层不用一层层转发参数,结构会干净很多。
页面结构怎么看
SharedCartContextPage是状态提供方,CartBadge和商品区域是消费方。读代码时先找@Provide,再找@Consume,最后看addToCart()和clearCart()怎么修改同一份数据。
如果你准备做商品详情、购物车角标、收藏夹数量这类功能,这个例子可以直接当页面级共享状态模板。
完整代码
下面是完整代码。入口组件名是SharedCartContextPage,可以作为 HarmonyOS7 页面内跨层状态共享的参考。
// @Provide 与 @Consume 跨层数据共享// 购物车状态(跨组件共享)// 说明:@Provide/@Consume 实现祖先到后代的数据共享,无需逐层传递interfaceCartEntry{id:numbername:stringprice:numberqty:numbericon:string}// 深层嵌套子组件:购物车角标(消费 cart 数据)@Componentstruct CartBadge{@Consume('cartItems')cartItems:CartEntry[]gettotalCount():number{returnthis.cartItems.reduce((s:number,e:CartEntry)=>s+e.qty,0)}build(){Stack({alignContent:Alignment.TopEnd}){Text('🛒').fontSize(26)if(this.totalCount>0){Text(this.totalCount>99?'99+':this.totalCount.toString()).fontSize(10).fontColor(Color.White).backgroundColor('#EF4444').borderRadius(9).width(18).height(18).textAlign(TextAlign.Center).offset({x:6,y:-6})}}.width(40).height(40)}}// 中间层组件:商品分类(不持有购物车状态,只透传)@Componentstruct ProductSection{@Consume('cartItems')cartItems:CartEntry[]title:string=''items:CartEntry[]=[]isInCart(id:number):boolean{returnthis.cartItems.some((e:CartEntry)=>e.id===id)}getQty(id:number):number{constfound=this.cartItems.find((e:CartEntry)=>e.id===id)returnfound?found.qty:0}addToCart(item:CartEntry){constidx=this.cartItems.findIndex((e:CartEntry)=>e.id===item.id)if(idx>=0){this.cartItems=this.cartItems.map((e:CartEntry,i:number)=>{if(i===idx){return{id:e.id,name:e.name,price:e.price,qty:e.qty+1,icon:e.icon}}returne})}else{item.qty=1;this.cartItems=this.cartItems.concat([item])}}build(){Column({space:8}){Text(this.title).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#374151').width('100%')ForEach(this.items,(item:CartEntry)=>{Row({space:12}){Text(item.icon).fontSize(28).width(48).height(48).textAlign(TextAlign.Center).backgroundColor('#F9FAFB').borderRadius(10)Column({space:4}){Text(item.name).fontSize(14).fontWeight(FontWeight.Medium).fontColor('#111827')Text('¥'+item.price.toFixed(2)).fontSize(14).fontColor('#EF4444').fontWeight(FontWeight.Bold)}.layoutWeight(1).alignItems(HorizontalAlign.Start)if(this.isInCart(item.id)){Row({space:6}){Button('−').fontSize(14).width(28).height(28).borderRadius(14).fontColor('#374151').backgroundColor('#F3F4F6').padding(0).onClick(()=>{constidx=this.cartItems.findIndex((e:CartEntry)=>e.id===item.id)if(idx>=0){constnewQty=this.cartItems[idx].qty-1if(newQty<=0){this.cartItems=this.cartItems.filter((e:CartEntry)=>e.id!==item.id)}else{this.cartItems=this.cartItems.map((e:CartEntry,i:number)=>{if(i===idx){return{id:e.id,name:e.name,price:e.price,qty:newQty,icon:e.icon}}returne})}}})Text(this.getQty(item.id).toString()).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#111827').width(24).textAlign(TextAlign.Center)Button('+').fontSize(14).width(28).height(28).borderRadius(14).fontColor('#2563EB').backgroundColor('#EFF6FF').padding(0).onClick(()=>this.addToCart(item))}.alignItems(VerticalAlign.Center)}else{Button('+加入').fontSize(12).fontColor(Color.White).backgroundColor('#2563EB').borderRadius(16).padding({left:12,right:12,top:6,bottom:6}).onClick(()=>this.addToCart(item))}}.width('100%').padding({left:14,right:14,top:10,bottom:10}).backgroundColor(Color.White).borderRadius(10).alignItems(VerticalAlign.Center)},(item:CartEntry)=>item.id.toString())}.width('100%').alignItems(HorizontalAlign.Start)}}@Entry@Componentstruct SharedCartContextPage{@Provide('cartItems')cartItems:CartEntry[]=[]privateelectronics:CartEntry[]=[{id:1,name:'无线蓝牙耳机',price:299,qty:0,icon:'🎧'},{id:2,name:'便携充电宝',price:89,qty:0,icon:'🔋'},{id:3,name:'USB-C 数据线',price:29,qty:0,icon:'🔌'},]privatebooks:CartEntry[]=[{id:4,name:'HarmonyOS 开发实战',price:69,qty:0,icon:'📘'},{id:5,name:'ArkUI 组件详解',price:59,qty:0,icon:'📗'},]gettotalPrice():number{returnthis.cartItems.reduce((s:number,e:CartEntry)=>s+e.price*e.qty,0)}clearCart(){this.cartItems=[]}build(){Column({space:0}){// 顶部:导航栏(包含 CartBadge 深层消费数据)Row(){Column({space:2}){Text('@Provide / @Consume').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#111827')Text('跨层数据共享,无需逐层传递').fontSize(12).fontColor('#6B7280')}.alignItems(HorizontalAlign.Start).layoutWeight(1)// CartBadge 是深层组件,通过 @Consume 直接获取购物车数据CartBadge()}.width('100%').padding({left:20,right:20,top:16,bottom:14}).backgroundColor(Color.White)// 说明Row(){Text('💡').fontSize(14).margin({right:6})Text('@Provide 在顶层注入数据;@Consume 在任意层级直接消费,无需 @Prop 层层传递').fontSize(12).fontColor('#1E40AF').layoutWeight(1)}.width('100%').padding({left:16,right:16,top:8,bottom:8}).backgroundColor('#DBEAFE').alignItems(VerticalAlign.Top)Scroll(){Column({space:16}){// 商品区域(ProductSection 是中间层,不持有状态)ProductSection({title:'📱 数码产品',items:this.electronics}).padding({left:20,right:20})ProductSection({title:'📚 技术书籍',items:this.books}).padding({left:20,right:20})// 购物车汇总if(this.cartItems.length>0){Column({space:10}){Row(){Text('🛒 购物车').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#374151').layoutWeight(1)Text('清空').fontSize(13).fontColor('#EF4444').onClick(()=>this.clearCart())}.width('100%')ForEach(this.cartItems,(entry:CartEntry)=>{Row({space:10}){Text(entry.icon).fontSize(20)Text(entry.name).fontSize(13).fontColor('#374151').layoutWeight(1)Text('×'+entry.qty.toString()).fontSize(13).fontColor('#9CA3AF')Text('¥'+(entry.price*entry.qty).toFixed(2)).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#EF4444')}.width('100%').alignItems(VerticalAlign.Center)},(entry:CartEntry)=>entry.id.toString())Divider().color('#E5E7EB')Row(){Text('合计').fontSize(14).fontColor('#374151').layoutWeight(1)Text('¥'+this.totalPrice.toFixed(2)).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#EF4444')}.width('100%').alignItems(VerticalAlign.Center)}.width('100%').padding(16).backgroundColor('#FFF7ED').borderRadius(12).margin({left:20,right:20,bottom:24})}else{Text('购物车为空,去选购商品吧~').fontSize(13).fontColor('#9CA3AF').width('100%').textAlign(TextAlign.Center).padding({top:16,bottom:24})}}.padding({top:12})}.layoutWeight(1)}.width('100%').height('100%').backgroundColor('#F9FAFB')}}写在最后
跨层购物车页 这个案例的价值,不在于用了多少组件,而在于它把@Provide 与 @Consume放进了一个完整页面里。照着这个思路改自己的业务,比单独背 API 更靠谱。
