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

Senior: питання на співбесіді з теми «Inertia й Laravel»

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

4 питання

Сторінка з важкою статистикою віддається повільно: сервер не відправить відповідь, доки не порахує все. Deferred props дають змогу показати сторінку одразу, а дорогі дані довантажити окремим запитом.

return Inertia::render('Dashboard', [
    'user' => $request->user()->only('id', 'name'),
    'orders' => Inertia::defer(fn () => Order::recentFor($request->user())),
    'stats' => Inertia::defer(fn () => Stats::heavy(), 'analytics'),
    'chart' => Inertia::defer(fn () => Stats::chart(), 'analytics'),
]);
<script setup>
import { Deferred } from '@inertiajs/vue3';
defineProps({ orders: Array, stats: Object, chart: Object });
</script>

<template>
  <Deferred data="orders">
    <template #fallback><OrdersSkeleton /></template>
    <OrdersTable :orders="orders" />
  </Deferred>

  <Deferred :data="['stats', 'chart']" #default="{ reloading }">
    <template #fallback><div class="h-64 animate-pulse" /></template>
    <Stats :class="{ 'opacity-50': reloading }" :stats="stats" :chart="chart" />
  </Deferred>
</template>

Групи: за замовчуванням усі відкладені props довантажуються одним запитом після рендеру. Другий аргумент ('analytics') - назва групи: різні групи йдуть паралельними запитами, щоб повільна статистика не затримувала замовлення.

Помилки: Inertia::defer(fn () => ..., rescue: true) - виняток не ламає відповідь, prop пропускається, а помилка йде в звіт; у <Deferred> для цього слот #rescue з кнопкою повтору.

Once props - для даних, що рідко змінюються (тарифи, країни, курси валют):

'plans' => Inertia::once(fn () => Plan::all()),
'rates' => Inertia::once(fn () => ExchangeRate::all())->until(now()->addDay()),

Клієнт запам'ятовує значення, а наступні відповіді сторінок з цим prop його не обчислюють і не передають. fresh() примушує оновити, as('roles') дає спільний ключ для різних сторінок. Поєднується з відкладеними: Inertia::defer(...)->once().

Що враховувати:

  • відкладене - не для SEO-важливого вмісту: у початковому HTML його немає;
  • кожна група - окремий запит з повним стеком middleware;
  • once props забуваються при переході на сторінку без них - і на наступній сторінці обчислюються знову;
  • заглушка (fallback) з тим самим розміром, що й вміст, - щоб сторінка не «стрибала».

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

Inertia за замовчуванням замінює prop новим значенням. Для «Завантажити ще» потрібно дописувати нові елементи до вже показаних - для цього є злиття props.

public function index(Request $request)
{
    return Inertia::render('Feed', [
        'posts' => Inertia::merge(
            Post::query()->latest()->paginate(15)
        )->append('data', matchOn: 'id'),
    ]);
}
<script setup>
import { router } from '@inertiajs/vue3';
const props = defineProps({ posts: Object });

function loadMore() {
  router.reload({
    only: ['posts'],
    data: { page: props.posts.current_page + 1 },
    preserveUrl: true,
  });
}
</script>

<template>
  <PostCard v-for="post in posts.data" :key="post.id" :post="post" />
  <button v-if="posts.next_page_url" @click="loadMore">Завантажити ще</button>
</template>

Як працює злиття:

  • лише при часткових перезавантаженнях. Звичайний візит на сторінку завжди замінює prop повністю;
  • Inertia::merge($items) - дописати в кінець масиву на верхньому рівні; ->prepend() - на початок (нові повідомлення чату);
  • ->append('data') - для пагінатора: дописати лише масив data, а current_page, next_page_url - замінити;
  • matchOn: 'id' - якщо елемент з таким id вже є, він оновлюється, а не дублюється. Важливо, коли за час прокрутки на початок стрічки додалися нові записи й «сторінки» зсунулися;
  • Inertia::deepMerge() - глибоке злиття всієї структури.

Нескінченна прокрутка - готове рішення на тому самому механізмі: Inertia::scroll(fn () => Post::paginate()) на сервері (сам налаштовує злиття й метадані пагінації, працює з paginate, simplePaginate і cursorPaginate) і компонент <InfiniteScroll> на клієнті, що довантажує сторінки при прокрутці.

Пастки:

  • пагінація через OFFSET зсувається, якщо записи додаються: сторінка 2 повторить останній елемент сторінки 1. matchOn прибирає дублікати, але курсорна пагінація (cursorPaginate) надійніша;
  • пам'ять: після сотні сторінок масив з тисяч елементів живе в props і в стані історії браузера;
  • повернення «Назад» відновлює весь довантажений список з історії - добре для користувача, але великий стан історії браузери можуть обмежувати;
  • preserveUrl - щоб адреса не ставала ?page=7, а оновлення сторінки не відкривало сьому сторінку без перших шести.

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

Inertia не змінює модель безпеки Laravel - маршрути, middleware, політики працюють як завжди. Ризики з'являються там, де дані йдуть у браузер.

1. Усі props публічні. Кожен prop і всі спільні дані - у JSON сторінки, який видно у вихідному коді й вкладці Network. Типові витоки:

  • 'user' => $request->user() у спільних даних - усі атрибути моделі на кожній сторінці;
  • колекції моделей цілком (User::all()) - email, телефони, службові поля всіх користувачів;
  • «прихований» prop, який шаблон просто не показує.

Ліки - явні поля (only(), get([...])), API Resources, перевірка JSON сторінок у тестах.

2. Авторизація лише на сервері. v-if="can.delete" ховає кнопку, але запит DELETE /posts/5 можна відправити й без неї. Кожна дія контролера перевіряє права ($this->authorize(), політики); у props передаються лише булеві прапорці для інтерфейсу.

3. Історія браузера. Inertia зберігає props сторінок у стані історії, щоб «Назад» працював миттєво. Після виходу з облікового запису інша людина за тим самим комп'ютером натисне «Назад» - і побачить сторінки з персональними даними з історії, без запиту до сервера.

Шифрування історії:

// глобально: config/inertia.php - history.encrypt => true
// або для окремих маршрутів
Route::middleware('inertia::encrypt')->group(function () { /* ... */ });

// при виході - зробити старі записи нечитабельними
public function logout(Request $request)
{
    Auth::logout();
    Inertia::clearHistory();
    return redirect('/');
}

Inertia шифрує стан сторінки ключем у sessionStorage (Web Crypto API). clearHistory() змінює ключ - старі записи розшифрувати неможливо, і Inertia запитує сторінку з сервера, де вже діє перевірка автентифікації. Потребує HTTPS.

4. CSRF. Inertia покладається на cookie XSRF-TOKEN Laravel; помилку 419 (сесія завершилася) варто обробити - перезавантажити сторінку чи показати повідомлення.

5. XSS. Vue екранує {{ }}, але v-html з даними користувача - діра, як і в будь-якому Vue-застосунку. При SSR дані сторінки вбудовуються в HTML - адаптер робить це безпечно (<script type="application/json"> у v3), власні вставки треба екранувати так само.

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

Без SSR перше завантаження Inertia-сторінки - це майже порожній HTML з даними в JSON, а вміст з'являється після завантаження й виконання JavaScript. Пошукові системи й соцмережі (прев'ю посилань) бачать порожню сторінку, а користувач - білий екран довше.

SSR рендерить Vue-компонент сторінки на сервері: у HTML одразу є вміст, а в браузері Vue гідрує його - підключається до готової розмітки.

Як це влаштовано: рендеринг виконує Node.js-процес, а Laravel надсилає йому дані сторінки й отримує HTML. На сервері потрібен Node.

Inertia 3 спростила налаштування:

  • плагін @inertiajs/vite налаштовує SSR автоматично;
  • у розробці SSR працює просто з npm run dev - без окремої збірки й окремого сервера;
  • у продакшені - збірка vite build && vite build --ssr і процес php artisan inertia:start-ssr (під наглядом supervisor), за потреби з кластеризацією на кілька потоків.

Вимкнути SSR для частини застосунку (адмінка, де SEO не потрібне): властивість $withoutSsr у middleware чи Inertia::withoutSsr(['admin/*']); повністю - ssr.enabled => false у конфігурації або Inertia::disableSsr().

Код, що працює лише в браузері (window, localStorage, бібліотеки з DOM), на сервері падає. Його виконують в onMounted або загортають у компонент WhenMounted, що рендерить вміст лише після монтування в браузері.

Версіонування ассетів. SPA живе в браузері годинами - після деплою користувач продовжує працювати зі старим JavaScript, який може не відповідати новим props. Inertia передає версію ассетів у кожному запиті; якщо версія на сервері змінилася, Inertia робить повне перезавантаження сторінки замість звичайного візиту, і браузер отримує нові файли.

  • з Vite версія обчислюється автоматично (метод version() у HandleInertiaRequests);
  • з Inertia 3.6 зміна версії, помічена у фоновому запиті (опитування, router.reload()), не перезавантажує сторінку - щоб не знищити незбережені дані, яких користувач не чекав втратити.

Пастка деплою: SSR-процес Node треба перезапускати при кожному деплої, інакше він рендеритиме старими компонентами.

Докладніше в документації: Inertia: серверний рендеринг