第三章 Vue3 组件基础:组件注册、通信与插槽

好,咱们今天聊聊 Vue3 的组件。说实话,组件化开发是 Vue 的灵魂,也是你从「会用」到「会设计」的分水岭。我在带团队时发现,很多人写组件就是「把页面拆碎」,但拆完之后怎么拼回去、怎么让它们好好协作,这才是真功夫。

这一章我会把组件注册、通信和插槽这三个核心点讲透。嗯,都是我在项目里踩过坑、又爬出来的经验。

3.1 组件注册:全局 vs 局部

先问个问题:你写了一个组件,怎么让 Vue 认识它?

两种方式:全局注册和局部注册。说白了,全局注册就是「广而告之」,局部注册就是「内部消化」。

3.1.1 全局注册

main.js 里用 app.component() 搞定。注册之后,整个应用任何地方都能直接用,不用再引入。

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import MyButton from './components/MyButton.vue'

const app = createApp(App)
app.component('MyButton', MyButton)
app.mount('#app')

然后你在任何组件里都能写:

<template>
  <MyButton label="提交" />
</template>
我的习惯:全局注册适合那些「到处都用」的基础组件,比如按钮、输入框、图标。但别什么都往全局塞,否则打包体积会变大,而且你很难追踪某个组件到底在哪定义的。

3.1.2 局部注册

局部注册更常见。在 <script setup> 里直接 import,然后就能用。Vue 会自动帮你注册。

<script setup>
import MyButton from './components/MyButton.vue'
</script>

<template>
  <MyButton label="取消" />
</template>
我曾经踩过的坑:全局注册的组件如果改名了,你得去所有用到它的地方手动改。局部注册就没这问题,因为每个组件都是显式引入的。所以我的建议是:能用局部就别用全局,除非你真的确定这个组件 90% 的页面都会用到。

3.2 组件通信:props 与 emit

组件之间怎么传数据?这是面试必问,也是项目里天天要用的。最基础也最核心的就是 props 向下传,emit 向上传

3.2.1 父传子:props

父组件通过属性传值,子组件用 defineProps 接收。你想想看,这就像你给下属分配任务——你把参数告诉他,他照着做。

父组件:

<template>
  <ChildComponent :title="pageTitle" :count="5" />
</template>

<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const pageTitle = ref('用户列表')
</script>

子组件:

<template>
  <div>
    <h3>{{ title }}</h3>
    <p>数量:{{ count }}</p>
  </div>
</template>

<script setup>
const props = defineProps({
  title: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0
  }
})
</script>
重点:props 是单向数据流。子组件不能直接修改 props,否则会报错。如果非要改,那就用 emit 通知父组件去改。这是 Vue 的设计哲学——数据流向要清晰。

3.2.2 子传父:emit

子组件想告诉父组件「我这边发生了什么事」,就用 emit。比如用户点击了按钮、输入框内容变了。

子组件:

<template>
  <button @click="handleClick">点击我</button>
</template>

<script setup>
const emit = defineEmits(['update'])

function handleClick() {
  emit('update', '子组件发来的消息')
}
</script>

父组件:

<template>
  <ChildComponent @update="handleUpdate" />
</template>

<script setup>
import ChildComponent from './ChildComponent.vue'

function handleUpdate(msg) {
  console.log('收到子组件消息:', msg)
}
</script>
我的经验:emit 的事件名最好用 kebab-case(短横线命名),比如 update-user。虽然 Vue3 也支持 camelCase,但 HTML 模板里不区分大小写,容易出 bug。我因为这个吃过亏,后来就统一用短横线了。

3.3 插槽(Slot):让组件更灵活

插槽是什么?说白了,就是组件里留个「坑位」,让父组件往里面填内容。这样组件就不只是「传数据」,还能「传模板」。

3.3.1 默认插槽

最简单的插槽,就是 <slot></slot>。父组件写在标签中间的内容,都会跑到这个位置。

子组件(Card.vue):

<template>
  <div class="card">
    <div class="card-header">卡片标题</div>
    <div class="card-body">
      <slot></slot>
    </div>
  </div>
</template>

父组件:

<template>
  <Card>
    <p>这是卡片里的内容</p>
  </Card>
</template>

3.3.2 具名插槽

一个组件里可能有多个坑位,那就用 name 区分。父组件用 v-slot:name 指定填哪个坑。

子组件:

<template>
  <div class="layout">
    <header>
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot>
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

父组件:

<template>
  <Layout>
    <template v-slot:header>
      <h1>页面标题</h1>
    </template>

    <p>这是主要内容</p>

    <template v-slot:footer>
      <p>版权信息</p>
    </template>
  </Layout>
</template>
注意:v-slot 可以简写为 #,比如 #header。但前提是你要清楚每个插槽的用途,否则代码可读性会下降。我个人习惯在组件内部用 #,在文档或演示里用完整写法。

3.3.3 作用域插槽

这个稍微有点绕。有时候子组件想把自己的数据传给插槽内容,让父组件决定怎么渲染。那就用作用域插槽。

子组件:

<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot :item="item" :index="index">
        {{ item.name }}
      </slot>
    </li>
  </ul>
</template>

<script setup>
const items = [
  { id: 1, name: '苹果', price: 5 },
  { id: 2, name: '香蕉', price: 3 }
]
</script>

父组件:

<template>
  <List>
    <template #default="{ item }">
      <span>{{ item.name }} - ¥{{ item.price }}</span>
    </template>
  </List>
</template>
避坑指南:作用域插槽的解构写法 #default="{ item }" 是 Vue3 推荐的。我曾经在 Vue2 项目里用 slot-scope,迁移到 Vue3 时全得改。所以现在写新项目,一律用 v-slot 加解构。

3.4 总结一下

这一章的内容,说白了就是三件事:

  • 注册:全局注册方便但容易失控,局部注册更可控。我建议你优先用局部注册。
  • 通信:props 向下传数据,emit 向上发事件。这是最基础的父子通信方式,也是你理解 Vue 数据流的关键。
  • 插槽:让组件从「数据容器」变成「模板容器」。默认插槽、具名插槽、作用域插槽,一层层递进,能解决大部分布局和复用问题。

嗯,这些基础打牢了,后面讲组件进阶、自定义指令、组合式 API 时,你才会觉得顺。下一章咱们聊聊生命周期和模板引用,到时候见。


公众号:蓝海资料掘金营,微信deep3321