Перейти к содержанию

<script setup>

<script setup> — это синтаксический сахар во время компиляции для использования Composition API внутри однофайловых компонентов (SFC). Это рекомендуемый синтаксис, если вы используете как SFC, так и Composition API. Он предоставляет ряд преимуществ по сравнению с обычным синтаксисом <script>:

  • Более лаконичный код с меньшим количеством шаблонов
  • Возможность объявлять параметры и испускаемые события, используя чистый TypeScript
  • Более высокая производительность в среде выполнения (шаблон компилируется в рендер-функцию в той же области видимости, без промежуточного прокси)
  • Улучшенная работа IDE по определению типов (меньше работы для языкового сервера по извлечению типов из кода)

Базовый синтаксис

Чтобы перейти на этот синтаксис, добавьте атрибут setup в блок <script>:

vue
<script setup>
console.log('приветствие от script setup')
</script>

Код внутри компилируется как содержимое функции компонента setup(). Это означает, что в отличие от обычного <script>, который выполняется только один раз при первом импорте компонента, код внутри <script setup> будет выполняться каждый раз при создании экземпляра компонента.

Связки верхнего уровня открыты для шаблона

При использовании <script setup> все привязки верхнего уровня (включая переменные, объявления функций и импорт), объявленные внутри <script setup>, можно напрямую использовать в шаблоне:

vue
<script setup>
// переменная
const msg = 'Hello!'

// функции
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>

Реактивность

Реактивное состояние должно быть явно создано с помощью Reactivity API. Подобно значениям, возвращаемым функцией setup(), ссылки автоматически разворачиваются, когда на них ссылаются в шаблонах:

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) => {
    // делаем что-нибудь с элементом
  }
}
</script>
<template>
  <h1 v-my-directive>Это заголовок</h1>
</template>

Если вы импортируете директиву из другого места, её можно переименовать, чтобы она соответствовала требуемой схеме именования:

vue
<script setup>
import { myDirective as vMyDirective } from './MyDirective.js'
</script>

defineProps() и defineEmits()

Для объявления таких опций, как props и emits с полной поддержкой вывода типов, мы можем использовать API defineProps и defineEmits, которые автоматически доступны внутри <script setup>:

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

const emit = defineEmits(['change', 'delete'])
// код setup
</script>
  • defineProps и defineEmits — это макросы компилятора, используемые только внутри <script setup>. Их не нужно импортировать, они компилируются при обработке <script setup>.

  • defineProps принимает то же значение, что и опция props, а defineEmits принимает то же значение, что и опция emits.

  • defineProps и defineEmits обеспечивают правильное выделение типов на основе переданных опций.

  • Опции, переданные в defineProps и defineEmits, будут извлечены из настройки в область видимости модуля. Поэтому опции не могут ссылаться на локальные переменные, объявленные в области setup. Это приведет к ошибке компиляции. Однако он может ссылаться на импортированные привязки, поскольку они также находятся в области видимости модуля.

Объявления типов для props/emit

Параметры и испускаемые события также можно объявить, используя синтаксис чистого типа, передав аргумент типа литерала в defineProps или defineEmits:

ts
const props = defineProps<{
  foo: string
  bar?: number
}>()

const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()

// 3.3+: альтернативный, более лаконичный синтаксис
const emit = defineEmits<{
  change: [id: number] // синтаксис именованного кортежа
  update: [value: string]
}>()
  • defineProps или defineEmits могут использовать только либо объявление среды выполнения, либо объявление типа. Одновременное использование обоих вариантов приведёт к ошибке компиляции.

  • При использовании объявления типов эквивалентное объявление среды выполнения автоматически генерируется на основе статического анализа, что устраняет необходимость в двойном объявлении и обеспечивает корректное поведение в среде выполнения.

    • В режиме разработки компилятор попытается вывести из типов соответствующую проверку во время выполнения. Например, здесь foo: String выводится из типа foo: string. Если тип является ссылкой на импортированный тип, то результатом вычисления будет foo: null (равный типу any), поскольку компилятор не располагает информацией о внешних файлах.

    • В продакшен-режиме компилятор сгенерирует объявление формата массива, чтобы уменьшить размер пакета (параметры здесь будут скомпилированы в ['foo', 'bar'])

  • В версии 3.2 и ниже параметр общего типа для defineProps() был ограничен литералом типа или ссылкой на локальный интерфейс.

    Это ограничение было устранено в версии 3.3. Последняя версия Vue поддерживает ссылки на импортируемые и ограниченный набор сложных типов в позиции параметра. Однако, поскольку преобразование типов к среде выполнения всё ещё основано на AST, некоторые сложные типы, требующие фактического анализа (например, условные типы), не поддерживаются. Вы можете использовать условные типы для отдельных параметров, но не всего объекта props.

Деструктуризация реактивных параметров

В Vue 3.5 и выше переменные, деструктурированные из возвращаемого значения defineProps, являются реактивными. Компилятор Vue автоматически добавляет префикс props., когда код в одном и том же блоке <script setup> обращается к переменным, деструктурированным из defineProps:

ts
const { foo } = defineProps(['foo'])

watchEffect(() => {
  // выполняется только один раз до 3.5
  // перезапускается при изменении параметра "foo" в версии 3.5+
  console.log(foo)
})

Всё вышесказанное сводится к следующему эквиваленту:

js
const props = defineProps(['foo'])

watchEffect(() => {
  // `foo` преобразуется компилятором в `props.foo`
  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
export interface Props {
  msg?: string
  labels?: string[]
}

const props = withDefaults(defineProps<Props>(), {
  msg: 'hello',
  labels: () => ['one', 'two']
})

Это будет скомпилировано в эквивалентные опции параметра default. Кроме того, хелпер withDefaults обеспечивает проверку типов для значений по умолчанию и гарантирует, что в возвращаемом типе props будут удалены необязательные флаги для свойств, у которых объявлены значения по умолчанию.

Примечание

Обратите внимание, что значения по умолчанию для изменяемых ссылочных типов (например, массивов или объектов) должны быть обёрнуты в функции при использовании withDefaults, чтобы избежать случайного изменения и внешних побочных эффектов. Это гарантирует, что каждый экземпляр компонента получит свою собственную копию значения по умолчанию. Это не нужно при использовании значений по умолчанию с деструктуризацией.

defineModel()

  • Доступно только в 3.4+

Этот макрос можно использовать для объявления двусторонней привязки параметра, которая может быть использована через v-model из родительского компонента. Пример использования также рассматривается в руководстве Компонентная v-model.

Под капотом этот макрос объявляет параметр модели и соответствующее событие обновления значения. Если первый аргумент является литеральной строкой, он будет использован в качестве имени параметра; в противном случае имя параметра по умолчанию будет равно "modelValue". В обоих случаях вы также можете передать дополнительный объект, который может содержать опции параметра и опции преобразования значений модели.

js
// объявляет параметр "modelValue", потребляемый родителем через v-model
const model = defineModel()
// ИЛИ: объявляет параметр "modelValue" с опциями
const model = defineModel({ type: String })

// выдает "update:modelValue" при мутации
model.value = 'hello'

// объявляет параметр "count", потребляемый родителем через v-model:count
const count = defineModel('count')
// ИЛИ: объявляет параметр "count" с опциями
const count = defineModel('count', { type: Number, default: 0 })

function inc() {
  // выдает "update:count" при мутации
  count.value++
}

Предупреждение

Если у вас есть значение default для свойства defineModel и вы не предоставляете никакого значения для этого свойства из родительского компонента, это может привести к рассинхронизации между родительским и дочерним компонентами. В примере ниже родительский myRef не определён, а дочерний model равен 1:

js
// дочерний компонент:
const model = defineModel({ default: 1 })

// родительский компонент:
const myRef = ref()
html
<Child v-model="myRef"></Child>

Модификаторы и трансформаторы

Чтобы получить доступ к модификаторам, используемым с директивой v-model, мы можем деструктурировать возвращаемое значение defineModel() следующим образом:

js
const [modelValue, modelModifiers] = defineModel()

// соответствует v-model.trim
if (modelModifiers.trim) {
  // ...
}

Когда присутствует модификатор, нам, скорее всего, потребуется преобразовать значение при чтении или синхронизации с родительским объектом. Для этого мы можем использовать опции трансформаторов get и set:

js
const [modelValue, modelModifiers] = defineModel({
  // get() опущен, так как здесь он не нужен
  set(value) {
    // если используется модификатор .trim, возвращает обрезанное значение
    if (modelModifiers.trim) {
      return value.trim()
    }
    // в противном случае возвращаем значение как есть
    return value
  }
})

Использование с TypeScript

Как и defineProps и defineEmits, defineModel также может принимать аргументы типа, чтобы указать типы значения модели и модификаторов:

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

// по умолчанию модель с опциями, required — удаляет возможные неопределённые значения
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: число, b: number } (ссылки автоматически разворачиваются, как и в обычных экземплярах).

defineOptions()

Этот макрос можно использовать для объявления опций компонента непосредственно внутри <script setup> без необходимости использовать отдельный блок <script>:

vue
<script setup>
defineOptions({
  inheritAttrs: false,
  customOptions: {
    /* ... */
  }
})
</script>
  • Поддерживается только в версии 3.3+.
  • Это макрос. Опции будут подняты в область видимости модуля и не смогут обращаться к локальным переменным в <script setup>, которые не являются литеральными константами.

defineSlots()

Этот макрос можно использовать для предоставления IDE подсказок типа для проверки имени слота и типа параметров.

defineSlots() принимает только параметр типа и никаких аргументов времени выполнения. Параметр type должен быть литералом типа, где ключ свойства — это имя слота, а тип значения — функция слота. Первым аргументом функции является параметр, который слот ожидает получить, и его тип будет использоваться для параметров слота в шаблоне. В настоящее время тип возврата игнорируется и может быть any, но в будущем мы можем использовать его для проверки содержимого слота.

Он также возвращает объект slots, который эквивалентен объекту slots, выставляемому в контексте setup или возвращаемому функцией useSlots().

vue
<script setup lang="ts">
const slots = defineSlots<{
  default(props: { msg: string }): any
}>()
</script>
  • Поддерживается только в версии 3.3+.

useSlots() и useAttrs()

Использование slots и attrs внутри <script setup> должно быть относительно редким, так как вы можете получить к ним прямой доступ как $slots и $attrs в шаблоне. В редких случаях, когда они вам понадобятся, используйте хелперы useSlots и useAttrs соответственно:

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

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

useSlots и useAttrs — это фактические функции времени выполнения, которые возвращают эквивалент setupContext.slots и setupContext.attrs. Их можно использовать и в обычных функциях API.

Использование вместе с обычным <script>

<script setup> можно использовать наряду с обычным <script>. Обычный <script> может понадобиться в тех случаях, когда это необходимо:

  • Объявление опций, которые не могут быть выражены в <script setup>, например, inheritAttrs или пользовательские опции, включаемые через плагины (может быть заменено на defineOptions в 3.3+).
  • Объявление именованных экспортов.
  • Запуск побочных эффектов или создание объектов, которые должны выполняться только один раз.
vue
<script>
// обычный <script>, выполняется в области видимости модуля (только один раз)
runSideEffectOnce()

// объявление дополнительных опций
export default {
  inheritAttrs: false,
  customOptions: {}
}
</script>

<script setup>
// выполняется в области видимости setup() (для каждого экземпляра)
</script>

Поддержка сочетания <script setup> и <script> в одном компоненте ограничена описанными выше сценариями. В частности:

  • НЕ используйте отдельную секцию <script> для опций, которые уже могут быть определены с помощью <script setup>, таких как props и emits.
  • Переменные, созданные внутри <script setup>, не добавляются в качестве свойств к экземпляру компонента, что делает их недоступными из Options API. Смешивать API таким образом категорически не рекомендуется.

Если вы оказались в одном из сценариев, которые не поддерживаются, то вам следует рассмотреть возможность перехода на явную функцию setup(), вместо использования <script setup>.

await верхнего уровня

await верхнего уровня можно использовать внутри <script setup>. Полученный код будет скомпилирован как async setup():

vue
<script setup>
const post = await fetch(`/api/post/1`).then((r) => r.json())
</script>

Кроме того, ожидаемое выражение будет автоматически скомпилировано в формат, сохраняющий контекст текущего экземпляра компонента после await.

Примечание

async setup() необходимо использовать в сочетании с Suspense, которая в настоящее время всё ещё является экспериментальной функцией. Мы планируем доработать и задокументировать его в одном из будущих выпусков, но если вам интересно, вы можете обратиться к тестам, чтобы увидеть, как она работает.

Импортируемые операторы

Импортируемые операторы в Vue следуют спецификации модулей ECMAScript. Кроме того, вы можете использовать псевдонимы, определённые в конфигурации вашего инструмента сборки:

vue
<script setup>
import { ref } from 'vue'
import { componentA } from './Components'
import { componentB } from '@/Components'
import { componentC } from '~/Components'
</script>

Дженерики

Параметры общего типа могут быть объявлены с помощью атрибута generic в теге <script>:

vue
<script setup lang="ts" generic="T">
defineProps<{
  items: T[]
  selected: T
}>()
</script>

Значение generic работает точно так же, как список параметров между <...> в TypeScript. Например, вы можете использовать несколько параметров, ограничения extends, типы по умолчанию и ссылки на импортированные типы:

vue
<script
  setup
  lang="ts"
  generic="T extends string | number, U extends Item"
>
import type { Item } from './types'
defineProps<{
  id: T
  list: U[]
}>()
</script>

Вы можете использовать директиву @vue-generic для явной передачи типов, когда они не могут быть выведены автоматически:

vue
<template>
  <!-- @vue-generic {import('@/api').Actor} -->
  <ApiSelect v-model="peopleIds" endpoint="/api/actors" id-prop="actorId" />

  <!-- @vue-generic {import('@/api').Genre} -->
  <ApiSelect v-model="genreIds" endpoint="/api/genres" id-prop="genreId" />
</template>

Чтобы использовать ссылку на универсальный компонент в ref, необходимо использовать библиотеку vue-component-type-helpers, поскольку InstanceType не будет работать.

vue
<script setup lang="ts">
import componentWithoutGenerics from '../component-without-generics.vue'
import genericComponent from '../generic-component.vue'
import type { ComponentExposed } from 'vue-component-type-helpers'

// Работает для компонента без дженериков
ref<InstanceType<typeof componentWithoutGenerics>>()
ref<ComponentExposed<typeof genericComponent>>()
</script>

Ограничения

  • Из-за разницы в семантике выполнения модулей код внутри <script setup> полагается на контекст SFC. Если перенести их во внешние файлы .js или .ts, это может привести к путанице как для разработчиков, так и для инструментов. Поэтому <script setup> нельзя использовать с атрибутом src.
  • <script setup> не поддерживает шаблон корневого компонента в DOM. (Связанное обсуждение)
<script setup>