Сучасна диджитал-освіта для дітей — безоплатне заняття в GoITeens ×
Mazda CX 5
×

Реліз Vue.js 3.3. Огляд головних нововведень

Усі статті, обговорення, новини про Front-end — в одному місці. Підписуйтеся на телеграм-канал!

Вітаю, друзі. Владислав Носаль на зв’язку, я Full Stack Web Developer і в основному займаюсь створенням різного характеру вебресурсів для різноманітних цілей та в різному їх життєвому циклі, починаючи з ідей та пустого package.json, та закінчуючи оптимізацією усталених проєктів та приведення їх до найкращих стандартів індустрії на сьогодні.

Можна сказати, що я скоріше належу до нового покоління розробників, які прийшли в цей диджиталізований світ, коли в ньому вже існували і рішуче стояли на ногах такі технології як React.js/ Vue.js/ Angular.js та інші.

Сьогодні нові технології стрімко і рішуче прогресують, рухаючи ринок веброзробки вперед, і приносячи кращий й інноваційніший підхід і досвід створення вебзастосунків.

Власне про одну з технологій, з якими я регулярно працюю, я б й хотів сьогодні розказати вам у цій статті. Ціллю матеріалу є ознайомити читачів з нещодавнім релізом нової версії Vue.js — 3.3, розповісти про головні нововведення простими словами, максимально інформативно та ефективно.

Отож, розпочнімо.

Терміни і скорочення

  • TS, тайпскрипт — Typescript.
  • Prop, пропса(и) — властивості, які батьківський компонент передає в нижчий по ієрархічній ланці.
  • Emit, еміт — концепція у Vue.js, що передбачає змогу компонента передати дані або повідомити свого предка (вищого у ланці компонента) про якусь дію, яка відбулась в цьому нащадку.
  • API — Application programming interface — інтерфейс, який та чи інша технологія надає для її належного використання.
  • Generic — параметр чи аргумент, який приймає той чи інший тип чи інтерфейс в тайпскрипті.
  • Options API — перший та старіший інтерфейс компоненту, суттю якого є угрупування схожих налаштувань в якості методів та ключів об’єктів.
  • Composition API — більш новий та більш React-подібний інтерфейс компоненту.
  • Декларація — оголошення якихось даних.

Vue і Typescript

Довгий час Vue.js страждав від того, що більшість його API не підтримувало Typescript, який активно розвивався й набирав популярність, і з часом став стандартом розробки будь-якого серйозного ресурсу. В цьому звичайно є сенс, оскільки TS дозволяє значно полегшити процес розробки в команді завдяки його статичній типізації.

На відміну від Vue, його конкуренти, як-от React, раніше почали впроваджувати підтримку типізації, що відбивалось на репутації Vue серед розробників і часто слугувало причиною, чому преференції надавались саме Реакту чи Ангуляру, який взагалі впровадив Typescript як невідʼємну частину своєї системи.

Однак, зараз ми бачимо тенденцію до приведення Vue до усіх найкращих стандартів і поступового впровадження типізації та її постійного покращення.

<script setup>  

❗️ Увага: в цій статті приклади будуть наведені при використанні Composition API.

Саме script setup став відправною точкою для масштабної підтримки Typescript, через можливість, яку надає атрибут lang: він дозволяє зазначити, що мовою яка буде використовуватись при написанні компонента буде саме TS. Виглядає це наступним чином:

<script setup lang=“ts”>  

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

Певні з цих фіч доступні лише після їх увімкнення у конфігурації VUE-CLI або Vite.

Для користувачів IDE від IntelliJ раджу прочитати цю статтю, в якій описуються проблеми, з якими ви можете зіштовхнутись, та як їх обійти задля використання нової версії Vue з підтримкою вашого Webstorm чи іншої програми для написання коду від Intellij.

(Звичайний <script lang="ts«> також підтримує використання типізації, однак на тому досвіді можу запевнити, що не користується такою популярністю серед розробників, як його новітній аналог.)

defineProps

До версії 3.3 декларація пропсів відбувалась завдяки тому ж самому макросу defineProps. Звичайно, варто зазначити, що це за умов, що ми використовуємо Composition API (якщо це Options API, то в цілому синтакс аналогічний до одного з перезавантажень defineProps).

Існує три варіанти використання цього макросу:

1. Простий синтаксис

defineProps = [“title”, “description”] 

Головною проблемою цього синтаксису є те, що він лише декларує пропси, при цьому не передбачає жодної їх валідації, типізації чи принаймні присвоєння значень за замовчуванням, в разі, якщо батьківський компонент не передає їх.

2. Options API синтаксис

Нижченаведений варіант використання цього макросу фактично повністю відображає типову декларацію пропсів в Options API. Його перевагою є те, що він дозволяє задати тип і значення за замовчуванням, а ще й надає можливість валідувати значення за допомогою передання validator функції, яка повинна прийняти і повернути передане значення пропси (якщо не передано — значення за замовчуванням) та повинна повернути boolean, на основі якого й буде визначено:

defineProps({ 
 prop: { 
 type: String, 
 default: "some", 
 required: false, 
 validator(value: any) { 
 return !!value.length 
 } 
}})

3.Типізована декларація

Нарешті, ми дійшли до забороненого плоду, найпоширенішого та найбільш тайпскрипт-сумісного варіанту декларації:

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

Тут ми одразу можемо помітити звичний нам синтакс тайпскрипту з generic інтерфейсами, і це не може не втішати, однак така декларація не була дуже зручною, адже несла в собі обмежену функціональність, а саме:

не дозволяла використовувати імпортовані типи, які призводили до помилки компілятора і неможливості зробити висновок про структуру даних та їхній тип (фактично, прирівнювало усі дані, що посилались на імпортований інтерфейс до any). В разі якщо інтерфейс пропси містив у собі велику кількість властивостей, якість коду псувалась і можливість його легкої інтерпретації відповідно теж. Це значно звужувало можливості підтримання декларативності, хоча й передбачало можливість посилання на локально створений тип, щось на кшталт:

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

Однак, буду відвертим, я завжди надавав перевагу першому зазначенню інтерфейсу в самому дженеріку, без використання посилання, адже, на мою суб’єктивну думку, компонент повинен містити в собі виключно бізнес-логіку, і не перейматись декларацією типів.

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

Одним з вирішень стало використання withDefaults макросу, який хоча й вирішував цю проблему, однак був дещо громіздким і нечитабельним:

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

Зміни в новій версії

Реліз нової 3.3 не тільки вирішив вищенаведені питання, а ще й привніс декілька крутих інновацій, на які варто звернути увагу.

Тепер макрос підтримуватиме імпортовані інтерфейси, й окрім того дозволятиме використовувати складніші типи. Це значить, що тепер ми можемо уникнути проблеми з декларативністю коду і користуватися іншими крутими можливостями тайпскрипту, такими як type intersection:

import type {Props} from “@/interfaces“ 
const props = defineProps<{ 
 foo: string 
 bar?: number 
}>() 

Однак, перед тим як використовувати цю нову можливість, зауважте:

  • підтримка складних типів, таких як умовні типи — все ще відсутня;
  • посилання на глобальні типи не підтримується і потребує чіткого імпорту типів: defineProps<UserData>() — (за умов, що UserData декларований в файлі .d.ts і не є чітко імпортованим — не спрацює);
  • помилка резолюції того чи іншого типу, що використовується при декларації пропсів, призведе до помилки компілятора, а якщо це вкладені властивості певного інтерфейсу — до їх прирівняння до undefined.

Що ж стосується withDefaults, то в новому релізі цей невдалий на мою думку макрос замінюється на довгоочікувану реактивну деструктуризацію пропсів.

const {message = “Some message”} = defineProps<{ message: string }>() 

Тепер зовсім не обовʼязково використовувати toRefs, toRef, щоб просто посилатись на пропсу за її ключем. Все це, звичайно, синтаксичний цукор, і просто служить як спрощення життя для нас, розробників.

Звичайно, перед тим як використовувати це, зважте всі плюси й мінуси, серед яких, між іншим, є й те, що ця фіча нестабільна і може бути джерелом різних багів, наприклад, при використанні деструктуризованих змінних в Composable-функціях.

В цілому, ця фіча потребує випробувань на міцність.

❗️ Експериментальна фішка: щоб використовувати реактивну деструктуризацію, її необхідно власноруч увімкнути, обравши її використання в налаштунках вашого vue-cli або vite.

defineEmits

Не дуже суттєва зміна спіткала defineEmits макрос. Він аналогічно до defineProps має декілька варіантів використання, однак я б хотів звернути вашу увагу тільки на типізований.

const emit = defineEmits<{ 
 (e: 'foo', id: number): void 
 (e: 'bar', name: string, ...rest: any[]): void 
}>()

Такий синтаксис в цілому був досить зручним і зрозумілим для досвідчених розробників, добре знайомих з описами функцій в Typescript, однак у випадку з новачками такий синтаксис міг викликати проблеми, оскільки не є надто декларативним.

З точки зору тайпскрипту emit — це функція, яка може викликатись з різними вхідними даними, щось схоже на перенавантаження функції (function overload), в якому перший аргумент зазначав саму подію, яку ми хочем передати, а інші аргументи — дані. Дещо незрозумілим був тип, що повертається функцією void, який фактично значить, що виклик функції нічого не повертає. Це й було точкою непорозуміння, що могла вводити новачків в оману.

В новій версії ми бачимо читабельніший варіант використання цього макросу.

const emit = defineEmits<{ 
 foo: [id: number] 
 bar: [name: string, ...rest: any[]] 
}>() 

Ми бачимо, що тепер це є простий опис об’єкта, на відміну від попереднього опису функції. Це покращує ситуацію і виглядає читабельніше. Ключ визначає ім’я івенту, аргумент, своєю чергою — дані, які ми передаємо за допомогою Emit API.

Однією з рекомендацій для використання є надання переваги labeled tuples (чіткому опису структури масивів, що цілком відрізняється від string[]).

Нічого не заважає нам використовувати any[], це не обовʼязково повинен бути tupple.

Generic components

Виходячи з усього, що ми бачили вище, можна підсумувати, що існує дуже чітка тенденція до типізації усього, що взагалі можна типізувати. І якщо підтримка інтерфейсів для декларації емірів чи пропсів вже досить усталено використовується, то питання generic-компонентів не було таким поширеним, однак вже довгий час витало в повітрі.

Якщо ви ніколи раніше не використовували дженерік-компоненти, а то й взагалі не чули про них — цілком нормально, адже ця тема не набула поширення, гадаю, через те що були пріоритетніші цілі для впровадження підтримки тайпскрипту.

Які проблеми вирішують дженерік-компоненти і чому це круто ?

Відповіддю на це питання, звичайно ж, є проблема чіткої типізації, а саме забезпечення належних інтерфейсів для props та emit API в template (HTML) частині нашого компоненту, уникаючи фіксованої типізації (те, в що компілятор зазвичай перетворює дані, які передаються через пропси — { «title»: string } — Object) в обмін на динамічнішу з підтримкою Intellisense (доповнення коду).

Уявімо наступний приклад:

В нас є компонент MySelect

<my-select 
 :options="[{ label: 'Label', value: 'value'}]" 
 @select="handleSelect"></my-select> 

<my-select 
 :options="['first', 'second']" 
 @select="handleSelect"></my-select> 

В першому прикладі він приймає масив обʼєктів наступної форми: { label: string, value: any}, а в другому — просто массив string-ів.

В залежності від типу даних, переданих в options, ми хотіли, щоб відповідний тип був заключений й для нашої функції, яка реагує на подію вибору select.

:options="[{ label: 'Label', value: ‘value'}]"  
=> handleSelect(option: {label: string, value: any)}
:options="['first', 'second']"  
=> handleSelect(option: string) 

Завдяки новій можливості, яка дозволяє оголосити generic-параметр, ми тепер можемо втілити такий задум у життя.

<script setup lang="ts" generic="T"> 
import {defineEmits} from "vue"; 
defineProps<{ 
 options: T[] 
}>() 
defineEmits<{ 
 (e: 'select', name: T): void 
}>() 
</script> 

Передавши дані в пропсу options, ми фактично кажемо Typescript, щоб він заключив тип Т власноруч, а це дасть нам можливість зазначити однаковий тип і структуру даних для того, що ми передамо через emit вгору і через props вниз.ʼ

Дещо складнішим випадком буде використання подовження типів (extends):

<script 
 setup 
 lang="ts" 
 generic="Clearable extends boolean, ValueType extends string | number |  null | undefined" 
> 
type OnChange<ValueType, Clearable> = Clearable extends true  ? (value: ValueType | null) => void 
 : (value: ValueType) => void; 
defineProps<{ 
 clearable?: Clearable; 
 value?: ValueType; 
 onChange?: OnChange<ValueType, Clearable>; 
}> 
</script> 

Згідно з прикладом вище, ми можемо оголошувати декілька дженерік-аргументів і використовувати extends та умовні типи (conditional types) разом з ними, на основі яких ми можемо задавати інтерфейс нашим прописам чи емітам.

Якщо розібрати, що відбувається вище, то простими словами це звучить приблизно ось так:

Тип пропси clearable обмежується лише до true, false, value — це юніон-тип, що може належати до типів: string, number, null або undefined. onChange — це функція, яка передається вниз і в залежності від того, чи clearable — true аргумент функції, може мати різний тип даних.

Якби нам довелось реалізовувати такий функціонал самотужки, без цього нововведення, це забрало б значно більше часу і зусиль.

Типізовані слоти

У Vue існує така концепція як scoped slots. Вона уможливлює ще один варіант взаємодії між батьківським компонентом і його дитиною. Ця взаємодія полягає в можливості компонента передати нащадку дані зі свого локального стану.

Разом з багатьма іншими новими фінами, ця концепція також отримала своє оновлення, і тепер життя будь-якого розробника, що використовує Vue, а тим більше розробників бібліотек компонентів типу Quasar чи Vuetify, помітно покращиться.

defineSlots — макрос компіляції, що у використанні не сильно відрізняється від вищезгаданих defineProps та defineEmits, однак є зовсім новою можливістю. Він дозволяє там типізувати scoped slots, тим самим зазначивши, які слоти очікує компонент і які пропси він передає вгору для батьківського компоненту.

Використовується він наступним чином:

defineSlots<{ 
 default?: (props: { msg: string }) => any 
 item?: (props: { id: number }) => any 
}>() 

Макрос приймає дженерік-параметр, який своєю чергою описує обʼєкт, де ключ — це ім’я слота <slot name=" item"/>, а значення — це функція з аргументом, типом якого є обʼєкт, в цьому випадку — аргумент props, що є обʼєктом з обовʼязковою властивістю msg, яка належить до типу string.

<slot name=“item” msg=“Hello”/> 

Зверніть увагу на те, що default слот не вимагає зазначення атрибута name.

Однією з цікавинок в майбутньому може стати тип даних, що повертає функція, який може бути використаний для зазначення контенту самого слота, однак наразі це лише

в планах, і функція повинна вертати або any, або масив віртуальних HTML — VNode[].

Не вдаючись в деталі, зазначу, що до введення даного макросу типізувати слоти можна було схожим до generic-компонентів чином — використовуючи функцію обгортку.

Model binding

Однією з особливостей Vue, звичайно, є його директиви, і напевно найпопулярніша й найбільш вживана з них є v-model. Вона дозволяє скоротити ваш код і просто є дуже зручним шляхом для взаємодії з, в більшості випадків, компонентами форм.

Vue 3.3 зробить її використання ще зручнішим, додавши новий макрос defineModel.

Ви тепер легко зможете перевикористати v-model для вашого компоненту, без необхідності оголошення пропси modelValue і події update:modelValue.

Задля кращого розуміння, поглянемо на приклад:

<app-date-picker 
 v-model="appointment.date" 
 label="Appointment Time" 
 :validation="v$.date" 
> 
</app-date-picker> 

Цей компонент використовується для вибору дати та часу. Очікуваним наслідком вибору є присвоєння властивості date обʼєкту appointment, вибраного нами значення.

Якщо ми спробуємо заглянути всередину AppDatePicker — ми помітимо наступне:

 <ion-modal :keep-contents-mounted="true"> 
 <ion-datetime 
 id="datetime"
 :model-value="modelValue" 
 @update:model-value="$emit('update:modelValue', $event)"  > 
 </ion-datetime> 
 </ion-modal> 
 </ion-item-group> 
 </ion-item> 
</template> 

Оскільки AppDatePicker — кастомний компонент, він фактично слугує просто ланкою на шляху до ion-datetime, який відповідальний за вибір дати.

Тобто якби ми просто використовували ion-datetime, ми могли б просто передати v-model і все б працювало як годинник, однак в нашому випадку нам потрібно ще оголосити вищезгаданий емі і пропсу, щоб наш v-model на app-date-picker працював.

defineProps<{ 
 modelValue: string 
}>() 
defineEmits<{ 
 (e: 'update:modelValue', val: string): void 
}>() 

З власного досвіду можу сказати, що це досить зручно, окрім того, ми можемо використовувати й v-model з аргументами, на кшталт:

<app-date-picker 
 v-model:errors="errors" 
 label="Appointment Time" 
> 
</app-date-picker> 

Однак в такому випадку змінюється й пропса, і Сміт:

defineProps<{ 
 errors: string 
}>() 
defineEmits<{ 
 (e: 'update:errors', val: string): void 
}>()

Все це тепер можна замінити, використовуючи defineModel:

/////
const modelValue = defineModel<string>() 

В цьому випадку modelValue — це просто змінна, на яку ми посилатимемось при її використанні в v-model (приклад нижче):

<ion-datetime 
 id="datetime" 
 v-model="modelValue" 
> 
</ion-datetime>

Функція defineModel також приймає аргументи для її конфігурації. Так приклад з v model:errors можна легко замінити наступним чином:

const modelValue = defineModel<string>('errors') 

Якщо перший аргумент присутній, то він відповідає аргументу v-model (errors) переданому нами в батьківському компоненті. Якщо ж відсутній — то він, за замовчуванням, modelValue.

Також можна зазначити другий аргумент з наступними налаштуваннями:

  • Local — приймає boolean і дозволяє локально змінювати значення оголошеної нами v model;
  • Required — зазначає необхідність використання батьківським компонентом v-model;
  • Default — задає значення за замовчуванням;
  • Validator — функція, що повинна повернути boolean, яка слугує валідатором значення v-model.
  • Якщо без local — то цей обʼєкт конфігурацій можна було б порівняти до налаштувань пропси в Options API.

Component Options

Цей крутий макрос повертає в гру ім’я компонентів та inheritAttrs, що дозволяє відімкнути успадкування пропсів, переданих в батьківському компоненті, корінному компоненті спадку. Простіше кажучи: пропси, що передаються дитячому компоненту, не будуть передані найвищому компоненту в <template> тезі.

defineOptions({ 
 name: 'AppSelect', 
 inheritAttrs: false 
}) 

Хоча й маленька, однак дуже крута штука значно спростить досвід написання компонентів й не вимагатиме від вас оголошувати ще один <script> тег в Options API стилі для налаштування атрибутів чи імені.

Раніше:

<script lang="ts"> 
export default { 
 name: 'MyComponent', 
 inheritAttrs: false 
} 
</script> 

Інші менш значні оновлення — toRef

  • Тепер може створювати getters на кшталт computed, однак не передбачає можливості вирахування даних.
  • Може використовуватись аналогічно до ref: задля створення реактивних значень — toValue — протилежність до toRef і краща версія unref.
  • Позбувається реактивності, конвертуючи реактивні значення в звичайні.
  • Покращена підтримка JSX.

Сподіваюсь, ця стаття була корисна як новачкам ,так і тим, хто Vue-шить вже давно. Дякую за увагу! Якщо вам цікаво обговорити цю або інші теми, додавайтесь до мене у контакти на LinkedIn.

👍ПодобаєтьсяСподобалось4
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Класна стаття. Дякую.

радий приносити користь! Будь ласка!

надеюсь это недоразумение в конце концов умрет

Підтримка TS наразі це зростаючий тренд, і Vue намагається бути в цьому тренді також, тому слід чекати багато нових інших оновлень які покращать досвід розробки. Радий що стаття принесла користь)

ts всё равно базируется на стандарте ecma. И таким образом, при внесении любых изменений в стандарт, разработчики ts разве что смогут похвастать, что они вовремя и оперативно подмахнули. А так вообще можно на Дарте писать, он тоже в js компилируется, и там ещё больше статической типизации, и ещё flutter прилагается.

Кому интересно что изменилось :)

github.com/...​/compare/v3.2.47...v3.3.3

Підписатись на коментарі