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

Junior: питання на співбесіді з теми «Vue Router і Pinia»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

4 питання

Vue Router зіставляє адресу в браузері з компонентом і показує його без перезавантаження сторінки.

import { createRouter, createWebHistory } from 'vue-router';

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/users/:id', name: 'user', component: UserProfile },
    { path: '/:pathMatch(.*)*', component: NotFound },   // усе інше - 404
  ],
});

app.use(router);
<template>
  <nav>
    <RouterLink to="/">Головна</RouterLink>
    <RouterLink :to="{ name: 'user', params: { id: 7 } }">Профіль</RouterLink>
  </nav>
  <RouterView />   <!-- тут рендериться компонент поточного маршруту -->
</template>

Динамічні параметри. :id у шляху - параметр. У компоненті він доступний через useRoute():

<script setup>
import { useRoute, useRouter } from 'vue-router';

const route = useRoute();    // поточний маршрут: params, query, hash, meta
const router = useRouter();  // навігація з коду

console.log(route.params.id);          // '7' - завжди рядок
router.push({ name: 'user', params: { id: 8 } });
</script>

Що варто знати:

  • параметри - рядки. route.params.id === 7 хибне - треба Number(route.params.id);
  • іменовані маршрути (name: 'user') надійніші за рядкові шляхи: при зміні URL посилання в коді не ламаються;
  • RouterLink замість <a href> - інакше браузер перезавантажить сторінку;
  • query-параметри (?page=2) - у route.query, вони не входять у шаблон шляху.

Vue Router 5 зберіг API четвертої версії й додав маршрутизацію на основі файлів (колишній unplugin-vue-router): маршрути генеруються зі структури каталогу src/pages, а route.params отримують точні типи в TypeScript.

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

Pinia - офіційний стор для Vue: спільний стан, до якого мають доступ будь-які компоненти без передачі props через десять рівнів. Наступник Vuex, без мутацій і з повною підтримкою TypeScript.

Option store - схожий на Options API:

import { defineStore } from 'pinia';

export const useCartStore = defineStore('cart', {
  state: () => ({ items: [] }),
  getters: {
    total: (state) => state.items.reduce((sum, item) => sum + item.price * item.qty, 0),
  },
  actions: {
    add(product) {
      this.items.push({ ...product, qty: 1 });
    },
  },
});

Setup store - функція в стилі Composition API:

export const useCartStore = defineStore('cart', () => {
  const items = ref([]);
  const total = computed(() => items.value.reduce((sum, i) => sum + i.price * i.qty, 0));

  function add(product) {
    items.value.push({ ...product, qty: 1 });
  }

  return { items, total, add };
});

ref стає станом, computed - гетером, функції - діями.

Використання однакове:

<script setup>
const cart = useCartStore();
</script>

<template>
  <span>{{ cart.items.length }} товарів на {{ cart.total }} грн</span>
  <button @click="cart.add(product)">У кошик</button>
</template>

Відмінності:

  • setup store гнучкіший: у ньому можна використовувати watch, інші composables, інжектовані значення;
  • у setup store треба повернути весь стан - неповернений ref не потрапить у devtools, SSR і плагіни;
  • $reset() вбудований лише в option store; у setup store його пишуть вручну;
  • option store простіший для новачків і для тих, хто переходить з Vuex.

Перший аргумент defineStore - унікальний id; два стори з однаковим id конфліктують.

Pinia 4 змінила лише збірку: пакет тепер тільки ESM, а @vue/devtools-api встановлюється окремо. API сторів той самий.

Докладніше в документації: Pinia: визначення стору

Стор Pinia - об'єкт, обгорнутий у reactive(). Звичайна деструктуризація копіює поточні значення в локальні змінні - і зв'язок з реактивністю зникає:

const cart = useCartStore();
const { items, total } = cart;   // items і total більше не оновлюються

Після cart.add(product) в інтерфейсі нічого не зміниться: total лишився числом, обчисленим у момент деструктуризації.

storeToRefs() перетворює стан і гетери на ref, що лишаються пов'язаними зі стором:

import { storeToRefs } from 'pinia';

const cart = useCartStore();
const { items, total } = storeToRefs(cart);   // реактивні ref
const { add, clear } = cart;                  // дії - звичайною деструктуризацією
<template>
  <p>Разом: {{ total }} грн</p>   <!-- оновлюється -->
</template>

Чому дії деструктурують без storeToRefs: це звичайні функції, прив'язані до стору. storeToRefs() їх пропускає - він повертає лише стан і гетери.

Що ще варто знати:

  • у скрипті значення з storeToRefs - це ref: total.value, у шаблоні - просто total;
  • зміна items.value = [] змінює стан стору, а не локальну копію - це двосторонній зв'язок;
  • toRefs() з Vue на сторі теж «працює», але перетворює на ref і дії, і внутрішні властивості - для сторів потрібен саме storeToRefs();
  • найпростіший варіант без пасток - не деструктурувати взагалі й звертатися через cart.total.

Та сама проблема виникає з reactive() і з props у компонентах: деструктуризація реактивного об'єкта завжди «знімає копію».

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

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

Ліниве завантаження - замість компонента передається функція, що його імпортує:

const routes = [
  { path: '/', component: () => import('./pages/Home.vue') },
  { path: '/reports', component: () => import('./pages/Reports.vue') },
  {
    path: '/admin',
    component: () => import('./layouts/AdminLayout.vue'),
    children: [
      { path: 'users', component: () => import('./pages/admin/Users.vue') },
    ],
  },
];

Vite бачить динамічний import() і виносить кожну сторінку в окремий файл. Він завантажується лише при першому переході на маршрут, а далі береться з кешу.

Що це дає:

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

Пастки:

  • не використовуйте defineAsyncComponent для маршрутів - роутер сам уміє чекати на функцію-імпорт; defineAsyncComponent - для асинхронних компонентів усередині сторінок;
  • помилка завантаження частини після деплою. Користувач з давно відкритою вкладкою переходить на маршрут, а файл старої збірки вже видалено з сервера. Варто обробити router.onError() і перезавантажити сторінку, а Vite генерує подію vite:preloadError;
  • затримка першого переходу - для очікуваних маршрутів частину можна підвантажити заздалегідь (при наведенні на посилання);
  • не дробити надто сильно: окремий файл для кожного дрібного маршруту - багато дрібних запитів. Пов'язані сторінки можна групувати.

У маршрутизації на основі файлів (Vue Router 5) сторінки з src/pages лінивими робляться автоматично.

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