单文件组件<script setup>

单文件组件主要是指 .vue 结尾的文件,其内容主要由 <template> 标签、<script> 标签、<style> 标签等构成,在使用 <script> 标签时可以直接配置 setup 属性来标识使用组合式 API,相比于普通的 <script> 语法,它具有更多优势:

  • 更少的样板内容,更简洁的代码。

  • 能够使用纯 TypeScript 声明 props 和抛出事件。

  • 更好的运行时性能(其模板会被编译成与其同一作用域的渲染函数,没有任何的中间代理)。

  • 更好的 IDE 类型推断性能(减少语言服务器从代码中抽离类型的工作)。

使用 <script setup> 方法的基本语法如示例代码 4-8-1 所示。

示例代码4-8-1 <script setup> 基本用法1
<script setup>
console.log('hello script setup')
</script>

<script setup> 中的代码会被编译成组件 setup() 方法中的内容,这意味着与普通的 <script> 只在组件被首次引入的时候执行一次不同,<script setup> 中的 代码会在每次组件实例被创建的时候执行

在 <script setup> 顶层的声明(包括变量、函数以及 import 引入的方法或者组件)都能在模板 <template> 中直接使用,相当于自动返回了这些内容,如示例代码 4-8-2 所示。

示例代码 4-8-2 <script setup> 基本用法2
<script setup>
    // import引入方法
    import { capitalize } from './helpers'
    // 组件
    import MyComponent from './MyComponent.vue'
    // 变量
    const msg = 'Hello!'
    // 响应式变量
    const count = ref(0)
    // 函数
    function add() {
        count.value++
    }
</script>
<template>
    <div @click="add">{{ msg }}</div>
    <div>{{ capitalize('hello') }}</div>
    <MyComponent />
</template>

从上面的代码中可以看出,使用 <script setup> 这种方式使代码更加简洁了,但是注意不需要的变量或者组件以及不需要的响应式逻辑就不要有这部分代码了。

如果不想默认全部变量都直接暴露给 <template> 使用,而是控制需要返回哪些数据给 <template> 使用,可以使用 defineExpose 方法来明确要暴露的属性,其用法如示例代码 4-8-3 所示。

<script setup>
    import { ref } from 'vue'
    const a = 1
    const b = ref(2)
    // defineExpose不需要引入,直接使用
    defineExpose({a,b})
</script>
<template>
    <h1>{{a}},{{b}}</h1>
</template>

注意,defineExpose 方法不需要引入,可以直接使用。

setup 方法接收两个参数,一个是 props,可以接收到父组件传递的数据;另一个是 context 对象,它暴露组件的三个属性,分别是 attrsslotsemit,来实现一些组件数据的传递。因此在 <script setup> 中可以使用 definePropsdefineEmits 以及 useSlotsuseAttrs 来实现等同于上述参数的功能,如示例代码 4-8-4 所示。

示例代码 4-8-4 <script setup> 基本用法4
<script setup>
    // defineProps、 defineEmits不需要引入
    const props = defineProps({
        foo: String
    })
    const emit = defineEmits(['change', 'delete'])
    // useSlots、 useAttrs需要import引入
    import { useSlots, useAttrs } from 'vue'
    const slots = useSlots()
    const attrs = useAttrs()
</script>

在使用这些方法时,需要注意以下几点:

  • definePropsdefineEmits<script setup> 使用时不需要导入,直接使用,useSlotsuseAttrs 则需要导入使用。

  • defineProps 接收与 props 参数相同的值,defineEmits 也接收与 setupContext.emits 选项相同的值,useSlotsuseAttrs 在调用时会返回与 setupContext.slotssetupContext.attrs 等价的值。

  • 传入 definePropsdefineEmits 的选项会从 setup 中提升到模块的范围。因此,传入的选项不能引用在 setup 范围中声明的局部变量,这样做会引起编译错误,但是它可以引用导入的绑定,因为导入的绑定也在模块范围内。

如以下代码所示,defineEmits 使用局部变量会报错:

<script setup>
const str = 'change' // 局部变量
// 传入str变量会报错
const emit = defineEmits([str, 'delete'])
</script>

报错信息如图 4-1 所示。

image 2024 02 22 20 04 23 668
Figure 1. 图4-1 报错信息

<script setup> 可以和普通的 <script> 一起使用。普通的 <script> 在有以下需要的情况下或许会被使用到:

  • 无法在 <script setup> 声明的选项,例如一些通过插件启用的自定义选项。

  • 声明命名导出。

  • 运行副作用或者创建只需要执行一次的对象。

代码如下:

<script>
    // 普通 <script>, 在模块范围下执行(只执行一次)
    runSideEffectOnce()
    // 声明额外的选项
    export default {
        customOptions: {}
    }
</script>
<script setup>
    // 在 setup() 作用域中执行 (对每个实例皆如此)
</script>

注意,由于模块执行语义的差异,<script setup> 中的代码依赖单文件组件的上下文。当将其移动到外部的 .js 或者 .ts 文件中的时候,对于开发者和工具来说都会感到混乱。因而 <script setup> 不能和 <script src> 属性一起使用。

<script setup> 使得组合式 API 代码看起来简单了很多,开发效率大大提高,在 Vue 3 且使用组合式 API 的项目中,非常推荐使用。

defineProps() 和 defineEmits()

Vue 3 中,defineProps()defineEmits() 是 Composition API 提供的两个辅助函数,主要用于在 setup() 函数中定义组件的 propsemits这两个函数可以帮助简化组件的定义,并提供类型推导支持,尤其在 TypeScript 中非常有用。

defineProps()

defineProps() 用于定义组件的 props,这是 Vue 3 中 Composition API 的一个重要功能。它允许在 setup() 函数中声明和访问 props,从而避免了在 dataprops 选项配置中显式声明它们。

const props = defineProps([propsOptions]);
  • propsOptions: 可以是一个对象,定义 props 的类型和默认值,或者是一个类型声明(TypeScript 中的接口)。

  • defineProps() 会根据传入的 props 配置,自动将这些 props 变成响应式数据,使它们可以在 setup() 中访问。

  • 如果使用 TypeScript,它支持类型推导和类型检查。

基本用法
<script setup>
import { defineProps } from 'vue';

// 定义 props
const props = defineProps({
  message: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0
  }
});

// 直接访问 props
console.log(props.message);  // 访问 message prop
</script>

在上面的例子中,defineProps() 用于定义 messagecount 两个 props。message 是必需的,而 count 有默认值 0

与 TypeScript 配合使用
<script setup lang="ts">
import { defineProps } from 'vue';

// 使用 TypeScript 定义 prop 类型
interface Props {
  message: string;
  count: number;
}

const props = defineProps<Props>();

console.log(props.message); // 正确推导类型
</script>

选项:

  • 类型定义:可以传入一个对象(typerequireddefault 等),也可以使用 TypeScript 的接口进行类型声明。

  • 默认值:可以为每个 prop 定义默认值(通过 default)。

  • 类型校验:支持 type 属性来定义 prop 的类型,还可以结合 TypeScript 的类型系统。

defineEmits()

defineEmits() 用于定义组件的 事件emits)。在 Vue 3 中,事件是父子组件之间通信的重要方式。defineEmits() 使得在 Composition API 中定义事件变得更简单,并且可以提供类型支持。

const emit = defineEmits([emitsOptions]);
  • emitsOptions: 可以是一个数组或对象。

    • 数组:列出所有可能触发的事件名称。

    • 对象:用于提供更详细的事件类型和验证。

  • defineEmits() 会帮助你在 setup() 中定义组件可能触发的事件。

  • 支持 TypeScript 类型推导。

基本用法
<script setup>
import { defineEmits } from 'vue';

// 定义 emits
const emit = defineEmits();

// 触发事件
const triggerEvent = () => {
  emit('update', 42); // 触发 'update' 事件,并传递数据 42
};
</script>

在这个例子中,我们使用 defineEmits() 来定义事件,但并未指定事件名称。在组件内部可以通过 emit('事件名', 数据) 来触发事件。

带类型的事件定义
<script setup lang="ts">
import { defineEmits } from 'vue';

interface MyEmits {
  (event: 'update', value: number): void;
  (event: 'delete', id: string): void;
}

const emit = defineEmits<MyEmits>();

// 触发事件
const triggerUpdate = () => {
  emit('update', 42); // 触发 'update' 事件,传递数字
};

const triggerDelete = () => {
  emit('delete', 'item-1'); // 触发 'delete' 事件,传递字符串
};
</script>

在这个例子中,我们使用 TypeScript 接口 MyEmits 来定义组件可能触发的事件及其参数类型。通过 defineEmits<MyEmits>(),我们为事件添加了类型推导和验证。

列出事件名称(数组语法)
<script setup>
import { defineEmits } from 'vue';

// 定义 emits 事件
const emit = defineEmits(['update', 'delete']);

// 触发事件
const triggerUpdate = () => {
  emit('update', 42); // 触发 'update' 事件
};
</script>

在这个例子中,defineEmits() 接收一个数组,列出了组件能够触发的所有事件名。这种方式适用于不需要事件参数类型的场景。

综合示例
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue';

// 定义 props 类型
interface Props {
  message: string;
  count: number;
}

const props = defineProps<Props>();

// 定义 emits 事件
interface MyEmits {
  (event: 'update', value: number): void;
}

const emit = defineEmits<MyEmits>();

// 触发事件
const increment = () => {
  emit('update', props.count + 1);
};
</script>

<template>
  <div>
    <p>{{ props.message }}</p>
    <p>{{ props.count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

总结:

特性 defineProps() defineEmits()

功能

用于定义和访问组件的 props

用于定义和触发组件的事件

返回值

返回 props 对象,可以访问传递给组件的所有 props

返回 emit 函数,允许触发事件并传递参数

类型支持

支持类型推导,可以传入对象或 TypeScript 类型定义

支持类型推导,允许定义事件名和参数类型

用法

const props = defineProps({ name: { type: String, required: true } })

const emit = defineEmits(['update', 'delete']) 或 const emit = defineEmits<MyEmits>()

默认值与校验

可以为 props 定义默认值、类型和必需属性

可通过类型定义来校验触发的事件和传递的参数

defineModel()

这个宏可以用来声明一个双向绑定 prop,通过父组件的 v-model 来使用。组件 v-model 指南中也讨论了示例用法。

在底层,这个宏声明了一个 model prop 和一个相应的值更新事件。如果第一个参数是一个字符串字面量,它将被用作 prop 名称;否则,prop 名称将默认为 "modelValue"。在这两种情况下,你都可以再传递一个额外的对象,它可以包含 prop 的选项和 model ref 的值转换选项。

// 声明 "modelValue" prop,由父组件通过 v-model 使用
const model = defineModel()
// 或者:声明带选项的 "modelValue" prop
const model = defineModel({ type: String })

// 在被修改时,触发 "update:modelValue" 事件
model.value = "hello"

// 声明 "count" prop,由父组件通过 v-model:count 使用
const count = defineModel("count")
// 或者:声明带选项的 "count" prop
const count = defineModel("count", { type: Number, default: 0 })

function inc() {
  // 在被修改时,触发 "update:count" 事件
  count.value++
}

修饰符和转换器

为了获取 v-model 指令使用的修饰符,我们可以像这样解构 defineModel() 的返回值:

const [modelValue, modelModifiers] = defineModel()

// 对应 v-model.trim
if (modelModifiers.trim) {
  // ...
}

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

const [modelValue, modelModifiers] = defineModel({
  // get() 省略了,因为这里不需要它
  set(value) {
    // 如果使用了 .trim 修饰符,则返回裁剪过后的值
    if (modelModifiers.trim) {
      return value.trim()
    }
    // 否则,原样返回
    return value
  }
})

在 TypeScript 中使用

definePropsdefineEmits 一样,defineModel 也可以接收类型参数来指定 model 值和修饰符的类型:

const modelValue = defineModel<string>()
//    ^? Ref<string | undefined>

// 用带有选项的默认 model,设置 required 去掉了可能的 undefined 值
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> 中声明的绑定。

可以通过 defineExpose 编译器宏来显式指定在 <script setup> 组件中要暴露出去的属性:

<script setup>
import { ref } from 'vue'

const a = 1
const b = ref(2)

defineExpose({
  a,
  b
})
</script>

当父组件通过模板引用的方式获取到当前组件的实例,获取到的实例会像这样 { a: number, b: number } (ref 会和在普通实例中一样被自动解包)

defineOptions()

问题:用了 <script setup> 后,就无法添加与其平级的属性了,比如定义组件的 name 或其他自定义的属性。

为了解决这一问题,引入了 definePropsdefineEmits 这两个宏,但这只解决了 propsemits 这两个属性。如果要定义其他的平级属性,还是得回到最原始的用法—​就再添加一个普通的 <script> 标签。这样就会存在两个 <script> 标签,让人无法接受。

所以在 Vue 3.3 中新引入了 defineOptions 宏。顾名思义,主要是用来定义 Option API 的选项。可以用 defineOptions 定义任意的选项,propsemitsexposeslots 除外(因为这些可以使用 defineXXX 来做到)

defineOptions 是全局的宏,无需导入。

这个宏可以用来直接在 <script setup> 中声明 组件选项,而不必使用单独的 <script> 块:

<script setup>
defineOptions({
    name: 'Foo',//组件重命名
    inheritAttrs: false,
    customOptions: {
        /* ... */
    }
})
</script>

这是一个宏定义,选项将会被提升到模块作用域中,无法访问 <script setup> 中不是字面常数的局部变量。

defineSlots()

这个宏可以用于为 IDE 提供插槽名称和 props 类型检查的类型提示。

defineSlots() 只接受类型参数,没有运行时参数。类型参数应该是一个类型字面量,其中属性键是插槽名称,值类型是插槽函数。函数的第一个参数是插槽期望接收的 props,其类型将用于模板中的插槽 props。返回类型目前被忽略,可以是 any,但我们将来可能会利用它来检查插槽内容。

它还返回 slots 对象,该对象等同于在 setup 上下文中暴露或由 useSlots() 返回的 slots 对象。

<script setup lang="ts">
const slots = defineSlots<{
  default(props: { msg: string }): any
}>()
</script>

useSlots() 和 useAttrs()

<script setup> 使用 slotsattrs 的情况应该是相对来说较为罕见的,因为可以在模板中直接通过 $slots$attrs 来访问它们。在你的确需要使用它们的罕见场景中,可以分别用 useSlotsuseAttrs 两个辅助函数:

<script setup>
import { useSlots, useAttrs } from 'vue'

const slots = useSlots()
const attrs = useAttrs()
</script>

useSlotsuseAttrs 是真实的运行时函数,它的返回与 setupContext.slotssetupContext.attrs 等价。它们同样也能在普通的组合式 API 中使用。

useSlots()

useSlots() 返回的对象:

  • 如果没有插槽内容,slots 对象会为空。

  • slots 对象的每个属性对应一个插槽的内容。默认插槽没有名字,可以通过 slots.default 访问。

  • 如果使用了命名插槽,可以通过 slots['slotName'] 来访问插槽内容。

处理多个插槽
<script setup>
import { useSlots } from 'vue';

const slots = useSlots();

// 判断某个插槽是否有内容
const hasHeaderSlot = !!slots.header;
</script>

<template>
  <header v-if="hasHeaderSlot">
    <slot name="header">Default Header</slot>
  </header>
  <main>
    <slot>Default content</slot>
  </main>
</template>

在这个例子中,使用 useSlots() 获取插槽内容,并通过 hasHeaderSlot 判断是否提供了 header 插槽。这样做的好处是你可以在插槽内容为空时提供默认内容。

useAttrs()

useAttrs()Vue 3 中的另一个函数,用于访问组件的 属性(attributes)。组件的属性是传递给组件的非 prop 的普通 HTML 属性。通常情况下,Vue 会自动处理这些属性并将它们传递给组件的根元素,但 useAttrs() 可以帮助你在 setup() 中显式地访问这些属性。

基本用法
<script setup>
import { useAttrs } from 'vue';

// 获取所有属性
const attrs = useAttrs();

// 输出属性
console.log(attrs);
</script>

<template>
    <!-- 将所有属性绑定到组件的根元素 -->
    <div v-bind="attrs">Hello, Vue!</div>
</template>

在这个例子中,useAttrs() 返回一个对象,其中包含了传递给组件的所有非 prop 的属性。你可以将这个对象通过 v-bind="attrs" 绑定到组件的根元素,这样就可以将所有属性自动传递给根元素。

useAttrs() 返回的对象:

  • useAttrs() 返回一个对象,包含了传递给组件的所有属性。

  • 如果父组件传递了属性,例如:<MyComponent id="comp1" class="box" />,这些属性将会包含在 attrs 对象中。

  • attrs 对象不会包含传递给组件的 props,这些是通过 defineProps() 明确声明的。