3.2 自定义组件与通信机制


3.2 自定义组件与通信机制

本节导读:选型之后是封装。本节把商城首页的商品卡片抽成自定义组件,沿线讲 props 的单向数据流、事件的命名与传参、插槽的差异化布局、跨层级的通信手段,最后交代动态组件与异步组件在小程序端的能力边界与替代方案。读完你应当能设计一套三端行为一致的组件通信结构。

把商品卡片抽成组件的第一刀

首页瀑布流里每张商品卡片的结构完全一致,运营却要求"猜你喜欢"与"限时秒杀"两个板块的卡片长得有差别。团队的第一刀切下去:结构公共部分抽成 goods-card 组件,差异区域留插槽。动手前先定通信契约——数据从哪进、行为往哪报、差异怎么留口。这三件事对应 props、事件与插槽,是组件通信的三大主通道,先把组件骨架立起来:

<!-- components/goods-card/goods-card.vue --> <template> <view class="card" @tap="onTap"> <image class="cover" :src="goods.cover" mode="aspectFill" /> <view class="info"> <text class="name">{{ goods.name }}</text> <text class="price">¥{{ goods.price }}</text> </view> <!-- 差异区域:秒杀板块在这里塞倒计时,猜你喜欢塞推荐语 --> <view class="extra"> <slot name="extra"> <text v-if="goods.tag" class="tag">{{ goods.tag }}</text> </slot> </view> </view> </template> <script> export default { name: 'goods-card', props: { goods: { type: Object, required: true }, clickable: { type: Boolean, default: true } }, emits: ['pick'], methods: { onTap() { if (!this.clickable) return; // 组件只报行为,不带业务路由——去哪个页面由页面自己决定 this.$emit('pick', { id: this.goods.id, from: 'card' }); } } }; </script>

这份骨架里有三个值得抄走的决定。其一,props 只收数据不带回调,组件不反向调用页面逻辑;其二,事件名用小写单词加 pick 这种动词,不拼 card-click 之类的复合描述——事件是行为通知不是样式说明;其三,emits 声明写全,Vue3 工程里未声明的事件会透传到根元素,排查起来极其隐蔽。

单向数据流:改数的权限只有数据的主人有

props 的铁律是单向:页面把 goods 传进卡片,卡片想改数量,正确做法不是直接改 goods.num,而是把意图上报给页面,由页面改完再传回来。初学者常在子组件里写 this.goods.num += 1,H5 端看着"也能跑",到小程序端某些写法直接静默失败,而且任何工具都查不出这种"反向改数"的所有权混乱。规范写法:

<!-- 页面侧 --> <goods-card :goods="item" @pick="onPick" @count-change="onCountChange" />
// 页面持有数据与修改权 export default { methods: { onCountChange({ id, delta }) { const target = this.list.find(g => g.id === id); if (target) target.num = Math.max(1, target.num + delta); } } };

跨层级的场景(深层子组件要通知顶层弹窗、多个页面共享的角标状态)分两档处理:跨一两层用 provide 与 inject,祖先声明、后代取用,适合主题配置、表单上下文这类"环境变量";跨页面或全局共享进状态管理,第 5 章的 Pinia 接管。在组件通信里引入全局状态要克制——组件一旦依赖全局 store,复用性就打了折,商业组件库里常见折中是 props 给默认值、store 只做兜底。

插槽:把"不一样"留成口子

默认插槽、具名插槽、作用域插槽三种形态在 uni-app 各端都支持,但小程序端的插槽是"按编译产物展开"的,两个习惯要养成:一是插槽内容里不要依赖页面作用域的复杂表达式,二是作用域插槽传出去的数据保持扁平(对象一层,别嵌三层),因为小程序端作用域插槽的数据经编译器中转,深层结构在部分基础库版本上表现不稳。回到商城案例,秒杀板块这样消费具名插槽:

<goods-card :goods="flashGoods"> <template v-slot:extra> <view class="countdown"> <text class="cd-num">{{ hh }}</text> <text class="cd-sep">:</text> <text class="cd-num">{{ mm }}</text> </view> </template> </goods-card>

倒计时的状态归页面管,卡片只留口子——组件越"笨",复用面越宽。

动态组件与异步组件:小程序端的能力边界

Vue 的动态组件 <component :is="..."> 在 H5 端畅通无阻,到了小程序端就有一条硬边界:is 绑定的组件必须是页面里已注册的组件,且部分端的编译产物不支持把页面级组件当动态组件切换。异步组件同理,defineAsyncComponent 这类按需加载手段主要服务于 H5 端拆包,小程序端的分包才是官方指定的减载体积通道(第 4 章展开)。工程上用两个替代方案覆盖绝大多数需求:

<template> <!-- 方案一:组件数量有限且已知,v-if 分支最稳 --> <goods-card v-if="mode === 'card'" :goods="goods" /> <goods-row v-else :goods="goods" /> </template>
// 方案二:真需要按需加载时,把"晚一点渲染"做成显示控制 export default { data() { return { heavyPanelReady: false }; }, mounted() { // 首屏渲染完成后再挂重组件,效果接近异步组件 setTimeout(() => { this.heavyPanelReady = true; }, 300); } };

前者牺牲一点灵活性换三端确定性,后者把"延迟加载"翻译成"延迟渲染",绕开各端加载机制的差异。商城的抽奖大转盘组件就是方案二的受益者:首屏只渲染占位骨架,转盘库在首页可交互后才挂载,首屏时间没有因为重组件劣化。

本节要点回顾

  • 组件通信三主通道:props 进、事件出、插槽留差异区;emits 声明写全防止事件透传;
  • 单向数据流是改数权限的契约,子组件报意图、页面持修改权,别在子组件里反向改 props;
  • 跨层级通信分两档:一两层用 provide 与 inject,跨页面共享交给第 5 章的状态管理;
  • 动态组件与异步组件在小程序端有硬边界,v-if 分支与延迟渲染是两个可靠的替代;
  • 组件越"笨"复用面越宽:状态与路由归页面,组件只报行为。

组件结构与通信立住了,下一节解决"长得对"的最后一块拼图:rpx 的换算规则、样式隔离,以及要不要引 UI 框架的取舍。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U