跳转到内容

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

为了声明具有完整类型推断支持的选项,如 propsemits,我们可以使用 definePropsdefineEmits API,这些 API 在 <script setup> 内自动可用。

vue
<script setup>
const props = defineProps({
  foo: String
})

const emit = defineEmits(['change', 'delete'])
// setup code
</script>
  • definePropsdefineEmits 是仅能在 <script setup> 内使用的 编译器宏。它们不需要导入,并且在 <script setup> 被处理时会编译掉。

  • defineProps 接受与 props 选项相同的值,而 defineEmits 接受与 emits 选项相同的值。

  • definePropsdefineEmits 提供基于传递的选项的正确类型推断。

  • 传递给 definePropsdefineEmits 的选项将被提升到模块作用域中。因此,选项不能引用在 setup 作用域中声明的局部变量。这样做会导致编译错误。然而,它 可以 引用导入的绑定,因为它们也在模块作用域中。

仅类型 props/emit 声明

可以使用纯类型语法通过将字面量类型参数传递给 definePropsdefineEmits 来声明 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]
}>()
  • definePropsdefineEmits 只能使用运行时声明或类型声明中的一种。同时使用两种声明会导致编译错误。

  • 当使用类型声明时,从静态分析自动生成等效的运行时声明,以消除双重声明的需求,并确保正确的运行时行为。

    • 在开发模式下,编译器将尝试从类型推断相应的运行时验证。例如,此处从 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) {
  // ...
}

当存在修饰符时,我们可能需要在读取或将其同步回父组件时转换值。我们可以通过使用getset转换器选项来实现这一点

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一起使用

definePropsdefineEmits类似,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()

在 `