# SFC и синтаксис <script setup>

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

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

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

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

<script setup>
console.log('привет синтаксис script setup')
</script>
1
2
3

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

# Привязки верхнего уровня будут доступны в шаблоне

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

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

// функция
function log() {
  console.log(msg)
}
</script>

<template>
  <div @click="log">{{ msg }}</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13

Импорты объявляются таким же образом. Это означает, что можно напрямую использовать импортированную вспомогательную функцию в выражениях шаблона, без необходимости объявлять её через опцию methods:

<script setup>
import { capitalize } from './helpers'
</script>

<template>
  <div>{{ capitalize('hello') }}</div>
</template>
1
2
3
4
5
6
7

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

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

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

const count = ref(0)
</script>

<template>
  <button @click="count++">{{ count }}</button>
</template>
1
2
3
4
5
6
7
8
9

# Использование компонентов

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

<script setup>
import MyComponent from './MyComponent.vue'
</script>

<template>
  <MyComponent />
</template>
1
2
3
4
5
6
7

Считайте, что на MyComponent ссылаются как на переменную. Если использовали JSX, то ментальная модель тут аналогична. Эквивалент в kebab-case <my-component> работает и в шаблоне — но настоятельно рекомендуем писать теги в PascalCase для консистентности. Это также помогает отличить их от нативных пользовательских элементов.

# Динамические компоненты

Поскольку на компоненты ссылаются как на переменные, а не регистрируют их под строковыми ключами, то внутри <script setup> при использовании динамических компонентов потребуется использовать динамическую привязку с помощью :is:

<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>

<template>
  <component :is="Foo" />
  <component :is="someCondition ? Foo : Bar" />
</template>
1
2
3
4
5
6
7
8
9

Обратите внимание, как компоненты могут использоваться в качестве переменных в тернарном выражении.

# Рекурсивные компоненты

Однофайловые компоненты могут неявно ссылаться сами на себя с помощью имени файла. Например, файл с именем FooBar.vue может ссылаться на себя как <FooBar/> в своём шаблоне.

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

import { FooBar as FooBarChild } from './components'
1

# Компоненты с пространством имён

Можно использовать теги компонентов с точками, например <Foo.Bar>, чтобы ссылаться на компоненты, вложенные в свойства объекта. Это полезно при импорте нескольких компонентов из одного файла:

<script setup>
import * as Form from './form-components'
</script>

<template>
  <Form.Input>
    <Form.Label>label</Form.Label>
  </Form.Input>
</template>
1
2
3
4
5
6
7
8
9

# Использование пользовательских директив

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

Но есть одно ограничение, о котором следует знать: необходимо именовать локальные пользовательские директивы в соответствии со следующей схемой: vNameOfDirective, чтобы их можно было использовать напрямую в шаблоне.

<script setup>
const vMyDirective = {
  beforeMount: (el) => {
    // сделать что-нибудь с элементом
  }
}
</script>

<template>
  <h1 v-my-directive>Какой-то заголовок</h1>
</template>
1
2
3
4
5
6
7
8
9
10
11
<script setup>
  // импорт также работает, его можно переименовать для соответствия схеме именования
  import { myDirective as vMyDirective } from './MyDirective.js'
</script>
1
2
3
4

# defineProps и defineEmits

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

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

const emit = defineEmits(['change', 'delete'])

// код setup
</script>
1
2
3
4
5
6
7
8
9
  • defineProps и defineEmitsмакросы компилятора используемые только внутри <script setup>. Их не нужно импортировать и они будут компилироваться при обработке <script setup>.

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

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

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

При использовании TypeScript можно также объявлять входные параметры и события с помощью аннотации чистых типов.

# defineExpose

Компоненты со <script setup> по умолчанию закрытые — т.е. публичный экземпляр компонента, получаемый через ссылку в шаблоне или цепочку $parent, не объявляет доступа к каким-либо привязкам внутри <script setup>.

Чтобы явно объявить свойства в компоненте со <script setup>, воспользуйтесь макросом компилятора defineExpose:

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

const a = 1
const b = ref(2)

defineExpose({
  a,
  b
})
</script>
1
2
3
4
5
6
7
8
9
10
11

Когда родитель запросит экземпляр этого компонента через ссылку в шаблоне, то полученный экземпляр будет иметь вид { a: number, b: number } (ref-ссылки автоматически разворачиваются, как и для обычных экземпляров).

# useSlots и useAttrs

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

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

const slots = useSlots()
const attrs = useAttrs()
</script>
1
2
3
4
5
6

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

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

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

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

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

<script setup>
// выполняется в области видимости setup() (для каждого экземпляра)
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

ВНИМАНИЕ

render-функции не поддерживаются в таком сценарии. Вместо них используйте обычный <script> с опцией setup.

# Верхне-уровневый await

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

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

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

Примечание

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

# Возможности только для TypeScript

# Объявление типов для входных параметров/генерируемых событий

Типы входных параметров и генерируемых событий также можно объявить с помощью синтаксиса с передачей аргумента литерального типа в defineProps или defineEmits:

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

const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()
1
2
3
4
5
6
7
8
9
  • defineProps или defineEmits могут использовать только объявления во время runtime ИЛИ объявление типа. Использование обоих одновременно приведёт к ошибке компиляции.

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

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

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

    • Выдаваемый код по-прежнему останется TypeScript с правильной типизацией, который может обрабатываться другими инструментами.

  • В настоящее время для обеспечения корректного статического анализа, аргумент объявления типа должен быть одним из следующих:

    • Литерал типа
    • Ссылка на интерфейс или литерал типа в том же файле

    В настоящее время сложные типы и импорт типов из других файлов не поддерживается. Теоретически возможно что такая поддержка импортов появится в будущем.

# Значения входных параметров по умолчанию при использовании объявления типов

Один из недостатков объявления типов через defineProps в том, что нет возможности указать значения по умолчанию для входных параметров. Для решения этой проблемы создан макрос для компилятора withDefaults:

interface Props {
  msg?: string
  labels?: string[]
}

const props = withDefaults(defineProps<Props>(), {
  msg: 'привет',
  labels: () => ['один', 'два']
})
1
2
3
4
5
6
7
8
9

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

# Ограничение: Никаких импортов с помощью src

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