7.8 KiB
Vue 组件
使用 Composition API 与 <script setup> 的 Vue 3 组件模式。
快速参考
| 模式 | 语法 |
|---|---|
| Props(解构) | const { name = 'default' } = defineProps<{ name?: string }>() |
| Props(仅模板使用) | defineProps<{ name: string }>() |
| Emits | const emit = defineEmits<{ click: [id: number] }>() |
| 双向绑定 | const model = defineModel<string>() |
| Slots 简写 | <template #header> 而非 <template v-slot:header> |
命名
文件: 使用 PascalCase(UserProfile.vue)或 kebab-case(user-profile.vue)——保持风格一致
代码中的组件名: 始终使用 PascalCase
组合式命名: 从通用到具体——应为 SearchButtonClear.vue,而非 ClearSearchButton.vue
Props
在 script 中使用或需要默认值时,使用解构 + 默认值(Vue 3.5+):
const { count = 0, message = 'Hello' } = defineProps<{
count?: number
message?: string
required: boolean
}>()
// 直接使用——保持响应性
console.log(count + 1)
// ⚠️ 传递给 watcher/函数时,需包裹在 getter 中:
watch(() => count, (newVal) => { ... }) // ✅ 正确
watch(count, (newVal) => { ... }) // ❌ 无法正常工作
仅当 props 只在模板中使用时,才使用非解构方式:
defineProps<{ count: number }>()
// 模板中:{{ count }}
同名简写(Vue 3.4+): 使用 :count 而非 :count="count"
<MyComponent :count :user :items />
<!-- 等同于::count="count" :user="user" :items="items" -->
Emits
类型安全的事件定义:
const emit = defineEmits<{
update: [id: number, value: string] // 多个参数
close: [] // 无参数
}>()
// 使用方式
emit('update', 123, 'new value')
emit('close')
模板语法: kebab-case(@update-item)与 script 中的 camelCase(updateItem)对应
Slots
始终使用简写: <template #header> 而非 <template v-slot:header>
所有 slot 始终使用显式的 <template> 标签
<template>
<Card>
<template #header>
<h2>Title</h2>
</template>
<template #default>
Content
</template>
</Card>
</template>
defineModel() —— 双向绑定
替代手动编写 modelValue prop + update:modelValue emit。
基础用法
<script setup lang="ts">
const title = defineModel<string>()
</script>
<template>
<input v-model="title">
</template>
带选项
<script setup lang="ts">
const [title, modifiers] = defineModel<string>({
default: 'default value',
required: true,
get: (value) => value.trim(),
set: (value) => {
if (modifiers.capitalize) {
return value.charAt(0).toUpperCase() + value.slice(1)
}
return value
},
})
</script>
⚠️ 警告: 当使用 default 且父组件未提供值时,父组件与子组件可能出现不同步(父组件为 undefined,子组件为默认值)。请始终在父组件中提供匹配的默认值,或将 prop 设为必填。
使用 required: true 防止重复 emit:
// ❌ 没有 required——会触发两次 emit(先 undefined,后实际值)
const model = defineModel<Item>()
// ✅ 使用 required——只触发一次 emit
const model = defineModel<Item>({ required: true })
当 model 应始终有值时,请使用 required: true,以避免初始化阶段的重复 emit 问题。
多个 Model
默认假定 prop 名为 modelValue。对于多个绑定,请使用显式名称:
<script setup lang="ts">
const firstName = defineModel<string>('firstName')
const age = defineModel<number>('age')
</script>
<!-- 使用方式 -->
<UserForm v-model:first-name="user.firstName" v-model:age="user.age" />
可复用模板
用于组件内类型安全的、带作用域的模板片段:
<script setup lang="ts">
import { createReusableTemplate } from '@vueuse/core'
const [DefineItem, UseItem] = createReusableTemplate<{
item: SearchItem
icon: string
color?: 'red' | 'green' | 'blue'
}>()
</script>
<template>
<DefineItem v-slot="{ item, icon, color }">
<div :class="color">
<Icon :name="icon" />
{{ item.name }}
</div>
</DefineItem>
<!-- 多次复用 -->
<UseItem v-for="item in items" :key="item.id" :item :icon="getIcon(item)" />
</template>
模板引用(Vue 3.5+)
使用 useTemplateRef() 实现类型安全的模板引用,并支持 IDE 提示:
<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'
const input = useTemplateRef<HTMLInputElement>('my-input')
onMounted(() => {
input.value?.focus()
})
</script>
<template>
<input ref="my-input">
</template>
相比 ref() 的优势:
- 通过泛型实现类型安全
- 更好的 IDE 自动补全和重构支持
- 使用字符串字面量作为显式 ref 名称
动态 ref:
<script setup lang="ts">
const items = ref(['a', 'b', 'c'])
const itemRefs = useTemplateRef<HTMLElement>('item')
// 挂载后访问 ref
onMounted(() => {
console.log(itemRefs.value) // 元素数组
})
</script>
<template>
<div v-for="item in items" :key="item" ref="item">
{{ item }}
</div>
</template>
带泛型的组件 ref:
对于泛型组件,使用 vue-component-type-helpers 中的 ComponentExposed:
import type { ComponentExposed } from 'vue-component-type-helpers'
import MyGenericComponent from './MyGenericComponent.vue'
// 获取具有正确泛型类型的暴露方法/属性
const compRef = useTemplateRef<ComponentExposed<typeof MyGenericComponent>>('comp')
onMounted(() => {
compRef.value?.someExposedMethod() // 类型已推导!
})
安装:pnpm add -D vue-component-type-helpers
SSR 水合(Vue 3.5+)
抑制服务端/客户端值不一致导致的水合不匹配:
<template>
<!-- 仅客户端值 -->
<span data-allow-mismatch>{{ new Date().toLocaleString() }}</span>
<!-- 特定类型的不匹配 -->
<span data-allow-mismatch="text">{{ timestamp }}</span>
<span data-allow-mismatch="children">
<ClientOnly>...</ClientOnly>
</span>
<span data-allow-mismatch="style">...</span>
<span data-allow-mismatch="class">...</span>
<span data-allow-mismatch="attribute">...</span>
</template>
生成 SSR 稳定的 ID:
<script setup lang="ts">
import { useId } from 'vue'
const id = useId() // 在服务端/客户端渲染间保持稳定
</script>
<template>
<label :for="id">Name</label>
<input :id="id">
</template>
延迟 Teleport(Vue 3.5+)
传送至在同一渲染周期中稍后渲染的元素:
<template>
<!-- 先渲染此部分 -->
<Teleport defer to="#late-div">
<span>Deferred content</span>
</Teleport>
<!-- 此部分后渲染,但 Teleport 会等待 -->
<div id="late-div"></div>
</template>
若不使用 defer,传送至 #late-div 会失败,因为该元素尚不存在。
常见错误
将解构值与 const props = 一起使用:
// ❌ 错误
const props = defineProps<{ count: number }>()
const { count } = props // 失去响应性
忘记 TypeScript 类型:
// ❌ 错误
const emit = defineEmits(['update'])
// ✅ 正确
const emit = defineEmits<{ update: [id: number] }>()
组件超过 300 行: 拆分为更小的组件,或将逻辑提取到 composable 中。