Middle: питання на співбесіді з теми «Vue Router і Pinia»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
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.
При переході між маршрутами з тим самим компонентом (/users/1 → /users/2) Vue Router не перестворює компонент - він лише оновлює route.params. Хуки onMounted і setup не виконуються вдруге, і дані першого користувача лишаються на екрані.
<script setup>
const route = useRoute();
const user = ref(null);
onMounted(async () => {
user.value = await fetchUser(route.params.id); // лише один раз!
});
</script>
Рішення 1 - стежити за параметром:
watch(
() => route.params.id,
async (id) => { user.value = await fetchUser(id); },
{ immediate: true },
);
Рішення 2 - guard оновлення в компоненті:
onBeforeRouteUpdate(async (to) => {
user.value = await fetchUser(to.params.id);
});
Рішення 3 - примусово перестворювати компонент через key:
<RouterView :key="$route.fullPath" />
Просто, але дорого: при кожній зміні query перемонтовується вся сторінка, губиться її стан.
Параметри як props - компонент не залежить від роутера:
{ path: '/users/:id', component: UserProfile, props: true }
// перетворення: число замість рядка, плюс query
{
path: '/search',
component: SearchPage,
props: (route) => ({ query: route.query.q, page: Number(route.query.page ?? 1) }),
}
<script setup>
const props = defineProps({ id: String });
watch(() => props.id, loadUser, { immediate: true });
</script>
Переваги props:
- компонент можна використати поза роутером і легко тестувати - достатньо передати props;
- приведення типів і значення за замовчуванням в одному місці (функція
props).
Проблема «компонент не оновився» з props не зникає - стежити за зміною все одно треба, але через props.id.
На відміну від Vuex, у Pinia немає мутацій - стан можна змінювати кількома способами.
1. Пряме присвоєння - найпростіше:
const cart = useCartStore();
cart.coupon = 'SALE10';
cart.items.push(item);
2. $patch з об'єктом - кілька полів за раз:
cart.$patch({ coupon: 'SALE10', deliveryMethod: 'courier' });
3. $patch з функцією - для масивів і складних змін:
cart.$patch((state) => {
state.items = state.items.filter((i) => i.qty > 0);
state.updatedAt = Date.now();
});
Об'єктна форма для масивів незручна: щоб видалити елемент, довелося б передати весь новий масив. Функція також об'єднує кілька змін в один запис у devtools.
4. Дії - для змін з логікою, перевірками чи запитами до API. Правило: якщо зміна має бізнес-сенс («оформити замовлення»), вона - дія стору, а не набір присвоєнь у компоненті.
$reset() - повернути початковий стан:
- в option store вбудований: викликає
state()заново; - у setup store його немає - пишуть самі:
export const useFiltersStore = defineStore('filters', () => {
const initial = () => ({ city: null, salaryFrom: null });
const filters = ref(initial());
function $reset() {
filters.value = initial();
}
return { filters, $reset };
});
$subscribe - реагувати на будь-яку зміну стану:
cart.$subscribe((mutation, state) => {
localStorage.setItem('cart', JSON.stringify(state.items));
}, { detached: true });
- за замовчуванням підписка знімається разом із компонентом;
detached: true- лишити; flush: 'sync'- викликати після кожної зміни, а не раз після групи;- для дій є окремий
$onAction- до і після виконання, з обробкою помилок.
Замінити стан повністю - pinia.state.value = {...} (для SSR і тестів), а не cart.$state = ... у звичайному коді.
Vue Router підтримує кілька способів зберігати маршрут в URL.
createWebHistory() - звичайні адреси через History API:
https://example.com/users/7
- красиві URL, які індексують пошукові системи;
- потребує налаштування сервера. Перехід усередині застосунку обробляє JavaScript, але якщо користувач оновить сторінку чи відкриє посилання напряму, браузер запитає в сервера
/users/7. Сервер, який про такий маршрут не знає, поверне 404.
createWebHashHistory() - маршрут після #:
https://example.com/#/users/7
- частина після
#на сервер не надсилається - сервер завжди віддаєindex.html, нічого налаштовувати не треба; - погано для SEO і виглядає застаріло. Доречно для внутрішніх інструментів, статичних хостингів без налаштувань, Electron.
createMemoryHistory() - без URL узагалі: для SSR на сервері й тестів.
Налаштування сервера для createWebHistory - будь-який невідомий шлях віддає точку входу SPA:
location / {
try_files $uri $uri/ /index.html;
}
У Laravel, якщо SPA живе всередині застосунку:
// routes/web.php - останнім, після всіх інших маршрутів
Route::view('/app/{any?}', 'spa')->where('any', '.*');
Пастки:
- справжні 404 зникають: сервер на будь-яку адресу віддає 200 з SPA. Сторінку «не знайдено» робить роутер (
/:pathMatch(.*)*), але HTTP-статус лишається 200 - для SEO це «м'які 404»; - catch-all маршрут має бути останнім і не перехоплювати
/api/*, інакше запити до API отримають HTML; base- якщо застосунок не в корені домену:createWebHistory('/app/'), і той самий префікс у налаштуваннях збирача.