Питання на співбесіді з Vue
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
108 питань
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 лінивими робляться автоматично.
Inertia - «клей» між серверним фреймворком (Laravel) і фронтенд-фреймворком (Vue, React, Svelte). Вона дає досвід SPA, але без окремого API.
Класична SPA з API:
- Laravel - REST/JSON API, автентифікація токенами чи Sanctum;
- Vue - власний роутер, стор, запити до API, обробка помилок;
- дві кодові бази, дублювання валідації, контрактів, маршрутів.
Inertia:
- маршрути, контролери, middleware, авторизація, валідація - звичайні в Laravel;
- контролер замість Blade-шаблону повертає Vue-компонент сторінки з даними:
public function index()
{
return Inertia::render('Users/Index', [
'users' => User::query()->latest()->paginate(20),
]);
}
<!-- resources/js/pages/Users/Index.vue -->
<script setup>
defineProps({ users: Object });
</script>
Як це працює:
- перше завантаження - звичайний HTML-відгук: кореневий шаблон з даними сторінки в JSON;
- переходи через
<Link>- запит XHR із заголовкомX-Inertia. Сервер бачить його й повертає JSON - назву компонента й props; - Inertia підміняє компонент сторінки без перезавантаження.
Чого в Inertia немає й не треба: клієнтського роутера (Vue Router), API для сторінок, сторів для серверних даних (дані приходять props).
Коли Inertia добре підходить: застосунок і фронтенд роблять одна команда, все в одному репозиторії Laravel - адмінки, SaaS, кабінети.
Коли ні: API потрібен ще й для мобільного застосунку чи сторонніх клієнтів; фронтенд і бекенд розробляють і деплоять незалежно.
Inertia 3 (актуальна версія) прибрала залежність від Axios (власний HTTP-клієнт), спростила SSR у розробці й вимагає Laravel 11+ та PHP 8.2+. Стартові набори Laravel з Vue побудовані саме на Inertia.
Inertia::render() приймає назву компонента сторінки й масив props - даних, які отримає компонент.
use Inertia\Inertia;
public function show(Post $post)
{
return Inertia::render('Posts/Show', [
'post' => PostResource::make($post->load('author')),
'comments' => $post->comments()->latest()->limit(20)->get(['id', 'body', 'created_at']),
'canEdit' => request()->user()?->can('update', $post) ?? false,
]);
}
<script setup>
const props = defineProps({
post: Object,
comments: Array,
canEdit: Boolean,
});
</script>
<template>
<h1>{{ post.title }}</h1>
<button v-if="canEdit">Редагувати</button>
</template>
Як перетворюються дані:
- моделі й колекції - через
toArray()(з урахуванням$hiddenі$appends); - API Resources - через
toResponse(), тожPostResourceдає контрольований формат; - замикання (
fn () => ...) обчислюються лише тоді, коли prop справді потрібен (часткові перезавантаження); - назва компонента може бути backed enum - для типобезпечних посилань на сторінки.
Головне правило безпеки: усі props видно в браузері. Вони потрапляють у JSON сторінки, який будь-хто бачить у вихідному коді чи вкладці Network. Модель, передана цілком, віддасть усі свої поля: email, телефон, службові прапорці, хеш токена, якщо його забули сховати.
Тому:
- передавати лише потрібні поля:
->only(['id', 'name']),get(['id', 'title'])чи API Resource; - не покладатися на те, що «Vue це не показує» - не показаний у шаблоні prop все одно є в JSON;
- права передавати готовими булевими значеннями (
canEdit), а перевіряти - на сервері в кожній дії.
Сторінка без контролера: Route::inertia('/about', 'About').
useForm - помічник Inertia для форм: зберігає дані, стан відправки, помилки валідації й прогрес завантаження.
<script setup>
import { useForm } from '@inertiajs/vue3';
const form = useForm({
title: '',
body: '',
cover: null,
});
function submit() {
form.post('/posts', {
preserveScroll: true,
onSuccess: () => form.reset(),
});
}
</script>
<template>
<form @submit.prevent="submit">
<input v-model="form.title">
<p v-if="form.errors.title">{{ form.errors.title }}</p>
<textarea v-model="form.body" />
<input type="file" @input="form.cover = $event.target.files[0]">
<progress v-if="form.progress" :value="form.progress.percentage" max="100" />
<button :disabled="form.processing">Опублікувати</button>
</form>
</template>
Що дає useForm:
- методи відправки:
get,post,put,patch,delete; form.errors- помилки валідації Laravel заповнюються автоматично;form.processing- чи йде відправка (захист від подвійного кліку);form.isDirty- чи змінювали форму;wasSuccessful,recentlySuccessful(дві секунди після успіху - для «Збережено»);reset(),clearErrors(),resetAndClearErrors(),defaults();transform()- змінити дані перед відправкою.
Файли. Якщо в даних є файл, Inertia сама перетворить їх на FormData. Але для put/patch з файлами PHP не розбирає multipart - відправляють post з _method: 'put'.
Валідація на сервері - звичайна:
public function store(Request $request)
{
$validated = $request->validate(['title' => 'required|max:255', 'body' => 'required']);
// при помилці Laravel поверне назад, а Inertia покладе помилки у form.errors
}
Корисне: useForm('CreatePost', {...}) з ключем зберігає дані форми в історії браузера - повернувшись «Назад», користувач не втратить введене; паролі виключають через dontRemember('password').
Простіша альтернатива - компонент <Form action="/posts" method="post">, що працює як звичайна HTML-форма з полями за атрибутом name.
Звичайний <a href> завантажує сторінку повністю: HTML, CSS, JavaScript, ініціалізація Vue. <Link> з @inertiajs/vue3 перехоплює клік і робить Inertia-візит: запит повертає лише JSON з назвою компонента й props, а сторінка підміняється без перезавантаження.
<script setup>
import { Link } from '@inertiajs/vue3';
</script>
<template>
<Link href="/posts">Блог</Link>
<Link :href="`/posts/${post.id}`" prefetch>{{ post.title }}</Link>
</template>
Можливості:
- інші HTTP-методи:
<Link href="/logout" method="post" as="button">Вийти</Link>
<Link :href="`/posts/${post.id}`" method="delete" as="button">Видалити</Link>
Для не-GET методів варто рендерити кнопку (as="button"): посилання, що виконує POST, порушує очікування доступності й «відкрити в новій вкладці».
- дані запиту:
:data="{ status: 'draft' }"; preserve-scroll- не прокручувати на початок (фільтри, пагінація посеред сторінки);preserve-state- зберегти локальний стан компонента (введене в поле пошуку);replace- замінити запис в історії, а не додати;:only="['users']"- часткове перезавантаження лише потрібних props;prefetch- завантажити дані заздалегідь при наведенні (75 мс за замовчуванням),prefetch="click"- на натисканні,prefetch="mount"- одразу; кеш на 30 секунд (cache-for).
Активне посилання - порівняння з $page.url чи usePage().url.
Чого не робити:
<Link>на зовнішні сайти - для них звичайний<a>;<Link>на маршрути Laravel, що повертають не Inertia-відповідь (файл для завантаження, сторінку без Inertia) - Inertia покаже модальне вікно з HTML. Для таких адрес - звичайне посилання абоInertia::location()на сервері;- вихід з облікового запису через GET - лише
method="post".
Однофайловий компонент (SFC, файл .vue) об'єднує в одному файлі три частини:
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">Натиснуто {{ count }} разів</button>
</template>
<style scoped>
button { font-weight: 600; }
</style>
<script>- логіка компонента;<template>- розмітка, яку компілятор перетворює на функцію рендеру;<style>- стилі; зscopedвони діють лише на цей компонент.
Браузер не розуміє .vue-файлів - їх компілює збирач (Vite з плагіном @vitejs/plugin-vue).
<script setup> - скорочений запис Composition API. Без нього довелося б писати:
export default {
setup() {
const count = ref(0)
return { count } // усе, що потрібно шаблону, - вручну
},
}
Що дає <script setup>:
- усе оголошене на верхньому рівні доступне в шаблоні - змінні, функції, імпортовані компоненти. Не треба повертати об'єкт і реєструвати компоненти в
components; - макроси компілятора
defineProps,defineEmits,defineModel,defineExpose- їх не імпортують, компілятор замінює їх на опції компонента; - кращі типи: props і події описуються типами TypeScript;
- продуктивніший код: шаблон компілюється в ту саму область видимості, що й скрипт, без проміжного проксі.
Що варто знати:
- компонент з
<script setup>за замовчуванням закритий: батьківський компонент через ref шаблону не бачить його внутрішніх змінних. Відкрити частину -defineExpose({ reset }); - код
<script setup>виконується для кожного екземпляра компонента, а звичайний<script>поруч - один раз при імпорті модуля (там зручно оголошувати типи й константи).
Офіційний спосіб створити проєкт - утиліта create-vue:
npm create vue@latest
Вона запитає, що додати: TypeScript, Vue Router, Pinia, Vitest, Playwright, ESLint, Prettier. Результат - проєкт на Vite з готовими налаштуваннями.
Що робить Vite:
- під час розробки (
npm run dev) віддає модулі браузеру майже як є, а.vue-файли компілює на льоту плагіном@vitejs/plugin-vue: шаблон - у функцію рендеру,<script setup>- у звичайний компонент, стилі - у CSS. Зміна компонента оновлюється через гарячу заміну модулів (HMR) зі збереженням стану; - для продакшену (
npm run build) збирає, мініфікує, розділяє код на частини й додає хеші в імена файлів.
TypeScript у Vite лише прибирає типи (транспіляція), але не перевіряє їх - заради швидкості. Перевірку робить окрема команда:
"scripts": {
"dev": "vite",
"build": "vue-tsc --build && vite build",
"type-check": "vue-tsc --build"
}
vue-tsc - обгортка над tsc, що розуміє .vue-файли. Звичайний tsc їх не бачить.
У Laravel-проєкті Vue підключають до вже наявного Vite:
// vite.config.js
import laravel from 'laravel-vite-plugin'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
laravel({ input: 'resources/js/app.ts', refresh: true }),
vue({ template: { transformAssetUrls: { base: null, includeAbsolute: false } } }),
],
})
Опції transformAssetUrls потрібні, щоб шляхи до зображень у шаблонах (/images/logo.png) вели на файли Laravel у public, а не перетворювалися на імпорти. Стартові набори Laravel (Vue + Inertia) мають усе це налаштованим.
Редактор: розширення Vue - Official (колишній Volar) для VS Code дає підсвітку, автодоповнення й перевірку типів у шаблонах.
Оголошення через тип - найзручніший спосіб у <script setup lang="ts">:
<script setup lang="ts">
interface Props {
title: string
count?: number
tags?: string[]
}
const props = defineProps<Props>()
</script>
Компілятор сам перетворює тип на runtime-оголошення (title: { type: String, required: true }), тож Vue перевіряє props і під час виконання в режимі розробки.
Значення за замовчуванням - два способи:
1. Деструктуризація props (Vue 3.5+):
const { title, count = 0, tags = () => [] } = defineProps<Props>()
Змінні, отримані деструктуризацією defineProps, лишаються реактивними: компілятор замінює звернення count на props.count. Але є нюанс - передати таку змінну у watch чи composable напряму не можна, лише як геттер:
watch(() => count, (value) => { /* ... */ }) // правильно
watch(count, ...) // помилка компіляції
useSomething(() => count)
2. withDefaults (до 3.5 і досі підтримується):
const props = withDefaults(defineProps<Props>(), {
count: 0,
tags: () => [],
})
Значення-об'єкти й масиви задаються функцією, що повертає нове значення, інакше всі екземпляри компонента ділили б один масив.
Обмеження оголошення через тип:
- тип має бути описаний у файлі чи імпортований з відносного шляху або пакета - компілятор аналізує його статично. Дуже складні умовні типи він не розбере;
- runtime-перевірка бачить лише базові типи (
String,Array), а не вмістstring[]- глибока перевірка лишається за TypeScript.
Не змінювати props усередині компонента. props.title = 'x' - попередження Vue: дані йдуть від батька до дитини, а зміни - назад через події чи defineModel.
Vue DevTools - інструмент налагодження Vue-застосунків. Доступний як розширення браузера та як плагін Vite (vite-plugin-vue-devtools), що вбудовує панель прямо в сторінку під час розробки.
Що в ньому корисно:
1. Дерево компонентів. Ієрархія компонентів сторінки, як вона є в застосунку, а не в DOM. Для обраного компонента видно:
- props, які він отримав;
- внутрішній стан (
ref,reactive,computed) - значення можна змінювати на льоту й одразу бачити результат; - події, які він випромінює.
Клік по елементу сторінки в режимі вибору показує, який компонент його відрендерив і в якому файлі він лежить.
2. Pinia. Стан кожного стору, геттери, і історія змін з можливістю повернутися до попереднього стану (time travel). Видно, яка дія що змінила.
3. Vue Router. Поточний маршрут, параметри, список маршрутів, історія переходів.
4. Хронологія (Timeline). Події компонентів, дії Pinia, переходи маршрутизатора, а також рендери й оновлення з тривалістю. Тут видно, який компонент перерендерюється частіше, ніж мав би.
5. Продуктивність. Час рендеру й оновлення кожного компонента - перша зупинка, коли інтерфейс гальмує.
Що варто знати:
- у продакшен-збірці DevTools за замовчуванням вимкнені: Vue прибирає службовий код для них, і розширення нічого не бачить. Це правильно - не вмикайте їх на продакшені без потреби;
- з
<script setup>компоненти показуються з назвою файлу; для анонімних компонентів назву можна задати черезdefineOptions({ name: 'OrderCard' }); - для Inertia-застосунків на Laravel DevTools працюють так само: сторінки Inertia - звичайні Vue-компоненти.
Порядок налагодження «чому не оновлюється»: подивитися в DevTools, чи змінився стан. Якщо змінився, а екран ні - проблема в рендері (ключі, реактивність). Якщо не змінився - у логіці, що мала його змінити.
Обидві директиви дозволяють пропустити роботу з оновлення частини шаблону.
v-once - відрендерити елемент чи компонент один раз і більше ніколи не оновлювати:
<footer v-once>
<p>© {{ year }} {{ companyName }}</p>
<LegalLinks />
</footer>
При наступних рендерах Vue пропускає всю цю гілку разом з нащадками. Підходить для статичного за змістом вмісту, що будується з даних лише раз.
v-memo - оновлювати лише тоді, коли змінилося одне зі значень у масиві залежностей:
<div v-for="item in list" :key="item.id" v-memo="[item.id === selectedId]">
<p>ID: {{ item.id }} - вибрано: {{ item.id === selectedId }}</p>
<ExpensiveRow :item="item" />
</div>
Коли змінюється selectedId, перерендерюються лише два рядки - той, що втратив вибір, і той, що його отримав. Решта тисячі рядків пропускається.
Коли це має сенс:
v-memo- великі списки (сотні й тисячі елементів), де зміна стосується небагатьох рядків, і вимірювання показали, що рендер списку гальмує;v-once- великі статичні блоки, побудовані з даних, які не змінюються протягом життя компонента.
Пастки:
- забута залежність: якщо в шаблоні під
v-memoвикористано значення, якого немає в масиві, зміни цього значення не з'являться на екрані. Найчастіша помилка - баг виглядає як «реактивність зламалася»; v-memo="[]"працює якv-once;v-memoвсерединіv-forне працює на дочірніх елементах - тільки на тому ж елементі, що йv-for;- передчасна оптимізація: у більшості компонентів оновлення й так дешеві. Ці директиви - інструмент для виміряних проблем, а не для кожного шаблону;
v-onceна компоненті з даними, які все ж змінюються (наприклад, переклад після зміни мови), - застарілий інтерфейс.
Звичайний імпорт компонента потрапляє в головний бандл: навіть якщо важкий редактор чи графік відкриють раз на тиждень, його код завантажується з кожною сторінкою.
defineAsyncComponent завантажує компонент при першому рендері:
import { defineAsyncComponent } from 'vue'
const ChartPanel = defineAsyncComponent(() => import('./ChartPanel.vue'))
<button @click="showChart = true">Показати графік</button>
<ChartPanel v-if="showChart" :data="sales" />
Динамічний import() змушує збирач винести компонент і його залежності (бібліотеку графіків) в окремий файл. Він завантажиться, лише коли showChart стане true.
Стани завантаження й помилки:
const ChartPanel = defineAsyncComponent({
loader: () => import('./ChartPanel.vue'),
loadingComponent: Spinner,
delay: 200, // показати спінер лише якщо завантаження довше 200 мс
errorComponent: LoadError,
timeout: 10000, // вважати помилкою після 10 с
})
delay за замовчуванням 200 мс - щоб на швидкому з'єднанні спінер не «блимав».
Де застосовувати:
- модальні вікна, редактори тексту, карти, графіки, PDF-переглядачі;
- вкладки, які відкривають рідко;
- компоненти для певних ролей (адмінські панелі всередині звичайної сторінки).
Сторінки маршрутизатора розділяють так само, але простіше - Vue Router приймає функцію з import() прямо в маршруті.
Що варто знати:
- асинхронний компонент приймає ті самі props, слоти й події, що й звичайний - для батька різниці немає;
- не дробити надто дрібно: десятки маленьких частин - десятки запитів. Виносять важке й рідко потрібне;
- помилки після деплою: у давно відкритій вкладці стара частина може зникнути з сервера -
errorComponentз пропозицією оновити сторінку краще за порожнє місце; - у SSR асинхронні компоненти можна ще й гідрувати ліниво (Vue 3.5+).
Коли компонент зникає з екрана (перемкнули вкладку, v-if став false, змінився маршрут), Vue знищує його екземпляр: стан, введені дані, позиція прокрутки губляться. При поверненні компонент створюється заново.
<KeepAlive> кешує екземпляри компонентів замість знищення:
<KeepAlive :include="['OrdersTab', 'ReportsTab']" :max="5">
<component :is="currentTab" />
</KeepAlive>
Перемкнулися з вкладки «Замовлення» на «Звіти» й назад - фільтри, введений пошук і завантажені дані на місці.
Параметри:
include/exclude- які компоненти кешувати (за назвою компонента: у<script setup>- назва файлу чиdefineOptions({ name }));max- скільки екземплярів тримати; при переповненні витісняється найдавніше використаний.
З маршрутизатором:
<RouterView v-slot="{ Component }">
<KeepAlive>
<component :is="Component" />
</KeepAlive>
</RouterView>
Хуки кешованого компонента: onActivated і onDeactivated - замість onMounted/onUnmounted, які при показі з кешу не викликаються.
Підводні камені:
- застарілі дані: компонент з кешу показує дані з моменту, коли його сховали. Оновлювати їх треба в
onActivated, інакше користувач бачить замовлення годинної давнини; - пам'ять: кожен кешований екземпляр тримає свій стан, DOM і дані. Без
maxіincludeкеш росте з кожною новою сторінкою - особливо з маршрутами виду/orders/:id, де кожен id - окремий запис кешу; - побічні ефекти продовжують працювати: таймери, підписки на WebSocket, обробники на
windowу прихованому компоненті живуть далі. Їх треба призупиняти вonDeactivated; - ключі маршрутів: кешування сторінки з параметром може показати дані попереднього id, якщо компонент перевикористовується - потрібен
:keyчи реакція на зміну параметра.
Коли не потрібен: якщо стан простіше зберегти в Pinia чи URL (фільтри, номер сторінки) - це надійніше й не тримає цілий компонент у пам'яті.
Без розділення весь застосунок - усі сторінки, адмінка, рідко потрібні розділи - потрапляє в один JavaScript-файл. Користувач, що відкрив головну, завантажує і код сторінки налаштувань, і графіки звітів.
Ліниві маршрути - компонент сторінки задається функцією з динамічним import():
const routes = [
{ path: '/', component: () => import('./pages/Home.vue') },
{ path: '/orders', component: () => import('./pages/Orders.vue') },
{ path: '/reports', component: () => import('./pages/Reports.vue') },
]
Збирач (Vite) виносить кожну сторінку разом з її унікальними залежностями в окремий файл. Він завантажується при першому переході на маршрут.
Не використовувати defineAsyncComponent для маршрутів - Vue Router сам уміє працювати з функцією, що повертає Promise; обгортка лише заважає.
Групування в одну частину - кілька пов'язаних сторінок разом (наприклад, розділ адмінки), щоб не робити три запити поспіль. У Vite це налаштовується через опції збирання (manualChunks / групування частин).
Що варто знати:
- затримка першого переходу: при кліку на маршрут код ще треба завантажити. Допомагає попереднє завантаження (
<link rel="modulepreload">, завантаження при наведенні на посилання) і показ індикатора завантаження; - помилки завантаження після деплою: якщо користувач тримав вкладку відкритою, а файли старої збірки видалили, перехід падає.
router.onError- місце, щоб обробити це (наприклад, перезавантажити сторінку на новий URL); - головна сторінка - не робіть лінивою найчастішу точку входу, якщо це не дає реального виграшу: це лише додатковий запит;
- спільні залежності (Vue, Pinia, бібліотека компонентів) збирач виносить в окремий спільний файл автоматично.
В Inertia-застосунках (Laravel + Vue) роль маршрутів виконують сторінки Inertia, і розділення налаштовується в resolve через import.meta.glob('./Pages/**/*.vue') - без { eager: true } кожна сторінка стає окремою частиною.
Як перевірити результат: rollup-plugin-visualizer показує, що потрапило в кожну частину і скільки важить.
Докладніше в документації: Vue Router: ліниве завантаження маршрутів
Питання з реальних технічних співбесід - 108 питань у 9 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.
Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії