<script setup>
<script setup>
是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。如果您同时使用 SFC 和组合式 API,则推荐使用此语法。它相较于普通的 <script>
语法提供了许多优势
- 更简洁的代码,更少的模板代码
- 使用纯 TypeScript 声明 props 和 emitted 事件的能力
- 更好的运行时性能(模板在相同的作用域内编译为渲染函数,没有中间代理)
- 更好的 IDE 类型推断性能(语言服务器从代码中提取类型的工作量更少)
基本语法
要启用此语法,请将 setup
属性添加到 <script>
块中
vue
<script setup>
console.log('hello script setup')
</script>
内部代码被编译为组件 setup()
函数的内容。这意味着与仅当组件首次导入时执行一次的普通 <script>
不同,<script setup>
内部的代码将在每次创建组件实例时执行。
顶级绑定暴露给模板
当使用 <script setup>
时,在 <script setup>
内部声明的任何顶级绑定(包括变量、函数声明和导入)都可以直接在模板中使用
vue
<script setup>
// variable
const msg = 'Hello!'
// functions
function log() {
console.log(msg)
}
</script>
<template>
<button @click="log">{{ msg }}</button>
</template>
导入也是以相同的方式暴露。这意味着您可以直接在模板表达式中使用导入的帮助函数,而无需通过 methods
选项暴露它
vue
<script setup>
import { capitalize } from './helpers'
</script>
<template>
<div>{{ capitalize('hello') }}</div>
</template>
响应性
响应式状态需要使用 响应式API 显式创建。与 setup()
函数返回的值类似,在模板中引用时,refs 会自动解包。
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
使用组件
<script setup>
作用域内的值也可以直接用作自定义组件标签名。
vue
<script setup>
import MyComponent from './MyComponent.vue'
</script>
<template>
<MyComponent />
</template>
将 MyComponent
视为一个变量引用。如果你使用过 JSX,这里的思维模型是类似的。其短横线等价形式 <my-component>
也在模板中有效 - 然而,为了保持一致性,强烈建议使用 PascalCase 组件标签。这也帮助区分于原生自定义元素。
动态组件
由于组件是以变量形式引用的,而不是在字符串键下注册,因此在 <script setup>
内使用动态组件时应使用动态的 :is
绑定。
vue
<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>
<template>
<component :is="Foo" />
<component :is="someCondition ? Foo : Bar" />
</template>
注意组件可以在三元表达式中作为变量使用。
递归组件
SFC 可以通过其文件名隐式引用自身。例如,一个名为 FooBar.vue
的文件可以在其模板中使用 <FooBar/>
来引用自身。
注意这比导入组件的优先级低。如果你有一个与组件推断名称冲突的命名导入,你可以给导入重命名。
js
import { FooBar as FooBarChild } from './components'
命名空间组件
你可以使用带点的组件标签,如 <Foo.Bar>
,来引用对象属性下的组件。当你从单个文件导入多个组件时,这很有用。
vue
<script setup>
import * as Form from './form-components'
</script>
<template>
<Form.Input>
<Form.Label>label</Form.Label>
</Form.Input>
</template>
使用自定义指令
全局注册的自定义指令正常工作。局部自定义指令不需要显式通过 <script setup>
注册,但它们必须遵循 vNameOfDirective
的命名方案。
vue
<script setup>
const vMyDirective = {
beforeMount: (el) => {
// do something with the element
}
}
</script>
<template>
<h1 v-my-directive>This is a Heading</h1>
</template>
如果你从其他地方导入指令,它可以被重命名为符合所需的命名方案。
vue
<script setup>
import { myDirective as vMyDirective } from './MyDirective.js'
</script>
defineProps() & defineEmits()
为了声明具有完整类型推断支持的选项,如 props
和 emits
,我们可以使用 defineProps
和 defineEmits
API,这些 API 在 <script setup>
内自动可用。
vue
<script setup>
const props = defineProps({
foo: String
})
const emit = defineEmits(['change', 'delete'])
// setup code
</script>
defineProps
和defineEmits
是仅能在<script setup>
内使用的 编译器宏。它们不需要导入,并且在<script setup>
被处理时会编译掉。defineProps
接受与props
选项相同的值,而defineEmits
接受与emits
选项相同的值。defineProps
和defineEmits
提供基于传递的选项的正确类型推断。传递给
defineProps
和defineEmits
的选项将被提升到模块作用域中。因此,选项不能引用在 setup 作用域中声明的局部变量。这样做会导致编译错误。然而,它 可以 引用导入的绑定,因为它们也在模块作用域中。
仅类型 props/emit 声明
可以使用纯类型语法通过将字面量类型参数传递给 defineProps
或 defineEmits
来声明 props 和 emits。
ts
const props = defineProps<{
foo: string
bar?: number
}>()
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
// 3.3+: alternative, more succinct syntax
const emit = defineEmits<{
change: [id: number] // named tuple syntax
update: [value: string]
}>()
defineProps
或defineEmits
只能使用运行时声明或类型声明中的一种。同时使用两种声明会导致编译错误。当使用类型声明时,从静态分析自动生成等效的运行时声明,以消除双重声明的需求,并确保正确的运行时行为。
在开发模式下,编译器将尝试从类型推断相应的运行时验证。例如,此处从
foo: string
类型推断出foo: String
。如果类型是导入类型的引用,由于编译器没有外部文件的信息,推断结果将是foo: null
(等同于any
类型)。在生产模式下,编译器将生成数组格式声明以减少包大小(这里的属性将编译为
['foo', 'bar']
)
在版本 3.2 及以下中,
defineProps()
的泛型类型参数仅限于类型字面量或局部接口的引用。此限制已在 3.3 中解决。Vue 的最新版本支持在类型参数位置引用导入的有限复杂类型。然而,由于类型到运行时的转换仍然是基于 AST 的,因此不支持需要实际类型分析的一些复杂类型,例如条件类型。您可以用于单个属性的类型条件类型,但不能用于整个属性对象。
响应式属性解构
在 Vue 3.5 及以上版本中,从 defineProps
的返回值解构的变量是响应式的。Vue 的编译器会自动在相同的 <script setup>
块中访问从 defineProps
解构的变量时,将 props.
预先添加到代码中。
ts
const { foo } = defineProps(['foo'])
watchEffect(() => {
// runs only once before 3.5
// re-runs when the "foo" prop changes in 3.5+
console.log(foo)
})
上面的代码编译为以下等效代码
js
const props = defineProps(['foo'])
watchEffect(() => {
// `foo` transformed to `props.foo` by the compiler
console.log(props.foo)
})
此外,您可以使用 JavaScript 的原生默认值语法为属性声明默认值。这在使用基于类型的属性声明时特别有用。
ts
interface Props {
msg?: string
labels?: string[]
}
const { msg = 'hello', labels = ['one', 'two'] } = defineProps<Props>()
使用类型声明时的默认属性值
在 3.5 及以上版本中,可以使用响应式属性解构自然地声明默认值。但在 3.4 及以下版本中,响应式属性解构默认未启用。为了使用基于类型的声明声明属性默认值,需要 withDefaults
编译器宏。
ts
interface Props {
msg?: string
labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
labels: () => ['one', 'two']
})
这将编译为等效的运行时属性 default
选项。此外,withDefaults
辅助程序为默认值提供类型检查,并确保返回的 props
类型已删除具有默认值声明的属性的必需标志。
INFO
请注意,当使用 withDefaults
时,对于可变引用类型(如数组或对象)的默认值应使用函数包装,以避免意外修改和外部副作用。这确保了每个组件实例都得到其自己的默认值副本。使用解构的默认值时不需要这样做。
defineModel()
- 仅适用于 3.4+
此宏可用于声明一个双向绑定属性,可以从父组件通过 v-model
使用。在 组件 v-model
指南 中也讨论了示例用法。
在底层,此宏声明了一个模型属性及其相应的值更新事件。如果第一个参数是字面量字符串,则将其用作属性名;否则,属性名将默认为"modelValue"
。在两种情况下,您还可以传递一个额外的对象,其中可以包含属性的选项和模型引用的值转换选项。
js
// declares "modelValue" prop, consumed by parent via v-model
const model = defineModel()
// OR: declares "modelValue" prop with options
const model = defineModel({ type: String })
// emits "update:modelValue" when mutated
model.value = 'hello'
// declares "count" prop, consumed by parent via v-model:count
const count = defineModel('count')
// OR: declares "count" prop with options
const count = defineModel('count', { type: Number, default: 0 })
function inc() {
// emits "update:count" when mutated
count.value++
}
警告
如果您为defineModel
属性指定了default
值,并且您没有从父组件为该属性提供任何值,这可能会导致父组件和子组件之间的不同步。在下面的示例中,父组件的myRef
是未定义的,但子组件的model
是1
js
// child component:
const model = defineModel({ default: 1 })
// parent component:
const myRef = ref()
html
<Child v-model="myRef"></Child>
修饰符和转换器
要访问与v-model
指令一起使用的修饰符,我们可以像这样解构defineModel()
的返回值
js
const [modelValue, modelModifiers] = defineModel()
// corresponds to v-model.trim
if (modelModifiers.trim) {
// ...
}
当存在修饰符时,我们可能需要在读取或将其同步回父组件时转换值。我们可以通过使用get
和set
转换器选项来实现这一点
js
const [modelValue, modelModifiers] = defineModel({
// get() omitted as it is not needed here
set(value) {
// if the .trim modifier is used, return trimmed value
if (modelModifiers.trim) {
return value.trim()
}
// otherwise, return the value as-is
return value
}
})
与TypeScript一起使用
与defineProps
和defineEmits
类似,defineModel
也可以接收类型参数来指定模型值和修饰符的类型
ts
const modelValue = defineModel<string>()
// ^? Ref<string | undefined>
// default model with options, required removes possible undefined values
const modelValue = defineModel<string>({ required: true })
// ^? Ref<string>
const [modelValue, modifiers] = defineModel<string, 'trim' | 'uppercase'>()
// ^? Record<'trim' | 'uppercase', true | undefined>
defineExpose()
使用<script setup>
的组件默认是封闭的 - 即通过模板引用或$parent
链检索到的组件的公共实例将不会暴露<script setup>
内部声明的任何绑定。
要显式在<script setup>
组件中暴露属性,请使用defineExpose
编译器宏
vue
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
defineExpose({
a,
b
})
</script>
当父组件通过模板引用获取此组件的实例时,检索到的实例将是{ a: number, b: number }
的形式(引用就像在正常实例上一样自动展开)。
defineOptions()
- 仅在3.3+版本中支持
此宏可用于在<script setup>
中直接声明组件选项,而无需使用单独的<script>
块
vue
<script setup>
defineOptions({
inheritAttrs: false,
customOptions: {
/* ... */
}
})
</script>
- 这是一个宏。选项将被提升到模块作用域,并且不能访问
<script setup>
中不是字面量常量的本地变量
defineSlots()
- 仅在3.3+版本中支持
此宏可用于为IDE提供类型提示,以进行插槽名称和属性类型检查。
defineSlots()
只接受一个类型参数,不接受运行时参数。类型参数应该是类型文字,其中属性键是插槽名称,值类型是插槽函数。函数的第一个参数是插槽期望接收的属性,其类型将用于模板中的插槽属性。返回类型目前被忽略,可以是any
,但我们可能会在将来利用它进行插槽内容检查。
它还返回slots
对象,该对象与在setup上下文上公开的slots
对象或由useSlots()
返回的对象等效。
vue
<script setup lang="ts">
const slots = defineSlots<{
default(props: { msg: string }): any
}>()
</script>
useSlots()
& useAttrs()
在 `