Vue: питання на співбесіді рівня Middle
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
37 питань
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().
Компонент повідомляє батька про події через emit. Оголошувати події не обов'язково технічно, але дуже бажано.
<script setup>
const emit = defineEmits(['save', 'cancel']);
function onSubmit() {
emit('save', { title: title.value });
}
</script>
<PostForm @save="createPost" @cancel="close" />
Чому оголошувати:
- документація інтерфейсу: видно, які події компонент генерує, - як props для вхідних даних;
- відсутність подвійних спрацювань: неоголошена подія прокидається як нативний слухач на кореневий елемент. Якщо дитина робить
emit('click'), аclickне оголошено, обробник батька спрацює і від нативного кліку, і відemit- двічі; - TypeScript перевіряє назви й типи параметрів подій.
Валідація параметрів - об'єктний синтаксис:
const emit = defineEmits({
save: (payload) => {
if (!payload?.title) {
console.warn('Подія save без title');
return false;
}
return true;
},
cancel: null, // без перевірки
});
Невдала перевірка лише виводить попередження в режимі розробки - подія все одно відправляється. Це інструмент налагодження, а не захист.
З TypeScript - типова сигнатура, що краще за валідатори:
const emit = defineEmits<{
save: [payload: { title: string }];
cancel: [];
}>();
Іменування: у скрипті - camelCase (emit('itemSelected')), у шаблоні батька можна писати kebab-case (@item-selected) - Vue перетворює автоматично.
Чим події компонентів відрізняються від подій DOM:
- не спливають - подію дитини чує лише її безпосередній батько. Для передачі через кілька рівнів - повторний
emitна кожному рівні,provide/injectз функцією або стор; - синхронні: обробники батька виконуються в момент
emit.
emit у setup поза <script setup> - другий аргумент setup(props, { emit }).
Повна форма директиви: v-назва:аргумент.модифікатор="значення". Наприклад, v-bind:href="url", v-on:submit.prevent="save".
Динамічний аргумент - назва атрибута чи події з виразу в квадратних дужках:
<a :[attributeName]="url">...</a>
<button @[eventName]="handler">...</button>
Якщо attributeName = 'href', це те саме, що :href="url". Значення null прибирає прив'язку.
Обмеження динамічних аргументів:
- вираз має давати рядок або
null; - без пробілів і лапок усередині дужок:
:['data-' + key]- синтаксична помилка. Складні назви обчислюють уcomputed; - у шаблонах прямо в HTML-сторінці (не в
.vue-файлах) браузер переводить атрибути в нижній регістр::[someAttr]стане:[someattr]і не знайде змінну.
Прив'язка об'єкта атрибутів - v-bind без аргументу:
<input v-bind="{ id: 'email', type: 'email', required: true }">
<BaseInput v-bind="field" /> <!-- розгорнути об'єкт у props/атрибути -->
Скорочення з однаковою назвою (Vue 3.4+): якщо атрибут і змінна називаються однаково, значення можна опустити:
<img :src :alt> <!-- те саме, що :src="src" :alt="alt" -->
<UserCard :user :role />
Kebab-case атрибут шукає camelCase змінну: :data-id → dataId.
Логічні атрибути: :disabled="isSaving" - атрибут з'являється при істинному значенні й зникає при хибному (false, null, undefined). Порожній рядок '' для логічних атрибутів вважається істинним, як у HTML.
Модифікатори v-bind:
.prop- встановити як властивість DOM, а не атрибут (:value.prop, скорочено.value);.attr- навпаки, примусово як атрибут (:width.attr, скорочено^width);.camel- перетворити kebab-case на camelCase (для атрибутів SVG на кшталтviewBoxу HTML-шаблонах).
Чому це корисно на практиці: спільні компоненти-обгортки (поле форми, кнопка-посилання), що приймають набір атрибутів ззовні, і генерація форм з конфігурації, де назви полів і подій приходять з даних.
Vue сам керує DOM, але інколи потрібен прямий доступ до елемента: фокус, прокрутка, вимір розміру, ініціалізація сторонньої бібліотеки. Для цього - template ref.
Vue 3.5+ - useTemplateRef:
<script setup>
import { useTemplateRef, onMounted } from 'vue';
const input = useTemplateRef('search');
onMounted(() => input.value.focus());
</script>
<template>
<input ref="search" type="search">
</template>
Рядок у useTemplateRef('search') збігається зі значенням атрибута ref.
До 3.5 - ref з тією самою назвою, що й атрибут:
const search = ref(null); // <input ref="search">
Цей спосіб працює й далі, але useTemplateRef явніший: видно, що змінна - саме посилання на елемент, і не потрібно збігу назв змінних.
Коли ref заповнений:
- після монтування - в
setupіonBeforeMountвінnull; - під
v-ifстаєnull, коли елемент прибрано, - звідси перевіркиinput.value?.focus(); - після зміни стану DOM оновлюється асинхронно: щоб звернутися до щойно показаного елемента, потрібен
await nextTick():
async function openEditor() {
editing.value = true;
await nextTick();
editorInput.value.focus();
}
Ref у v-for дає масив елементів (порядок не гарантовано збігається з масивом даних):
const items = useTemplateRef('items'); // <li v-for="..." ref="items">
Ref-функція - для повного контролю: :ref="(el) => { if (el) map.set(id, el) }" викликається з елементом при монтуванні й з null при видаленні.
Ref на компонент дає екземпляр компонента - але з <script setup> доступне лише те, що відкрито через defineExpose.
Коли НЕ треба ref: змінювати вміст, класи, атрибути напряму через DOM (el.textContent = ..., el.classList.add). Наступний рендер Vue перезапише зміни, а стан стане неузгодженим. Усе, що можна виразити через стан і шаблон, - через стан і шаблон.
Модальне вікно логічно належить компоненту, що його відкриває (кнопка, стан, обробники - поруч). Але візуально воно має бути поверх усієї сторінки. Якщо розмітка вікна лишається глибоко в DOM усередині батьків, CSS батьків ламає позиціонування:
transform,filter,perspectiveна предку роблятьposition: fixedвідносним до цього предка, а не до вікна;overflow: hiddenобрізає вікно;z-indexобмежений контекстом накладання (stacking context) батька.
<Teleport> рендерить вміст в інше місце DOM, лишаючи його логічно частиною компонента:
<script setup>
const open = ref(false);
</script>
<template>
<button @click="open = true">Видалити акаунт</button>
<Teleport to="body">
<div v-if="open" class="fixed inset-0 z-50 grid place-items-center bg-black/50">
<div role="dialog" aria-modal="true" class="rounded bg-white p-6">
<p>Ви впевнені?</p>
<button @click="open = false">Скасувати</button>
</div>
</div>
</Teleport>
</template>
Що зберігається: реактивність, props, події, provide/inject - вміст лишається дочірнім у дереві компонентів. Змінюється лише місце в DOM.
Параметри:
to- CSS-селектор чи елемент; ціль має існувати на момент монтування;:disabled="isMobile"- рендерити на місці (наприклад, на мобільних вікно стає звичайним блоком);defer(Vue 3.5+) - відкласти пошук цілі до кінця поточного монтування, щоб ціллю міг бути елемент, який Vue рендерить пізніше в тому самому дереві.
Кілька Teleport в одну ціль дописують вміст по черзі - зручно для стеку сповіщень.
Що ще треба для якісного модального вікна (Teleport цього не робить):
- фокус: перевести фокус у вікно при відкритті, втримувати його всередині (focus trap) і повертати на кнопку після закриття;
- закриття по Escape і кліку на підкладку;
- блокування прокрутки сторінки під вікном;
role="dialog",aria-modal,aria-labelledbyдля зчитувачів екрана.
Нативна альтернатива - елемент <dialog> з showModal(): браузер сам дає верхній шар (top layer, без проблем із z-index), фокус і Escape. У сучасних застосунках це часто простіше за Teleport.
SSR: телепортований вміст рендериться окремо - його треба вставити в HTML вручну, інакше буде розбіжність при гідратації.
<Transition> анімує один елемент чи компонент, коли той з'являється або зникає через v-if, v-show, зміну динамічного компонента чи key.
<Transition name="fade">
<p v-if="visible">Збережено</p>
</Transition>
<style>
.fade-enter-active,
.fade-leave-active { transition: opacity 0.2s ease; }
.fade-enter-from,
.fade-leave-to { opacity: 0; }
</style>
Класи, які Vue додає по черзі (name - префікс):
-enter-from→-enter-active→-enter-to- поява;-leave-from→-leave-active→-leave-to- зникнення.
Vue сам визначає, коли анімація закінчилася (за transitionend/animationend), і лише тоді прибирає елемент з DOM.
З Tailwind - класи напряму через props:
<Transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="opacity-0 -translate-y-2"
leave-active-class="transition duration-150 ease-in"
leave-to-class="opacity-0"
>
Режими для заміни одного елемента іншим: mode="out-in" - спершу зникає старий, потім з'являється новий (без накладання).
<TransitionGroup> - для списків (v-for): анімує додавання, видалення й переміщення елементів:
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">{{ item.title }}</li>
</TransitionGroup>
<style>
.list-move { transition: transform 0.3s; } /* плавне переміщення при зміні порядку */
.list-leave-active { position: absolute; } /* щоб сусіди плавно зсувалися */
</style>
Обов'язково :key на кожному елементі - без нього Vue не знає, що анімувати.
JavaScript-хуки (@before-enter, @enter, @leave з колбеком done) - для анімацій бібліотеками (GSAP, Web Animations API).
Пастки:
<Transition>очікує рівно один кореневий елемент - компонент з кількома коренями всередині не анімується;- анімація не запускається при першому рендері - для цього атрибут
appear; prefers-reduced-motion- користувачі, які вимкнули анімації в системі, мають їх не бачити:@media (prefers-reduced-motion: reduce)чиmotion-reduce:у Tailwind;- для переходів між сторінками в сучасних браузерах можна використати View Transitions API.
Navigation guards - функції, що виконуються перед переходом і можуть його дозволити, скасувати чи перенаправити.
Глобальний guard з перевіркою meta маршруту:
const routes = [
{ path: '/login', name: 'login', component: Login },
{ path: '/dashboard', component: Dashboard, meta: { requiresAuth: true } },
];
router.beforeEach((to, from) => {
const auth = useAuthStore();
if (to.meta.requiresAuth && !auth.isLoggedIn) {
return { name: 'login', query: { redirect: to.fullPath } };
}
// нічого не повертати (або true) - перехід дозволено
});
Що може повернути guard:
undefined/true- продовжити;false- скасувати перехід;- маршрут (рядок чи об'єкт) - перенаправити;
- Promise - роутер дочекається результату (async-guard з запитом до API).
Третій аргумент next досі підтримується, але документація радить повертати значення: з next легко забути виклик чи викликати його двічі.
meta успадковується: перевірка to.meta.requiresAuth спрацює й для вкладених маршрутів, якщо мета задана на батьківському (точніше - to.matched.some(r => r.meta.requiresAuth) враховує всі рівні).
Інші guards:
beforeEnterу конфігурації маршруту - лише для нього;onBeforeRouteLeaveу компоненті - попередити про незбережені зміни;onBeforeRouteUpdate- коли змінюються параметри того самого маршруту;router.afterEach- аналітика, заголовок сторінки (не може скасувати перехід).
Головне застереження: guard - це зручність інтерфейсу, а не захист. Код сторінки вже завантажено в браузер, а будь-хто може змінити стан стору в devtools. Справжній захист - на сервері: API має перевіряти автентифікацію й права на кожен запит.
Пастка нескінченного перенаправлення: guard, що відправляє на /login, не повинен застосовуватися до самого /login.
Питання рівня Middle з реальних технічних співбесід - 37 питань у 9 темах, розібраних із відповідями. Нижче - теми цього рівня та сусідні рівні, якщо хочете звузити або розширити підготовку.
Готуєтесь до співбесіди не просто так: зараз на сайті 78 відкритих вакансій рівня Middle. Переглянути вакансії