【Web全栈进阶】Composition API与组件通信:props深入、emit、插槽

ArticleCard会写了,但组件之间还是"孤岛 "------早报站的页面结构要正式开搭,先解决组件怎么说话:父传子(props)、子传父(emit)、注入内容(slot)、双向绑定(v-model)。

🎯 本篇产出 :ArticleCard v2(收藏按钮)、SearchInput搜索框组件、插槽使用------三大通信姿势一次讲全。含代码约100行。


📌 太长不看版(给想快速上手的你)

项目信息 一句话说明
本篇目标 打通Vue组件之间的通信
代码行数 ~100行(含注释)
依赖 无新增(脚手架已就位)
核心功能 emit收藏 + v-model搜索 + 具名插槽
跑起来的命令 npm run dev → http://localhost:5173
核心知识点 单向数据流、defineEmits、v-model原理、slot
做完你能得到 一套组件通信的完整姿势

一、概念课:组件通信的三种姿势 + 一条铁律

组件之间的关系只有一种:父子 。通信方式就三种,外加一个表单专用语法糖:

🚨 先立那条铁律------单向数据流

📌 数据只有一个主人:谁拥有它,谁才能修改它。

子组件想改父组件的数据?把"请求"发上去(emit),让主人自己改。

🔄 对后端读者的翻译

Vue概念 Python对应物 说明
props 函数参数(只读) 父传子
emit 事件通知 父组件把handler传下来,子组件在恰当时机触发它------近似"把回调传下去"
slot 模板里的"占位插槽" 父组件注入内容

📌 数据流永远自上而下、事件永远自下而上------方向不乱,代码就乱不了。这和第12篇"底层抛异常、顶层接异常"的分层纪律是同一个思想。


二、第1步:ArticleCard v2------加收藏按钮(emit实战)

给卡片升级:点收藏 → 卡片自己不知道收藏存哪(那是父组件和将来的API的事),它只负责把事件报上去。

src/components/ArticleCard.vue(v2关键改动):

vue 复制代码
<script setup>
import { ref } from 'vue'

const props = defineProps({
  id: Number,                 // 新增:上报时要带上"是谁"(id就是载荷的一部分)
  title: String,
  summary: String,
  url: String,
})

// ① 声明本组件会发出的事件------声明过的才算数(第六节①会验证这点)
const emit = defineEmits(['favorite'])

const favorited = ref(false)

function onFavorite() {
  favorited.value = !favorited.value
  // ② 上报事件 + 载荷(对象里带id和最新状态)
  emit('favorite', { id: props.id, state: favorited.value })
}
</script>

<template>
  <article class="card">
    <h2><a :href="url" target="_blank">{{ title }}</a></h2>
    <p>{{ summary }}</p>
    <button @click="onFavorite">
      {{ favorited ? '★ 已收藏' : '☆ 收藏' }}
    </button>
    <!-- ③ 具名插槽出口:父组件可以往这里注入内容(如"阅读原文"链接) -->
    <slot name="actions"></slot>
  </article>
</template>

📌 两个关键点

# 关键点 说明
① defineEmits(['favorite'])声明事件清单 声明即契约,父组件按这个名字监听
② emit('favorite', 载荷)把id和状态一起报上去 子组件不决定"收藏后干嘛",只负责"告诉爸爸:用户收藏了id=3的文章"

💡 <slot name="actions">是插槽出口:组件留好"插座",父组件往里面插内容(见第3步)。


三、第2步:App.vue------监听事件、持有状态

src/App.vue(关键改动):

vue 复制代码
<script setup>
import { ref } from 'vue'
import ArticleCard from './components/ArticleCard.vue'
import SearchInput from './components/SearchInput.vue'

const articles = [
  { id: 1, title: '重构教会我的事', summary: '......', url: '#' },
  // ... 同第09篇
]

// 收藏计数:状态的主人在这里(App层)
const favorites = ref(0)

function handleFavorite({ id, state }) {
  favorites.value += state ? 1 : -1
  console.log('收藏事件:', id, state)   // 第13篇这里换成调API
}

const keyword = ref('')
</script>

<template>
  <h1>Python 早报</h1>
  <SearchInput v-model="keyword" />
  <p v-if="keyword">正在搜索:{{ keyword }}(第13篇接真搜索)</p>
  <p>已收藏 {{ favorites }} 篇</p>

  <ArticleCard
    v-for="a in articles"
    :key="a.id"
    v-bind="a"
    @favorite="handleFavorite"
  >
    <!-- 具名插槽的使用:往actions插座里插入"阅读原文"链接 -->
    <template #actions>
      <a :href="a.url" target="_blank">阅读原文 ↗</a>
    </template>
  </ArticleCard>
</template>

🎯 看清楚数据的流向

text 复制代码
卡片里点收藏 → emit上报 → App的handleFavorite修改favorites → 顶部的"已收藏N篇"自动更新

📌 数据的主人(App)始终只有一个,修改永远发生在主人手里------单向数据流完整闭环。

💡 v-bind="a"的妙用

把对象的所有字段展开传给props的简写(等价于:id="a.id" :title="a.title"......),列表场景省一大截代码。


四、第3步:SearchInput------v-model的双向绑定原理

v-model是"双向绑定"语法糖。先拆穿它的真面目:

text 复制代码
v-model="keyword"
    ≡  :model-value="keyword"  +  @update:model-value="keyword = $event"

📌 一个输入框组件要实现v-model,只需遵守两条约定:prop叫modelValue,事件叫update:modelValue。

src/components/SearchInput.vue:

vue 复制代码
<script setup>
defineProps({ modelValue: String })               // 约定1:prop名
const emit = defineEmits(['update:modelValue'])   // 约定2:事件名

function onInput(e) {
  emit('update:modelValue', e.target.value)       // 输入 → 上报给主人
}
</script>

<template>
  <input type="text" :value="modelValue"
         @input="onInput"
         placeholder="搜索文章..." />
</template>

🎯 数据流依旧单向

text 复制代码
输入事件 → emit上报 → App改keyword → keyword流回:value

📌 "双向绑定"是错觉,底下还是单向流 + 语法糖 ------理解这一点,将来调试任何v-model问题都不慌(第六节④)。

✅ 浏览器实测

  • 搜索框打字 → "正在搜索:xxx"实时跟上;
  • 点卡片收藏 → 顶部计数+1。

🎉 全部无刷新、无后端------纯前端状态流的胜利。


五、验收清单

bash 复制代码
1. 点收藏 → ★切换 + 顶部"已收藏N篇"实时更新
2. 连点两次同一张卡 → 计数先+1再-1(状态上报正确)
3. 搜索框输入 → "正在搜索"文案实时跟上
4. 卡片底部出现"阅读原文"链接(具名插槽生效)
5. 控制台无Vue warn(事件已声明、props未被修改)
6. 全程无报错后提交Git
bash 复制代码
git add .
git commit -m "组件通信:emit收藏 + v-model搜索 + 具名插槽"

六、常见报错:这6个,组件通信的标配(重点!)

① 控制台警告Component emitted event "favorite" but it is neither declared nor listened to

🔍 原因 :emit了没在defineEmits声明的事件(或父组件没监听)。

✅ 解法 :defineEmits声明 + 父组件@favorite监听,两头对齐。

📌 事件和props一样是契约,先声明后使用。


② 父组件监听没反应:@favorite写了但handler不触发

🔍 原因 :事件名拼写不一致------emit('favorite')与@favorite的大小写/拼写差一个字符。

✅ 解法:统一用camelCase命名。

📌 事件名是字符串,字符串的匹配没有任何容错。


③ [Vue warn]: Component is mutating props

🔍 原因 :子组件直接改了props(比如props.summary = 'xx')------违反单向数据流。

✅ 解法:

  • 想改数据?emit上报让父组件改;
  • 只想内部用?先拷贝成本地ref。

④ v-model绑定的输入框不响应 / 输入后立刻"弹回"

🔍 原因 :子组件没emit update:modelValue(或prop名不是modelValue)------输入了但主人没收到,value被旧值弹回。

✅ 解法 :对照第4节的两条约定逐字检查。

📌 v-model不是魔法,是两条约定。


⑤ 插槽内容死活不显示

🔍 原因 :子组件模板里没放<slot />出口------内容没地方插。

✅ 解法 :检查子组件有没有"插座";具名插槽要配对:<slot name="actions"> ↔ <template #actions>。


⑥ 具名插槽内容显示在了错误位置 / 和默认内容混在一起

🔍 原因 :#actions写成了普通内容,或子组件默认slot与具名slot混淆。

✅ 解法:

  • 具名插槽一律用<template #名字>包裹;
  • 默认内容放<slot>(不带name)。

七、课后练习

# 练习 难度 提示
1 状态提升:把点赞的liked状态也提升到App(favorites列表存id),刷新页面后计数不丢 ⭐⭐⭐ 状态提升是组件设计的第一课
2 删除卡片 :给卡片加删除按钮,emit('remove', id),App里从数组移除 ⭐⭐ 复习"主人改数据"
3 双v-model:搜索框加"区分大小写"复选框 ⭐⭐⭐ :checked + @change,或v-model第二个绑定
4(选做) 给emit加校验 :v-model组件的modelValue prop加required: true与类型校验 ⭐⭐ props深入的最后一块拼图

📦 配套代码

完整组件已上传Git(python_daily/,node_modules不入仓):【gitee仓库地址】

相关推荐
ZoeLandia1 小时前
TypeScript 03:泛型与工具类型
前端·typescript
code_Bo1 小时前
从策划到上线:我和 AI 一起做了个微信记账小程序
前端·ai编程·cursor
Shirley~~1 小时前
Three.js加载Gltf模型并播放动画
前端·3d
Data analyse4561 小时前
Session超时与浏览器休眠的计时校验
大数据·前端·数据库·数据分析
星始流年2 小时前
AI 时代,软件程序员未来 10—15 年的职业前景
前端·后端·ai编程
猪猪拆迁队2 小时前
跨电脑复制共享-局域网直连踩坑
前端·后端·go
静默回滚2 小时前
逐帧处理视频的内存去哪了,我从看错读数查到没排队
前端
重庆穿山甲2 小时前
我们不做「看起来很聪明」的知识库:一套企业级 RAG 平台的 37 屏真机实录
前端
风花一世月2 小时前
🛩️ 我把一段夕阳飞行做成了能按键驾驶的网页(24 秒四段航线,在线直接玩)
前端