项目文件夹

文件
2026-07-13 21:37:07 +08:00

324 行
7.8 KiB
Markdown

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# 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+):**
```ts
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 只在模板中使用时,才使用非解构方式:**
```ts
defineProps<{ count: number }>()
// 模板中:{{ count }}
```
**同名简写(Vue 3.4+):** 使用 `:count` 而非 `:count="count"`
```vue
<MyComponent :count :user :items />
<!-- 等同于:count="count" :user="user" :items="items" -->
```
[响应式解构文档](https://vuejs.org/guide/components/props#reactive-props-destructure)
## Emits
类型安全的事件定义:
```ts
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>` 标签**
```vue
<template>
<Card>
<template #header>
<h2>Title</h2>
</template>
<template #default>
Content
</template>
</Card>
</template>
```
## defineModel() —— 双向绑定
替代手动编写 `modelValue` prop + `update:modelValue` emit。
### 基础用法
```vue
<script setup lang="ts">
const title = defineModel<string>()
</script>
<template>
<input v-model="title">
</template>
```
### 带选项
```vue
<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**
```ts
// ❌ 没有 required——会触发两次 emit(先 undefined,后实际值)
const model = defineModel<Item>()
// ✅ 使用 required——只触发一次 emit
const model = defineModel<Item>({ required: true })
```
当 model 应始终有值时,请使用 `required: true`,以避免初始化阶段的重复 emit 问题。
### 多个 Model
默认假定 prop 名为 `modelValue`。对于多个绑定,请使用显式名称:
```vue
<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" />
```
[v-model 修饰符文档](https://vuejs.org/guide/components/v-model#handling-v-model-modifiers)
## 可复用模板
用于组件内类型安全的、带作用域的模板片段:
```vue
<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 提示:
```vue
<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**
```vue
<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`
```ts
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+
**抑制服务端/客户端值不一致导致的水合不匹配:**
```vue
<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**
```vue
<script setup lang="ts">
import { useId } from 'vue'
const id = useId() // 在服务端/客户端渲染间保持稳定
</script>
<template>
<label :for="id">Name</label>
<input :id="id">
</template>
```
## 延迟 TeleportVue 3.5+
传送至在同一渲染周期中稍后渲染的元素:
```vue
<template>
<!-- 先渲染此部分 -->
<Teleport defer to="#late-div">
<span>Deferred content</span>
</Teleport>
<!-- 此部分后渲染 Teleport 会等待 -->
<div id="late-div"></div>
</template>
```
若不使用 `defer`,传送至 `#late-div` 会失败,因为该元素尚不存在。
## 常见错误
**将解构值与 `const props =` 一起使用:**
```ts
// ❌ 错误
const props = defineProps<{ count: number }>()
const { count } = props // 失去响应性
```
**忘记 TypeScript 类型:**
```ts
// ❌ 错误
const emit = defineEmits(['update'])
// ✅ 正确
const emit = defineEmits<{ update: [id: number] }>()
```
**组件超过 300 行:** 拆分为更小的组件,或将逻辑提取到 composable 中。