Files
2026-07-13 21:37:07 +08:00

7.8 KiB
Raw Permalink Blame History

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>

命名

文件: 使用 PascalCaseUserProfile.vue)或 kebab-caseuser-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 中的 camelCaseupdateItem)对应

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" />

v-model 修饰符文档

可复用模板

用于组件内类型安全的、带作用域的模板片段:

<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>

延迟 TeleportVue 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 中。