Новые возможности Vue 3.3

This entry is part 2 of 2 in the series Обзор обновлений Vue 3

Статьи серии
  1. Новые возможности Vue 3.1 — 3.2
  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, а indexnumber.

Несколько слотов

Можно описывать сразу все:

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. Лучшие практики.

0 0 голоса
Рейтинг статьи
guest
0 комментариев
Старые
Новые Популярные