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.
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 - об'єкт, обгорнутий у 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 у компонентах: деструктуризація реактивного об'єкта завжди «знімає копію».
Якщо всі сторінки імпортовані статично, збирач кладе їх в один бандл: користувач, що відкрив головну, завантажує й код адмінки, звітів і налаштувань.
Ліниве завантаження - замість компонента передається функція, що його імпортує:
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 лінивими робляться автоматично.