HarmonyOS7 父子购物车通信页实战:@Prop 与 @Link 不只是会用,还要用得顺手
文章目录Prop 适合只读输入子组件要反向通知就用回调或 LinkLink 适合双向绑定状态页面结构怎么看完整代码写在最后父子组件通信最容易在购物车里讲清楚。父组件知道商品列表和总价子组件负责某一行商品的数量调整。问题来了哪些数据只能父传子哪些变化又要从子组件通知回父组件这个 HarmonyOS7 案例把Prop、Link和回调放到同一个购物车页面里读完能比较直观地理解它们的边界。Prop 适合只读输入QuantitySelector里maxStock和productName用的是Prop。这些数据来自父组件子组件只拿来展示或做限制不应该直接改父组件的数据。这类场景很好判断如果子组件只是需要知道库存上限、商品名称、初始配置用Prop就够了。子组件要反向通知就用回调或 Link数量变化时父组件通常需要重新计算总价。这个时候子组件不能闷头自己改状态而要把变化通知出去。案例里通过onQuantityChange回调把数量传回父组件。购物车行删除也是类似思路。removeItem()同时过滤商品数组和数量数组保证两个列表保持一致。removeItem(idx:number){this.cartItemsthis.cartItems.filter((_:ProductInfo,i:number)i!idx)this.cartQuantitiesthis.cartQuantities.filter((_:number,i:number)i!idx)}这里最怕只删了商品没删数量或者只删了数量没删商品。购物车页面很多奇怪总价 bug都是这种状态不同步造成的。Link 适合双向绑定状态如果某个状态确实要父子双方都能更新比如主题开关、选中状态、数量值就可以考虑Link。它比回调更直接但也要克制使用。我的习惯是简单通知用回调明确双向状态用Link纯展示用Prop。这样组件职责会清楚很多。页面结构怎么看ParentChildCartPage里有商品数量选择器、主题切换和购物车行组件。读代码时可以先看子组件参数声明再看父组件如何传入状态最后看回调如何改父组件数组。如果你要做订单确认页、商品规格页、结算页这篇里的通信方式都能直接复用。完整代码下面是完整代码。入口组件名是ParentChildCartPage适合用来练习 HarmonyOS7 父子组件通信。// Prop 与 Link 父子组件通信interfaceProductInfo{id:numbername:stringprice:numberstock:numbericon:string}// 子组件商品数量选择器Prop 单向传入Componentstruct QuantitySelector{PropmaxStock:number10PropproductName:stringStatequantity:number1onQuantityChange:(qty:number)void(){}build(){Column({space:6}){Text(${this.productName}数量选择).fontSize(12).fontColor(#6B7280)Row({space:10}){Button(−).fontSize(18).width(36).height(36).borderRadius(18).fontColor(this.quantity1?#D1D5DB:#374151).backgroundColor(this.quantity1?#F9FAFB:#F3F4F6).onClick((){if(this.quantity1){this.quantity--this.onQuantityChange(this.quantity)}})Text(this.quantity.toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor(#111827).width(40).textAlign(TextAlign.Center)Button().fontSize(18).width(36).height(36).borderRadius(18).fontColor(this.quantitythis.maxStock?#D1D5DB:#374151).backgroundColor(this.quantitythis.maxStock?#F9FAFB:#F3F4F6).onClick((){if(this.quantitythis.maxStock){this.quantitythis.onQuantityChange(this.quantity)}})}Text(库存${this.maxStock}).fontSize(11).fontColor(#9CA3AF)}.alignItems(HorizontalAlign.Center)}}// 子组件主题切换Link 双向绑定Componentstruct ThemeToggle{LinkisDark:booleanbuild(){Row({space:8}){Text(☀️).fontSize(18).fontColor(this.isDark?#9CA3AF:#D97706)Toggle({type:ToggleType.Switch,isOn:this.isDark}).selectedColor(#374151).onChange((val:boolean){this.isDarkval})Text().fontSize(18).fontColor(this.isDark?#60A5FA:#9CA3AF)}.alignItems(VerticalAlign.Center)}}// 子组件购物车商品行Prop 回调Componentstruct CartRow{Propproduct:ProductInfoPropquantity:number1onRemove:()void(){}build(){Row({space:12}){Text(this.product.icon).fontSize(28).width(48).height(48).textAlign(TextAlign.Center).backgroundColor(#F9FAFB).borderRadius(10)Column({space:4}){Text(this.product.name).fontSize(14).fontWeight(FontWeight.Medium).fontColor(#111827)Row({space:8}){Text(¥this.product.price.toFixed(2)).fontSize(15).fontColor(#EF4444).fontWeight(FontWeight.Bold)Text(×this.quantity.toString()).fontSize(13).fontColor(#9CA3AF)}}.layoutWeight(1).alignItems(HorizontalAlign.Start)Text(¥(this.product.price*this.quantity).toFixed(2)).fontSize(14).fontWeight(FontWeight.Bold).fontColor(#111827).width(60).textAlign(TextAlign.End)Text(️).fontSize(18).onClick(()this.onRemove())}.width(100%).padding({left:16,right:16,top:12,bottom:12}).backgroundColor(Color.White).borderRadius(10).alignItems(VerticalAlign.Center)}}EntryComponentstruct ParentChildCartPage{StateisDark:booleanfalseStateselectedProduct:ProductInfo{id:1,name:无线耳机,price:299,stock:5,icon:}StateselectedQty:number1StatecartItems:ProductInfo[][{id:1,name:无线耳机,price:299,stock:5,icon:},{id:2,name:机械键盘,price:599,stock:3,icon:⌨️},]StatecartQuantities:number[][1,2]gettotalPrice():number{returnthis.cartItems.reduce((sum:number,item:ProductInfo,idx:number)sumitem.price*this.cartQuantities[idx],0)}removeItem(idx:number){this.cartItemsthis.cartItems.filter((_:ProductInfo,i:number)i!idx)this.cartQuantitiesthis.cartQuantities.filter((_:number,i:number)i!idx)}build(){Scroll(){Column({space:0}){// 顶部主题切换Link 演示Row(){Column({space:2}){Text(Prop Link 通信).fontSize(18).fontWeight(FontWeight.Bold).fontColor(this.isDark?#FFFFFF:#111827)Text(父子组件数据传递).fontSize(12).fontColor(this.isDark?#9CA3AF:#6B7280)}.alignItems(HorizontalAlign.Start).layoutWeight(1)// Link 双向绑定演示ThemeToggle({isDark:$isDark})}.width(100%).padding({left:20,right:20,top:16,bottom:14}).backgroundColor(this.isDark?#1F2937:#FFFFFF)// Link 说明Row(){Text().fontSize(14).margin({right:6})Text(ThemeToggle 子组件通过 Link 修改父组件 isDark双向同步).fontSize(12).fontColor(this.isDark?#D1FAE5:#065F46).layoutWeight(1)}.width(100%).padding({left:16,right:16,top:8,bottom:8}).backgroundColor(this.isDark?#064E3B:#D1FAE5)// Prop 演示区Column({space:12}){Text(一、Prop父→子 单向).fontSize(14).fontWeight(FontWeight.Bold).fontColor(this.isDark?#FFFFFF:#374151).width(100%)Row(){Text().fontSize(14).margin({right:6})Text(Prop 接收父组件数据子组件内部可修改但不回传单向).fontSize(12).fontColor(#92400E).layoutWeight(1)}.padding({left:12,right:12,top:8,bottom:8}).backgroundColor(#FEF3C7).borderRadius(8).alignItems(VerticalAlign.Top)// QuantitySelector 子组件演示Column({space:8}){QuantitySelector({maxStock:this.selectedProduct.stock,productName:this.selectedProduct.name,onQuantityChange:(qty:number){this.selectedQtyqty}})Text(父组件接收到数量回调${this.selectedQty}).fontSize(13).fontColor(#2563EB)}.padding(16).backgroundColor(this.isDark?#1F2937:#F9FAFB).borderRadius(10)}.width(100%).padding({left:20,right:20,top:16,bottom:0}).alignItems(HorizontalAlign.Start)// 购物车CartRow 子组件演示Column({space:12}){Text(二、CartRow 子组件Prop 回调).fontSize(14).fontWeight(FontWeight.Bold).fontColor(this.isDark?#FFFFFF:#374151).width(100%)Column({space:8}){ForEach(this.cartItems,(item:ProductInfo,idx:number){CartRow({product:item,quantity:this.cartQuantities[idx],onRemove:(){this.removeItem(idx)}})},(item:ProductInfo)item.id.toString())}Row(){Text(合计).fontSize(14).fontColor(this.isDark?#9CA3AF:#374151).layoutWeight(1)Text(¥this.totalPrice.toFixed(2)).fontSize(20).fontWeight(FontWeight.Bold).fontColor(#EF4444)}.width(100%).padding({left:16,right:16,top:12,bottom:12}).backgroundColor(this.isDark?#1F2937:#FFF7ED).borderRadius(10).alignItems(VerticalAlign.Center)}.width(100%).padding({left:20,right:20,top:16,bottom:24}).alignItems(HorizontalAlign.Start)}.backgroundColor(this.isDark?#111827:#F9FAFB)}.width(100%).height(100%).backgroundColor(this.isDark?#111827:#F9FAFB)}}写在最后如果你正在整理 HarmonyOS7 组件案例可以把这篇当成Prop 与 Link的实践模板。先跑通页面再逐步替换数据和样式效率会高不少。