Vue3组件

Vue 3 组件完全指南

本文档从零开始讲解 Vue 3 的组件:什么是组件、如何定义和使用、如何通过 props 传数据、如何通过事件与父组件通信、以及插槽、v-model、多根节点等,配有大量示例,适合新手系统学习。


一、什么是组件?

1.1 生活中的类比

可以把组件想象成积木块:每块积木有自己的形状和功能,可以反复使用、随意组合。
在 Vue 里,组件就是一块“可复用的界面 + 逻辑”的单元:例如一个按钮、一张卡片、一个表单输入框。
你把多个组件拼在一起,就得到一整页。

1.2 为什么要用组件?

  • 复用:同一个“按钮样式 + 点击逻辑”在多处使用,只写一次组件即可。
  • 拆分:页面拆成多个小组件,结构清晰,便于维护。
  • 协作:不同人负责不同组件,减少冲突。

1.3 组件长什么样?

在 Vue 3 中,一个组件通常对应一个 .vue 文件,称为单文件组件(SFC)
文件里一般包含三块:

  1. <template>:HTML 结构(可写 Vue 模板语法)。
  2. <script>:逻辑(数据、方法、生命周期等)。
  3. <style>:样式(可加 scoped 只影响当前组件)。
<!-- HelloWorld.vue -->
<template>
  <div>
    <p>你好,{{ name }}!</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'
const name = ref('Vue 3')
</script>

<style scoped>
p {
  color: blue;
}
</style>

上面就是一个最简单的组件:有结构、有数据、有样式。
下面从“如何使用组件”开始,再讲“如何通过 props 和事件与外界通信”。


二、如何使用组件?

使用组件分两步:引入在模板里当标签用

2.1 在别的组件里使用(局部注册)

  1. import 把组件文件引进来。
  2. <script setup> 里,import 进来的组件会自动在当前组件的模板里可用,不用再写 components 注册(这是 <script setup> 的便利之处)。
  3. <template> 里用标签的形式写组件,标签名用帕斯卡命名(PascalCase)短横线(kebab-case)都可以,Vue 会识别。
<!-- App.vue -->
<template>
  <div>
    <h1>我的应用</h1>
    <HelloWorld />
    <!-- 也可写成 <hello-world /> -->
  </div>
</template>

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

注意:

  • 路径 ./components/HelloWorld.vue 要按你实际文件位置改。
  • 组件名推荐用 PascalCase(如 HelloWorld),在模板里可以写成 <HelloWorld /><hello-world />

2.2 全局注册(可选)

若希望整个应用很多地方都用同一个组件,可以在入口文件里全局注册,这样就不用在每个页面里 import 了。

// 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)  // 全局注册,任意模板里可直接用 <MyButton />
app.mount('#app')

一般建议:只有真的到处都要用的组件才全局注册,其它用局部 import,更清晰。


三、Props:父组件向子组件传数据

父组件把数据通过属性的形式传给子组件,子组件用 props 接收。
这样同一个组件在不同地方可以显示不同内容(例如同一个“卡片”组件,显示不同标题和正文)。

3.1 子组件:声明 props

在子组件的 <script setup> 里用 defineProps 声明“我接收哪些属性、类型是什么、是否必填、默认值是什么”。
defineProps 是编译器宏,不需要 import,直接写即可。

只写属性名(简单写法):

<!-- Card.vue -->
<template>
  <div class="card">
    <h3>{{ title }}</h3>
    <p>{{ content }}</p>
  </div>
</template>

<script setup>
defineProps(['title', 'content'])
</script>

<style scoped>
.card {
  border: 1px solid #ddd;
  padding: 16px;
  border-radius: 8px;
}
</style>

带类型和默认值(推荐):

<script setup>
defineProps({
  title: {
    type: String,
    required: true
  },
  content: {
    type: String,
    default: ''
  },
  count: {
    type: Number,
    default: 0
  }
})
</script>

这样父组件传过来的 titlecontentcount 在模板里可以直接用(只读,不要改)。

3.2 父组件:传 props

在父组件里,把子组件当标签用,通过属性把数据传进去。
属性名用 kebab-case(如 user-name)或 camelCase(如 userName)都可以,Vue 会对应到 props 里的 camelCase 名。

<!-- App.vue -->
<template>
  <div>
    <Card title="第一张卡片" content="这是内容" />
    <Card :title="dynamicTitle" :content="dynamicContent" />
    <Card title="只有标题" />
  </div>
</template>

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

const dynamicTitle = ref('动态标题')
const dynamicContent = ref('动态内容')
</script>
  • 静态传字符串title="第一张卡片"
  • 动态传变量:用 :title="dynamicTitle":v-bind,后面是表达式)。
  • 没传的 prop 会使用子组件里定义的 default

3.3 单向数据流:不要改 props

Props 是只读的。 父组件传过来的值,子组件不要直接修改(如 props.title = 'x')。
若子组件要根据 prop 做计算或内部状态,应该:

  • 计算属性 基于 prop 派生新值;或
  • ref/reactive 在组件内拷贝一份,改自己的拷贝。
<script setup>
const props = defineProps({ initialCount: { type: Number, default: 0 } })
// 若需要内部可变的“计数”,用 ref 拷贝
const count = ref(props.initialCount)
</script>

四、事件:子组件向父组件“汇报”

子组件通过 触发事件 把“发生了某件事”告诉父组件,父组件监听事件并执行自己的逻辑。
这样点击子组件里的按钮,可以让父组件去请求接口、改状态等。

4.1 子组件:定义并触发事件

<script setup> 里用 defineEmits 声明“我会触发哪些事件”,然后在需要的地方调用 emit('事件名', 参数)

<!-- MyButton.vue -->
<template>
  <button @click="handleClick">点击我</button>
</template>

<script setup>
const emit = defineEmits(['click'])
function handleClick() {
  emit('click')
}
</script>

若需要传参给父组件,把参数放在 emit 的第二个及之后的参数里:

<script setup>
const emit = defineEmits(['submit'])
function submit() {
  emit('submit', { name: '小明', age: 18 })
}
</script>

4.2 父组件:监听事件

在父组件里,给子组件标签上写 @事件名="处理函数",和监听原生 DOM 事件一样。

<!-- App.vue -->
<template>
  <div>
    <MyButton @click="onClick" />
    <MyForm @submit="onSubmit" />
  </div>
</template>

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

function onClick() {
  console.log('按钮被点了')
}
function onSubmit(data) {
  console.log('提交的数据', data)
}
</script>

这样就把“子组件发生的事”和“父组件的逻辑”连起来了。


五、组件上的 v-model(双向绑定)

我们不仅希望父传子(props),还希望子组件内部变化能同步回父组件(例如输入框的值)。
除了用“prop + 事件”自己实现,还可以用 v-model,让子组件支持 v-model="父组件的变量"

5.1 子组件:接收 modelValue 并触发 update:modelValue

在 Vue 3 中,组件上的 v-model 默认对应:

  • 名为 modelValue 的 prop;
  • 名为 update:modelValue 的事件。

子组件内部:用 props 接收 modelValue,在值变化时 emit('update:modelValue', 新值)

<!-- CustomInput.vue -->
<template>
  <input
    :value="modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

<script setup>
defineProps({
  modelValue: {
    type: String,
    default: ''
  }
})
const emit = defineEmits(['update:modelValue'])
</script>

5.2 父组件:直接 v-model

<template>
  <CustomInput v-model="username" />
  <p>你输入了:{{ username }}</p>
</template>

<script setup>
import { ref } from 'vue'
import CustomInput from './components/CustomInput.vue'
const username = ref('')
</script>

这样父组件的 username 和子组件输入框就实现了双向绑定
若需要多个“双向绑定”(例如一个组件同时绑定“用户名”和“密码”),可以用多个 v-model,见下一小节。

5.3 多个 v-model:指定名字

给 v-model 加“名字”:v-model:名字=”变量”
子组件则接收 名字 这个 prop,并触发 update:名字

<!-- UserForm.vue -->
<template>
  <div>
    <input :value="username" @input="emit('update:username', $event.target.value)" />
    <input :value="age" @input="emit('update:age', Number($event.target.value))" />
  </div>
</template>

<script setup>
defineProps({
  username: String,
  age: Number
})
const emit = defineEmits(['update:username', 'update:age'])
</script>
<!-- 父组件 -->
<template>
  <UserForm v-model:username="user.name" v-model:age="user.age" />
</template>

<script setup>
import { reactive } from 'vue'
const user = reactive({ name: '', age: 0 })
</script>

六、插槽:父组件往子组件里“塞内容”

子组件可以在模板里留插槽<slot>),父组件在使用子组件时,把一段内容(HTML 或其它组件)塞进插槽里。
这样同一个“外壳”组件可以显示不同内容。
插槽分为:默认插槽、具名插槽、作用域插槽。这里只做简要说明,详细用法见《Vue3插槽.md》。

子组件留一个洞:

<!-- Card.vue -->
<template>
  <div class="card">
    <div class="card-body">
      <slot></slot>
    </div>
  </div>
</template>

父组件塞内容:

<Card>
  <p>任意内容、按钮、图片都可以</p>
</Card>

七、组件的根节点:单根与多根(Vue 3 支持多根)

  • Vue 2<template> 里必须有一个根元素
  • Vue 3<template> 里可以有多个根元素(多个根节点),称为 Fragment
<!-- Vue 3 允许这样写 -->
<template>
  <header>头部</header>
  <main>主体</main>
  <footer>底部</footer>
</template>

多根时,属性、事件只能写在某一个根上,不会自动继承到所有根;需要时请用一个包裹用的根(如 <div>)或分别写在每个根上。


八、组件名与使用方式

  • 定义时:文件名和组件名推荐用 PascalCase,如 UserCard.vue
  • 模板里使用
    • PascalCase<UserCard />
    • kebab-case<user-card />
      Vue 会把两者都识别为同一个组件。
  • DOM 里(非 Vue 模板,如直接写 HTML),只能写小写短横线,所以最终渲染出来的标签会是小写(如 <user-card>)。

发表评论