Питання на співбесіді з Vue
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
108 питань
Стандартний набір для тестування Vue-компонентів:
- Vitest - запускач тестів на основі Vite: та сама конфігурація, що й у проєкті, підтримка
.vue-файлів і TypeScript без додаткових налаштувань; - Vue Test Utils (
@vue/test-utils) - монтує компонент, дає змогу знаходити елементи, взаємодіяти з ними й перевіряти результат; - jsdom чи happy-dom - імітація DOM браузера в Node.js.
npm i -D vitest @vue/test-utils jsdom
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: { environment: 'jsdom' },
})
Тест:
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Counter from './Counter.vue'
describe('Counter', () => {
it('starts from the given value and increments', async () => {
const wrapper = mount(Counter, { props: { start: 2 } })
expect(wrapper.get('[data-test="count"]').text()).toBe('2')
await wrapper.get('button').trigger('click')
expect(wrapper.get('[data-test="count"]').text()).toBe('3')
})
})
Ключові моменти:
mountрендерить компонент разом з усіма дочірніми;propsпередають значення як від батька; також єslotsіglobal(плагіни, заглушки, provide);triggerповертає Promise -awaitпотрібен, щоб Vue встиг оновити DOM;getкидає помилку, якщо елемент не знайдено (з зрозумілим повідомленням),findповертає порожню обгортку - зручно для перевірки відсутності:expect(wrapper.find('.error').exists()).toBe(false).
Атрибути data-test для пошуку елементів кращі за класи й структуру: стилі й розмітка змінюються, а тест - ні.
Що перевіряти: те, що бачить і робить користувач - текст, видимі елементи, події, виклики API, - а не внутрішні змінні компонента. Тоді рефакторинг не ламає тести.
Запуск: npx vitest (режим спостереження) чи npx vitest run у CI.
Введення в поле - setValue, клік чи інша подія - trigger. Обидва повертають Promise, який завершується після оновлення DOM:
const wrapper = mount(SearchForm)
await wrapper.get('input[name="query"]').setValue('laravel')
await wrapper.get('select').setValue('php')
await wrapper.get('input[type="checkbox"]').setValue(true)
await wrapper.get('form').trigger('submit.prevent')
setValue встановлює значення і генерує відповідну подію (input чи change), тож v-model спрацює як у браузері.
Клавіші й модифікатори:
await wrapper.get('input').trigger('keydown', { key: 'Enter' })
await wrapper.get('input').trigger('keydown.enter')
Перевірка подій компонента - emitted():
await wrapper.get('[data-test="save"]').trigger('click')
expect(wrapper.emitted()).toHaveProperty('save')
expect(wrapper.emitted('save')).toHaveLength(1)
expect(wrapper.emitted('save')![0]).toEqual([{ id: 7, title: 'Новий' }])
emitted('save') - масив викликів, кожен виклик - масив аргументів. Тому для emit('changed', 3) результат [[3]], а не [3].
v-model на власному компоненті перевіряється через подію оновлення:
expect(wrapper.emitted('update:modelValue')!.at(-1)).toEqual(['нове значення'])
Що варто пам'ятати:
awaitпісляtrigger/setValueобов'язковий, якщо далі перевіряється DOM. Без нього тест бачить старий стан;- запити й інші Promise всередині обробника можуть завершитися пізніше за наступний тік рендеру - тоді потрібен
flushPromises(); - вимкнені елементи:
triggerна кнопці зdisabledподію не генерує - це відповідає браузеру і зручно для перевірки, що заблокована кнопка нічого не робить; - події, яких не має бути, перевіряються так само:
expect(wrapper.emitted('save')).toBeUndefined().
Перевіряйте результат, а не виклик методу. Тест «після кліку з'явилось повідомлення й пішла подія» витримає рефакторинг, а «викликався метод onSave» - ні.
Props передають при монтуванні й змінюють пізніше через setProps:
const wrapper = mount(PriceTag, {
props: { amount: 1250, currency: 'UAH' },
})
expect(wrapper.text()).toContain('1 250')
await wrapper.setProps({ amount: 99 })
expect(wrapper.text()).toContain('99')
setProps повертає Promise - після await DOM уже оновлений. Так перевіряють, що компонент правильно реагує на зміну props, а не лише на початкові значення.
Слоти - рядками з розміткою, компонентами чи функціями рендеру:
const wrapper = mount(Card, {
slots: {
default: '<p>Основний вміст</p>',
header: '<h2>Заголовок</h2>',
footer: FooterActions,
},
})
expect(wrapper.get('header').text()).toBe('Заголовок')
expect(wrapper.html()).toContain('Основний вміст')
Scoped slots - шаблон отримує параметри слоту через params:
const wrapper = mount(DataList, {
props: { items: [{ id: 1, name: 'Оля' }] },
slots: {
item: '<span class="name">{{ params.item.name }}</span>',
},
})
expect(wrapper.get('.name').text()).toBe('Оля')
Так перевіряється контракт слоту: які дані компонент передає назовні.
Атрибути, що не є props (class, id, data-*), перевіряються на корені:
const wrapper = mount(BaseButton, { attrs: { class: 'w-full', 'data-test': 'submit' } })
expect(wrapper.classes()).toContain('w-full')
Що варто перевіряти:
- граничні значення props: порожній масив,
null, дуже довгий текст, нуль - саме там живуть баги; - відсутній слот: компонент має коректно рендеритися, якщо необов'язковий слот не передали (і, наприклад, не показувати порожню обгортку);
- заглушка за замовчуванням у слоті показується, коли вміст не передано.
Чого не варто: перевіряти, що prop «дійшов» у внутрішню змінну через wrapper.vm - це деталь реалізації. Перевіряйте вивід.
Найпростіший підхід - один реактивний об'єкт з усіма полями форми і v-model на кожному полі:
<script setup lang="ts">
import { reactive, ref } from 'vue'
const initial = () => ({ name: '', email: '', role: 'editor', agree: false })
const form = reactive(initial())
const submitting = ref(false)
async function submit() {
submitting.value = true
try {
await api.post('/users', { ...form })
Object.assign(form, initial()) // скидання
} finally {
submitting.value = false
}
}
</script>
<template>
<form @submit.prevent="submit">
<input v-model.trim="form.name" />
<input v-model="form.email" type="email" />
<select v-model="form.role">
<option value="editor">Редактор</option>
<option value="admin">Адміністратор</option>
</select>
<input v-model="form.agree" type="checkbox" />
<button :disabled="submitting">Зберегти</button>
</form>
</template>
reactive чи ref:
reactive({...}) |
ref({...}) |
|
|---|---|---|
доступ у <script> |
form.email |
form.value.email |
| замінити весь об'єкт | ні - лише Object.assign |
так: form.value = initial() |
| деструктуризація | ламає реактивність | теж ламає (form.value - реактивний об'єкт) |
ref зручніший, коли форму треба повністю замінити (завантажили дані для редагування). reactive - коротший запис полів.
Модифікатори v-model: .trim прибирає пробели, .number перетворює на число, .lazy оновлює значення на change, а не на кожен введений символ.
Типові помилки:
@submitбез.prevent- браузер відправляє форму звичайним запитом і перезавантажує сторінку;- скидання через
form = initial()дляreactive- зміниться лише локальна змінна, а шаблон лишиться прив'язаним до старого об'єкта; - відправка самого реактивного проксі - бібліотеки зазвичай серіалізують його коректно, але безпечніше передавати звичайну копію (
{ ...form }чиtoRaw(form)); - checkbox для масиву: кілька чекбоксів з одним
v-modelі масивом у стані дають список вибраних значень; - немає захисту від подвійного кліку - кнопка має бути вимкнена на час відправки.
Для складних форм (валідація, вкладені масиви, стан «змінено») варто взяти бібліотеку форм, а не винаходити все самому.
Коли валідація Laravel не проходить у запиті, що очікує JSON, сервер відповідає статусом 422 і тілом:
{
"message": "The email has already been taken. (and 1 more error)",
"errors": {
"email": ["The email has already been taken."],
"items.0.qty": ["The items.0.qty field must be at least 1."]
}
}
Ключі - назви полів, для вкладених даних - через крапку. Значення - масив повідомлень.
Обробка в Vue:
<script setup lang="ts">
import { reactive, ref } from 'vue'
import axios from 'axios'
const form = reactive({ name: '', email: '' })
const errors = ref<Record<string, string[]>>({})
async function submit() {
errors.value = {}
try {
await axios.post('/api/users', form, { headers: { Accept: 'application/json' } })
} catch (e) {
if (axios.isAxiosError(e) && e.response?.status === 422) {
errors.value = e.response.data.errors
return
}
throw e // інші помилки - далі, до глобального обробника
}
}
</script>
<template>
<input v-model="form.email" :aria-invalid="!!errors.email" />
<p v-if="errors.email" class="error">{{ errors.email[0] }}</p>
</template>
З fetch код схожий, але fetch не кидає виняток на 422 - перевіряти треба response.status самостійно, а тіло читати через await response.json().
Важливі деталі:
- заголовок
Accept: application/jsonобов'язковий: без нього Laravel вважає запит звичайним переходом форми й відповідає редиректом назад з помилками в сесії, а не JSON; - очищайте помилки перед новою відправкою, інакше старі повідомлення лишаються на екрані;
- прибирайте помилку поля при його зміні -
watchна поле чи очищення в@input- користувач бачить, що виправлення прийнято; - вкладені ключі (
items.0.qty) треба зіставити з полем у списку - зберігайте помилки за тим самим шаблоном назв; - 401 і 419 - це не валідація: 401 - не автентифікований, 419 - прострочений CSRF-токен (для SPA на Sanctum спершу потрібен запит до
/sanctum/csrf-cookie).
Типізація: опишіть тип відповіді 422 ({ message: string; errors: Record<string, string[]> }) один раз і використовуйте в усіх формах - через спільну функцію чи composable useServerErrors().
Доступна форма - та, яку можна заповнити з клавіатури й зрозуміти з екранним читачем. Більшість потрібного дає правильний HTML, а Vue лише має не зламати його.
1. Мітка для кожного поля, пов'язана з полем:
<script setup>
import { useId } from 'vue'
const id = useId()
</script>
<template>
<label :for="id">Email</label>
<input :id="id" v-model="form.email" type="email" autocomplete="email" required />
</template>
Плейсхолдер - не мітка: він зникає при введенні й часто має низький контраст. useId() (Vue 3.5) генерує стабільний id, однаковий на сервері й у браузері при SSR.
2. Помилка, пов'язана з полем:
<input
:id="id"
v-model="form.email"
:aria-invalid="errors.email ? 'true' : undefined"
:aria-describedby="errors.email ? `${id}-error` : undefined"
/>
<p v-if="errors.email" :id="`${id}-error`">{{ errors.email[0] }}</p>
aria-invalidповідомляє, що значення некоректне;aria-describedby- екранний читач прочитає текст помилки разом з назвою поля;- помилка не повинна передаватися лише кольором (червона рамка) - потрібен текст.
3. Фокус на першому полі з помилкою після відправки:
import { nextTick } from 'vue'
async function onInvalid() {
await nextTick() // дочекатися рендеру повідомлень
document.querySelector<HTMLElement>('[aria-invalid="true"]')?.focus()
}
Без цього користувач клавіатури чи екранного читача натискає «Зберегти» - і нічого не відбувається з його точки зору.
4. Зведення помилок для довгих форм: блок з role="alert" нагорі зі списком помилок-посилань на поля.
Ще кілька правил:
<button type="submit">у<form>, а неdivз@click- тоді працює Enter і екранні читачі розуміють, що це форма;- групи перемикачів - у
<fieldset>з<legend>; autocomplete(email,new-password,one-time-code) - менеджери паролів і мобільні клавіатури;- стан відправки -
aria-busyчи текст на кнопці, а не лише спінер; - не вимикайте кнопку відправки до заповнення всіх полів: користувач не розуміє, чого бракує. Краще дозволити натиснути й показати помилки.
Перевірка: пройти форму лише з клавіатури (Tab, Enter, Space) і запустити axe DevTools чи Lighthouse.
watch(source, callback)стежить за явно вказаним джерелом. Колбек отримує нове й старе значення і за замовчуванням не запускається одразу - лише при першій зміні.watchEffect(callback)запускається одразу і сам відстежує всі реактивні значення, які прочитав під час виконання. Старого значення немає.
watch(userId, async (id, oldId) => {
user.value = await fetchUser(id);
});
watchEffect(async () => {
user.value = await fetchUser(userId.value); // залежність від userId знайдена сама
});
Опції watch:
immediate: true- викликати колбек одразу з поточним значенням, якwatchEffect.deep: true- реагувати на зміни вкладених полів об'єкта, а не лише на заміну всього значення. Заreactive-об'єктомwatchстежить глибоко автоматично. З Vue 3.5deepприймає й число - глибину обходу.once: true(3.4+) - спрацювати лише раз.
Нюанси:
watchEffectвідстежує лише те, що прочитано синхронно до першогоawait. Значення, прочитані після, у залежності не потрапляють.deepна великій структурі - дорогий обхід при кожній зміні. Краще стежити за конкретним полем:watch(() => form.email, ...).- Гонки: якщо
userIdзмінився двічі швидко, відповіді можуть прийти не в тому порядку.onCleanup(чиonWatcherCleanupу 3.5) дозволяє скасувати попередній запит.
Правило: watch - коли важливо, що саме змінилося і нові/старі значення; watchEffect - коли ефект просто має відповідати поточному стану.
Реактивність Vue працює через проксі: Vue знає про зміну, бо зміна йде через проксі (state.count = 1). Деструктуризація копіює значення полів у звичайні змінні - зв'язок із проксі втрачається.
const state = reactive({ count: 0, name: 'Оля' });
const { count } = state; // просто число 0
state.count++; // count лишився 0
Як уникнути:
toRefsперетворює кожне поле наref, пов'язаний з початковим об'єктом:
const { count, name } = toRefs(state);
count.value++; // змінює state.count
toRef- для одного поля:const count = toRef(state, 'count').- Повертати з composables
ref-и, а неreactive, - тоді деструктуризація на місці виклику безпечна.
Props: props - теж реактивний об'єкт. До Vue 3.5 const { title } = defineProps(...) давав нереактивну змінну. З Vue 3.5 деструктуризація defineProps у <script setup> реактивна: компілятор сам перетворює title на props.title. Але передати таку змінну у функцію чи watch напряму не вийде - потрібен геттер: watch(() => title, ...).
Ще місця, де реактивність губиться:
- Заміна всього
reactive-об'єкта:state = reactive({...}). - Передача примітиву в функцію:
useSomething(state.count)передає число, а не реактивне джерело. ПередавайтеtoRef(state, 'count')чи геттер() => state.count.
Ці утиліти розв'язують одну проблему: як передати реактивне значення далі, не втративши реактивності.
toRefs(reactiveObj) - перетворює кожну властивість reactive-об'єкта на ref, пов'язаний з оригіналом. Головне застосування - повернути стан з composable так, щоб його можна було деструктуризувати:
function useMouse() {
const state = reactive({ x: 0, y: 0 });
// ...
return toRefs(state);
}
const { x, y } = useMouse(); // x і y - ref-и, реактивні
toRef - один ref:
const title = toRef(props, 'title'); // ref, що читає props.title
const fullName = toRef(() => `${props.first} ${props.last}`); // ref тільки для читання з геттера
toValue(source) (3.3+) - «розгорнути що завгодно»: ref - .value, геттер - результат виклику, звичайне значення - як є.
Composable, що приймає ref, геттер або значення - тип MaybeRefOrGetter:
import { ref, watchEffect, toValue, type MaybeRefOrGetter } from 'vue';
export function useFetch<T>(url: MaybeRefOrGetter<string>) {
const data = ref<T | null>(null);
watchEffect(async () => {
data.value = null;
const response = await fetch(toValue(url)); // читання всередині ефекту = залежність
data.value = await response.json();
});
return { data };
}
useFetch('/api/users'); // рядок
useFetch(userUrl); // ref
useFetch(() => `/api/users/${props.id}`); // геттер від props
Чому toValue треба викликати всередині watchEffect/computed: саме там Vue відстежує читання. Якщо розгорнути значення один раз на початку composable, зміни props.id вже не підхопляться.
Типова помилка з props:
useFetch(`/api/users/${props.id}`); // рядок обчислено один раз - не реактивно
useFetch(() => `/api/users/${props.id}`); // правильно
unref - старший аналог toValue, що не викликає геттери. У новому коді краще toValue.
До Vue 3.5 деструктуризація props губила реактивність: const { title } = defineProps(...) давала звичайне значення на момент створення компонента.
З Vue 3.5 деструктуризація з defineProps реактивна:
<script setup>
const { title, count = 0 } = defineProps(['title', 'count']);
</script>
<template>
<h1>{{ title }}</h1>
<p>{{ count }}</p>
</template>
Компілятор <script setup> переписує звернення title на __props.title, тож значення завжди актуальне. Заодно це найзручніший спосіб задати значення за замовчуванням - звичайним синтаксисом JavaScript замість withDefaults.
Пастка - передача у функцію чи watch:
const { id } = defineProps(['id']);
watch(id, () => { /* ... */ }); // помилка компіляції: передано значення, а не джерело
watch(() => id, () => { /* ... */ }); // правильно: геттер
useFetch(id); // composable отримає число, а не реактивне джерело
useFetch(() => id); // правильно
Деструктуризована змінна виглядає як звичайна змінна, але реактивна лише там, де компілятор може переписати звернення. Передаючи її кудись, треба загорнути в геттер, - інакше функція отримає «знімок» значення.
Спостереження за props без деструктуризації:
const props = defineProps(['id', 'filters']);
watch(() => props.id, (id) => load(id));
watch(() => props.filters, reload, { deep: true }); // об'єкт - глибоко
watch(props.id, ...) - та сама помилка: значення замість джерела.
Що варто знати:
- props змінювати не можна - вони належать батьківському компоненту. Для локальної копії -
ref(props.initial)(свідомо не реактивна до змін батька) абоcomputed; - об'єкти в props передаються за посиланням, і зміна їх полів технічно спрацює, але порушує односпрямований потік даних - батько не знатиме, хто змінив його стан;
- для
v-modelна компоненті -defineModel(), а не зміна props.
Типова задача: при зміні id завантажити дані. Якщо id змінюється швидко (користувач клацає по списку), запити летять паралельно, і відповідь на старий запит може прийти пізніше за новий - на екрані опиняться дані не того запису.
watch(id, async (newId) => {
const response = await fetch(`/api/users/${newId}`);
user.value = await response.json(); // може перезаписати свіжі дані старими
});
Очищення побічного ефекту. Перед наступним запуском спостерігача (і при його зупинці) Vue викликає функцію очищення - туди кладуть скасування попереднього запиту:
import { watch, onWatcherCleanup } from 'vue';
watch(id, async (newId) => {
const controller = new AbortController();
onWatcherCleanup(() => controller.abort()); // Vue 3.5+
try {
const response = await fetch(`/api/users/${newId}`, { signal: controller.signal });
user.value = await response.json();
} catch (error) {
if (error.name !== 'AbortError') throw error;
}
});
Важливо: onWatcherCleanup треба викликати синхронно, до першого await - після нього Vue вже не знає, до якого спостерігача він належить.
До Vue 3.5 - третій аргумент колбеку:
watch(id, async (newId, oldId, onCleanup) => {
const controller = new AbortController();
onCleanup(() => controller.abort());
// ...
});
Цей варіант працює й зараз і не має обмеження щодо await.
Інші застосування очищення: зняти таймер (clearInterval), відписатися від WebSocket-каналу, знищити екземпляр сторонньої бібліотеки, створений для попереднього значення.
Зупинка спостерігача:
- спостерігачі, створені синхронно в
setup, зупиняються автоматично разом з компонентом; - створені асинхронно (після
awaitчи вsetTimeout) - ні: їх треба зупинити вручну, інакше витік пам'яті:
const stop = watch(source, callback);
stop();
У Vue 3.5 повернений об'єкт має ще й pause() і resume().
Альтернатива для даних, що залежать від параметрів, - бібліотеки на кшталт TanStack Query, що вже розв'язують гонитву, кешування й повтори.
Докладніше в документації: Спостерігачі: очищення побічних ефектів
Composable - функція, яка використовує Composition API (ref, computed, watch, хуки життєвого циклу) і повертає стан та методи. Спосіб винести логіку зі стейтом із компонента й перевикористати її.
// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue';
export function useMouse() {
const x = ref(0);
const y = ref(0);
const update = (event) => { x.value = event.pageX; y.value = event.pageY; };
onMounted(() => window.addEventListener('mousemove', update));
onUnmounted(() => window.removeEventListener('mousemove', update));
return { x, y };
}
const { x, y } = useMouse();
Чим це краще за mixins з Vue 2:
- Зрозуміле джерело. З mixin незрозуміло, звідки в компоненті взялося
this.isLoading. З composable видно:const { isLoading } = useFetch(...). - Немає конфліктів імен. Два mixin з полем
dataтихо перезаписують одне одне. Результати composable можна перейменувати при деструктуризації. - Параметри. Composable приймає аргументи, зокрема реактивні:
useFetch(() =>/api/users/${id.value}). - Типізація працює природно, бо це звичайні функції.
Правила: назва з use, виклик синхронно в setup (щоб хуки життєвого циклу прив'язалися до компонента), повертати ref-и, щоб деструктуризація не губила реактивність.
Готові composables для типових задач є в бібліотеці VueUse.
Слот - місце в шаблоні компонента, куди батько вставляє свою розмітку. Компонент задає рамку, а вміст вирішує той, хто його використовує.
<!-- Card.vue -->
<div class="card">
<header><slot name="title" /></header>
<slot /> <!-- слот за замовчуванням -->
</div>
<Card>
<template #title>Замовлення №42</template>
<p>Деталі замовлення...</p>
</Card>
Scoped slot - слот, у який компонент передає дані назад батькові. Компонент знає дані, а батько - як їх показати.
<!-- DataTable.vue -->
<tr v-for="row in rows" :key="row.id">
<slot name="row" :row="row" :selected="selectedId === row.id" />
</tr>
<DataTable :rows="orders">
<template #row="{ row, selected }">
<td :class="{ 'font-bold': selected }">{{ row.number }}</td>
<td>{{ formatMoney(row.total) }}</td>
</template>
</DataTable>
Навіщо: таблиці, списки, випадаючі меню й інші компоненти з логікою (сортування, вибір, пагінація), але без жорстко заданого вигляду. Це той самий патерн, що render props у React.
Корисне:
$slots.title(чиuseSlots()) дозволяє перевірити, чи передали слот, і не рендерити порожню обгортку.- Вміст слота компілюється в області батька: він бачить змінні батька, а дані дитини - лише ті, що передані через scoped slot.
Батько отримує доступ до дочірнього компонента через template ref:
<script setup>
import { useTemplateRef } from 'vue';
import VideoPlayer from './VideoPlayer.vue';
const player = useTemplateRef('player'); // Vue 3.5+; раніше - const player = ref(null)
function startOver() {
player.value?.seek(0);
player.value?.play();
}
</script>
<template>
<VideoPlayer ref="player" />
<button @click="startOver">Спочатку</button>
</template>
Але компоненти з <script setup> закриті за замовчуванням: через ref батько не бачить нічого - ні змінних, ні функцій дитини. Те, що можна викликати ззовні, дитина оголошує явно:
<!-- VideoPlayer.vue -->
<script setup>
import { useTemplateRef } from 'vue';
const video = useTemplateRef('video');
function play() { video.value.play(); }
function seek(seconds) { video.value.currentTime = seconds; }
defineExpose({ play, seek });
</script>
<template>
<video ref="video" src="/intro.mp4"></video>
</template>
Це навмисне рішення: публічний інтерфейс компонента - props, events, slots і явно відкриті методи, а не будь-яка внутрішня змінна.
Коли виклик методу дитини доречний - імперативні дії, які погано виражаються через стан:
- фокус на полі (
input.focus()), прокрутка до елемента; - керування медіа (play, pause, seek);
- відкриття модального вікна сторонньої бібліотеки, скидання форми.
Коли краще інакше: якщо батько змінює дані дитини - це props чи v-model. Виклик методів для синхронізації стану робить потік даних непрозорим: незрозуміло, хто й коли змінив стан.
Що варто знати:
- ref заповнюється після монтування - у
setupбатька він щеnull; звертатися вonMountedчи обробниках подій; - ref на компонент усередині
v-ifстаєnull, коли компонент прибрано, - звідси?.; refуv-forдає масив екземплярів;- TypeScript: тип відкритого API береться з
defineExpose-useTemplateRef<InstanceType<typeof VideoPlayer>>('player')чи автоматичне виведення в Vue 3.5 з Volar.
v-model на компоненті - це скорочення для пари «prop + подія оновлення». З Vue 3.4 для цього є макрос defineModel():
<!-- QuantityInput.vue -->
<script setup>
const quantity = defineModel({ type: Number, default: 1 });
</script>
<template>
<button type="button" @click="quantity--" :disabled="quantity <= 1">-</button>
<span>{{ quantity }}</span>
<button type="button" @click="quantity++">+</button>
</template>
<QuantityInput v-model="item.qty" />
defineModel() повертає ref: читання дає значення від батька, запис - надсилає подію update:modelValue, і батько оновлює свою змінну.
Що генерує компілятор (так писали до 3.4 і це варто розуміти):
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
// v-model="item.qty" на батьку = :modelValue="item.qty" @update:modelValue="v => item.qty = v"
Кілька v-model на одному компоненті - іменовані моделі:
<script setup>
const firstName = defineModel('firstName');
const lastName = defineModel('lastName');
</script>
<UserName v-model:first-name="user.first" v-model:last-name="user.last" />
Модифікатори (власні чи вбудовані на кшталт .trim) читаються з другого елемента й обробляються в set:
const [title, modifiers] = defineModel({
set(value) {
return modifiers.capitalize ? value.charAt(0).toUpperCase() + value.slice(1) : value;
},
});
Пастки:
- значення за замовчуванням в
defineModel({ default: ... })і відсутністьv-modelна батьку: дитина бачить своє значення, а батько -undefined. Стан розсинхронізований; - об'єкт у моделі: зміна поля
model.value.name = 'x'змінює об'єкт батька напряму, без події. Для об'єктів правильніше присвоювати новий об'єкт (model.value = { ...model.value, name: 'x' }) або мати окремі моделі на поля; - для нативного поля всередині - простіше прокинути
v-modelдалі:<input v-model="value">, деvalue = defineModel().
Питання з реальних технічних співбесід - 108 питань у 9 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.
Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії