【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仓库地址】

相关推荐
小天源37 分钟前
智叟云集系统 · 架构与职责分层
服务器·前端·数据库
Highcharts.js2 小时前
React 中原生支持使用 JSX 组件配置自定义表格 Highcharts Grid
前端·javascript·react.js·前端框架·jsx·highcharts·自定义表格
薛定谔的猫-菜鸟程序员3 小时前
Electron开发超详细教程(从入门到放弃)
前端·javascript·electron
最强小杰3 小时前
DeepSeek V4 Flash + Codex CLI 接入教程:原生 Responses API 适配,从配置到跑通收藏这篇就够了(2026)
java·前端·python·ai
静水leo3 小时前
使用 Whistle 拦截接口并模拟后端响应
前端
掘金者阿豪4 小时前
SQL Server数据迁移完成后,凌晨的数据库告警该怎么查?
前端·后端
小郎君6584 小时前
Vue 多标签页的加载态设计:为什么我没有用 Suspense 实现 PageLoading
vue.js·算法
JudithHuang4 小时前
React 嵌套路由与布局
前端·react.js·arcgis·react·react router
啾咪orJIUMI4 小时前
Vue 3 + SCSS 实现全屏系统门户:五栏展开与灯光联动
css