Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

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.

Докладніше в документації: Vue Router: навігаційні guards

При переході між маршрутами з тим самим компонентом (/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.

Докладніше в документації: Vue Router: параметри як props

На відміну від 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 = ... у звичайному коді.

Докладніше в документації: Pinia: стан

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/'), і той самий префікс у налаштуваннях збирача.

Докладніше в документації: Vue Router: режими історії