Статьи серии
- Новые возможности Vue 3.1 — 3.2
- Новые возможности Vue 3.3
В этой статье продолжаем обзор возможностей, которые появились во Vue. Переходим к версии 3.3.
В Vue 3.3 появились два новых макроса для <script setup>, которые делают код чище и лучше интегрируются с TypeScript. Что такое макросы в данном контексте, можете прочитать в предыдущей статье.
defineOptions()
До Vue 3.3, если нужно было указать обычные опции компонента (name, inheritAttrs и т.д.), приходилось писать два блока <script>.
<script>
export default {
name: 'BaseButton',
inheritAttrs: false
}
</script>
<script setup lang="ts">
const count = ref(0)
</script>
Теперь все можно сделать в одном месте:
<script setup lang="ts">
defineOptions({
name: 'BaseButton',
inheritAttrs: false
})
const count = ref(0)
</script>
Это чаще всего используют для name и inheritAttrs.
Также можно передавать собственные опции, которые читают плагины:
defineOptions({
customOptions: {
requiresAuth: true
}
})
Главная польза — больше не нужно создавать второй <script> только ради пары строк.
defineSlots()
Если defineProps() описывает входные данные компонента, то defineSlots() описывает слоты, которые компонент ожидает получить.
Важно понимать его назначение:
defineSlots()практически не влияет на работу приложения. Он нужен в первую очередь для TypeScript, Volar иvue-tsc, чтобы IDE могла проверять типы слот-пропсов и производить автодополнение и вывод подсказок.
Без defineSlots()
Представим компонент списка:
<List>
<template #item="{ user }">
...
</template>
</List>
Редактор зачастую знает лишь, что существует user, но не знает его тип.
С defineSlots()
Внутри компонента:
defineSlots<{
default(props: { users: User[] }): any
item(props: {
user: User
index: number
}): any
}>()
Теперь пользователь компонента получает полноценные подсказки:
<template #item="{ user, index }">
IDE знает, что user имеет тип User, а index — number.
Несколько слотов
Можно описывать сразу все:
defineSlots<{
header(): any
default(props: {
items: Item[]
}): any
footer(props: {
total: number
}): any
}>()
Получается своего рода контракт компонента.
Возвращаемое значение
Макрос возвращает тот же объект, что и useSlots():
const slots = defineSlots<{
default(props: { message: string }): any
}>()
То есть можно одновременно получить типизированный объект slots.
До Vue 3.3 типизировать слоты было довольно неудобно. defineSlots() сделал это настолько же простым, как уже привычные defineProps() и defineEmits().
defineModel()
defineModel() — одно из самых заметных нововведений Vue 3.3. На момент выхода 3.3 он, правда, был экспериментальным (experimental) и требовал явного включения в настройках сборщика. А в Vue 3.4 он стал стабильным и рекомендуется как основной способ реализации v-model в компонентах.
Его цель очень проста — убрать шаблонный код, который раньше приходилось писать для поддержки v-model.
Как было раньше
Предположим, мы пишем компонент поля ввода.
Чтобы можно было сделать так:
<MyInput v-model="name" />
приходилось объявить prop и событие:
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
А затем в шаблоне нужно было самостоятельно связывать их:
<input
:value="props.modelValue"
@input="emit('update:modelValue', $event.target.value)"
>
Это повторялось практически в каждом компоненте.
С defineModel()
Теперь достаточно написать defineModel:
const model = defineModel<string>()
И использовать его как обычный ref:
<input v-model="model">
Компилятор автоматически сгенерирует примерно такой код:
defineProps({
modelValue: String
})
defineEmits([
'update:modelValue'
])
и создаст реактивную связь между ними.
Что представляет собой model
defineModel() возвращает обычный Ref.
Поэтому с ним можно работать как с любым другим ref:
const model = defineModel<number>()
model.value++
или
watch(model, value => {
console.log(value)
})
То есть он ничем не отличается от результата ref().
Типизация
Тип задается через generic:
const model = defineModel<User>()
Теперь TypeScript знает тип значения v-model.
Значение по-умолчанию
Можно указать настройки модели:
const model = defineModel({
default: ''
})
или
const model = defineModel<number>({
default: 0
})
Компилятор автоматически превратит это в default для modelValue.
Обязательное значение
Можно сделать модель обязательной:
const model = defineModel<string>({
required: true
})
Тогда тип станет Ref<string> вместо Ref<string | undefined>
Несколько v-model
До появления defineModel() поддержка нескольких моделей выглядела громоздко.
<MyDialog
v-model:opened="opened"
v-model:title="title"
/>
Нужно было объявлять два props и два события.
Теперь достаточно:
const opened = defineModel<boolean>('opened')
const title = defineModel<string>('title')
И пользоваться ими как обычными ref.
Модификаторы v-model
defineModel() умеет работать и с модификаторами.
Например:
<MyInput v-model.trim="text" />
Можно получить информацию о модификаторах:
const [model, modifiers] = defineModel<string>()
Теперь modifiers.trim будет равно true, если использован .trim.
Аналогично работают .number , .lazy и собственные модификаторы.
Преобразование значения
Можно определить собственые преобразования через get и set:
const [model, modifiers] = defineModel({
set(value) {
if (modifiers.trim) {
return value.trim()
}
return value
}
})
Это избавляет от необходимости самостоятельно обрабатывать событие update:modelValue.
Когда стоит использовать
Начиная с Vue 3.4 defineModel() рекомендуется использовать практически во всех новых компонентах, поддерживающих v-model. Ведь defineModel имеет следующие плюсы:
- значительно сокращает объем шаблонного кода;
- улучшает читаемость компонента;
- обеспечивает отличную типизацию в TypeScript;
- одинаково хорошо подходит как для одного, так и для нескольких
v-model.
В результате компонент становится проще: вместо отдельных props, emits и обработчиков обновления вы работаете с одним реактивным ref, что делает код более естественным и лаконичным.
toRef() теперь поддерживает getter-функции
До Vue 3.3 toRef() умел создавать Ref из свойства реактивного объекта или существующего Ref:
const state = reactive({
count: 1
})
const count = toRef(state, 'count')
Если же нужно было получить реактивную ссылку на вычисляемое значение, приходилось использовать computed():
const id = computed(() => props.id)
В Vue 3.3 toRef() научился принимать getter-функции:
const id = toRef(() => props.id)
Это особенно удобно при написании composable-функций, которые принимают значения в разных формах (обычное значение, Ref или getter) и приводят их к единому типу Ref.
Важно:
toRef(() => ...)создаетreadonly-ссылку. Изменятьid.valueнельзя.
Новая функция toValue()
Вместе с поддержкой getter-функций появился toValue(). Это расширенная версия unref().
Если unref() умеет извлекать значение только из Ref:
unref(ref(10)) // 10
unref(() => 10) // вернет саму функцию
то toValue() понимает и getter-функции:
toValue(ref(10)) // 10
toValue(() => 10) // 10
toValue(10) // 10
Поэтому toValue() удобно использовать в Composable, когда нужно получить текущее значение независимо от того, передали обычное значение, Ref или getter.
Простое правило:
toRef()— когда нужен реактивныйRef.toValue()— когда нужно просто получить текущее значение.
Обе функции появились в Vue 3.3 как часть улучшений Composition API и значительно упростили создание универсальных composable-функций.
О применении toValue в Composable рассказывалось в заметке Vue.js Composables. Лучшие практики.