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

Питання на співбесіді: Шаблони й директиви

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

12 питань

v-model - двостороння прив'язка поля форми до стану: введене потрапляє в змінну, зміна змінної - у поле.

<script setup>
import { ref } from 'vue';

const name = ref('');
const agree = ref(false);
const city = ref('kyiv');
const tags = ref([]);
</script>

<template>
  <input v-model="name">
  <input type="checkbox" v-model="agree">
  <select v-model="city">
    <option value="kyiv">Київ</option>
    <option value="lviv">Львів</option>
  </select>
  <label><input type="checkbox" value="php" v-model="tags"> PHP</label>
  <label><input type="checkbox" value="vue" v-model="tags"> Vue</label>
</template>

Для різних елементів v-model використовує різні властивості й події: value + input для тексту, checked + change для чекбоксів і радіо, value + change для select. Чекбокси з value, прив'язані до масиву, додають і прибирають значення.

Модифікатори:

  • .lazy - оновлювати на change (вихід з поля), а не на кожне натискання;
  • .number - перетворювати введене на число (parseFloat). Якщо перетворити неможливо, лишається рядок. Для type="number" застосовується автоматично;
  • .trim - обрізати пробіли на початку й у кінці.
<input v-model.trim="email" type="email">
<input v-model.number="age" type="number">
<textarea v-model.lazy="bio"></textarea>

Пастки:

  • v-model ігнорує атрибути value, checked, selected у розмітці - джерело істини лише стан. Початкове значення задається в ref;
  • select без початкового значення, що збігається з опцією, в iOS не дає вибрати першу опцію - тому додають порожню <option disabled value="">Оберіть місто</option>;
  • IME (введення ієрогліфів, частина мобільних клавіатур) - v-model не оновлює значення під час складання символу; якщо потрібно - власний обробник input;
  • v-model на prop - не можна: props належать батькові. Для компонента-поля - defineModel().

Значення не лише рядки: :value="{ id: 1 }" на опціях select чи чекбоксах дає змогу прив'язувати об'єкти, а true-value/false-value на чекбоксі - власні значення замість true/false.

Докладніше в документації: Прив'язка полів форми

У Vue 3 на одному елементі v-if має вищий пріоритет, ніж v-for. Умова обчислюється раніше за цикл - і не має доступу до змінної циклу.

<!-- помилка: todo ще не визначено, коли перевіряється v-if -->
<li v-for="todo in todos" v-if="!todo.done">{{ todo.title }}</li>

У Vue 2 було навпаки (v-for першим), тож код, перенесений зі старих проєктів, ламається саме тут.

Правильні варіанти:

1. Фільтр - в обчислюваній властивості (найкращий варіант для фільтрації):

<script setup>
const activeTodos = computed(() => todos.value.filter((todo) => !todo.done));
</script>

<template>
  <li v-for="todo in activeTodos" :key="todo.id">{{ todo.title }}</li>
</template>

Фільтр виконується лише при зміні todos, а не на кожен рендер, і шаблон стає простішим.

2. <template> з v-for зовні, v-if усередині - коли умова для кожного елемента окрема:

<template v-for="todo in todos" :key="todo.id">
  <li v-if="!todo.done">{{ todo.title }}</li>
</template>

3. v-if зовні - для умови на весь список:

<ul v-if="todos.length">
  <li v-for="todo in todos" :key="todo.id">{{ todo.title }}</li>
</ul>
<p v-else>Завдань немає</p>

Що ще варто знати про v-for:

  • :key обов'язковий - стабільний ідентифікатор, а не індекс;
  • перебір об'єкта: v-for="(value, key, index) in object";
  • діапазон: v-for="n in 10" дає числа від 1 до 10;
  • на <template> key ставиться на сам <template>.

Чому computed краще за фільтр у шаблоні: вираз v-for="todo in todos.filter(...)" створює новий масив на кожному рендері компонента - навіть якщо змінилося щось зовсім не пов'язане зі списком.

Докладніше в документації: v-for разом з v-if

Для class і style Vue має розширений синтаксис :class / :style: крім рядків, вони приймають об'єкти й масиви.

Об'єкт - клас за умовою:

<button :class="{ 'opacity-50': loading, 'bg-red-600': danger }">Видалити</button>

Ключ - клас, значення - умова. Статичний class і динамічний :class об'єднуються:

<div class="card" :class="{ 'ring-2': selected }"></div>

Масив - кілька джерел класів:

<div :class="[sizeClass, { 'font-bold': active }, isError ? 'text-red-600' : 'text-gray-900']"></div>

Обчислювана властивість - коли логіка складніша:

const badgeClass = computed(() => ({
  paid: 'bg-green-100 text-green-800',
  pending: 'bg-amber-100 text-amber-800',
  failed: 'bg-red-100 text-red-800',
})[props.status]);

Стилі - об'єкт з camelCase чи kebab-case властивостями:

<div :style="{ width: progress + '%', backgroundColor: color }"></div>
<div :style="{ '--accent': theme.accent }"></div>   <!-- CSS-змінна -->

Vue автоматично додає вендорні префікси, якщо властивість їх потребує.

На компонентах: class і style, передані компоненту, додаються до класів його кореневого елемента.

Пастки з Tailwind:

  • не збирати назви класів з частин: :class="`bg-${color}-500`" - Tailwind сканує файли й генерує лише ті класи, які бачить повністю. Склеєних класів у CSS не буде. Потрібна мапа з повними назвами (як у прикладі з badgeClass);
  • конфлікти класів: class="p-4" на компоненті і p-2 всередині дають обидва класи - переможе той, що пізніше в згенерованому CSS, а не той, що «ззовні». Для керованого злиття - утиліти на кшталт tailwind-merge.

Динамічні значення з даних (ширина прогрес-бару, колір користувача) краще через :style чи CSS-змінні, а не через генерацію класів.

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

v-on (скорочено @) підписує обробник на подію DOM чи компонента:

<button @click="count++">+1</button>                 <!-- вираз -->
<button @click="save">Зберегти</button>              <!-- метод: отримає об'єкт події -->
<button @click="remove(item.id)">Видалити</button>   <!-- виклик з аргументами -->
<button @click="remove(item.id, $event)">...</button><!-- $event - сама подія -->

Модифікатори подій замість ручного event.preventDefault():

Модифікатор Що робить
.prevent preventDefault() - форма не перезавантажує сторінку
.stop stopPropagation()
.self лише якщо подія виникла на самому елементі
.once спрацювати один раз
.capture слухати у фазі перехоплення
.passive пасивний слухач (прокрутка без затримок)
<form @submit.prevent="submit">...</form>
<div class="overlay" @click.self="close">...</div>

Модифікатори клавіш і миші:

<input @keyup.enter="search">
<input @keydown.esc="clear">
<textarea @keydown.ctrl.enter="send"></textarea>
<button @click.right.prevent="openMenu">...</button>
<input @keyup.enter.exact="send">   <!-- лише Enter, без Shift/Ctrl -->

Доступні .enter, .tab, .delete, .esc, .space, .up, .down, .left, .right, системні .ctrl, .alt, .shift, .meta, а також будь-яка назва клавіші з event.key у kebab-case (.page-down).

Порядок модифікаторів має значення: @click.prevent.self скасовує дію для кліків на будь-якому дочірньому елементі, а @click.self.prevent - лише на самому елементі.

Події компонентів слухаються так само: @save="onSave". Модифікатори на кшталт .prevent до них не застосовуються - це не DOM-події.

Пастки:

  • .passive разом з .prevent - конфлікт: пасивний слухач не може скасувати дію, браузер проігнорує .prevent і виведе попередження;
  • важкі обробники на scroll, input, mousemove - потрібне обмеження частоти (debounce/throttle), Vue сам цього не робить;
  • обробники на window/document у шаблоні не повісиш - їх додають в onMounted і знімають в onUnmounted (або useEventListener з VueUse).

Докладніше в документації: Обробка подій

Повна форма директиви: v-назва:аргумент.модифікатор="значення". Наприклад, v-bind:href="url", v-on:submit.prevent="save".

Динамічний аргумент - назва атрибута чи події з виразу в квадратних дужках:

<a :[attributeName]="url">...</a>
<button @[eventName]="handler">...</button>

Якщо attributeName = 'href', це те саме, що :href="url". Значення null прибирає прив'язку.

Обмеження динамічних аргументів:

  • вираз має давати рядок або null;
  • без пробілів і лапок усередині дужок: :['data-' + key] - синтаксична помилка. Складні назви обчислюють у computed;
  • у шаблонах прямо в HTML-сторінці (не в .vue-файлах) браузер переводить атрибути в нижній регістр: :[someAttr] стане :[someattr] і не знайде змінну.

Прив'язка об'єкта атрибутів - v-bind без аргументу:

<input v-bind="{ id: 'email', type: 'email', required: true }">
<BaseInput v-bind="field" />   <!-- розгорнути об'єкт у props/атрибути -->

Скорочення з однаковою назвою (Vue 3.4+): якщо атрибут і змінна називаються однаково, значення можна опустити:

<img :src :alt>          <!-- те саме, що :src="src" :alt="alt" -->
<UserCard :user :role />

Kebab-case атрибут шукає camelCase змінну: :data-id → dataId.

Логічні атрибути: :disabled="isSaving" - атрибут з'являється при істинному значенні й зникає при хибному (false, null, undefined). Порожній рядок '' для логічних атрибутів вважається істинним, як у HTML.

Модифікатори v-bind:

  • .prop - встановити як властивість DOM, а не атрибут (:value.prop, скорочено .value);
  • .attr - навпаки, примусово як атрибут (:width.attr, скорочено ^width);
  • .camel - перетворити kebab-case на camelCase (для атрибутів SVG на кшталт viewBox у HTML-шаблонах).

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

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

Vue сам керує DOM, але інколи потрібен прямий доступ до елемента: фокус, прокрутка, вимір розміру, ініціалізація сторонньої бібліотеки. Для цього - template ref.

Vue 3.5+ - useTemplateRef:

<script setup>
import { useTemplateRef, onMounted } from 'vue';

const input = useTemplateRef('search');

onMounted(() => input.value.focus());
</script>

<template>
  <input ref="search" type="search">
</template>

Рядок у useTemplateRef('search') збігається зі значенням атрибута ref.

До 3.5 - ref з тією самою назвою, що й атрибут:

const search = ref(null);   // <input ref="search">

Цей спосіб працює й далі, але useTemplateRef явніший: видно, що змінна - саме посилання на елемент, і не потрібно збігу назв змінних.

Коли ref заповнений:

  • після монтування - в setup і onBeforeMount він null;
  • під v-if стає null, коли елемент прибрано, - звідси перевірки input.value?.focus();
  • після зміни стану DOM оновлюється асинхронно: щоб звернутися до щойно показаного елемента, потрібен await nextTick():
async function openEditor() {
  editing.value = true;
  await nextTick();
  editorInput.value.focus();
}

Ref у v-for дає масив елементів (порядок не гарантовано збігається з масивом даних):

const items = useTemplateRef('items');   // <li v-for="..." ref="items">

Ref-функція - для повного контролю: :ref="(el) => { if (el) map.set(id, el) }" викликається з елементом при монтуванні й з null при видаленні.

Ref на компонент дає екземпляр компонента - але з <script setup> доступне лише те, що відкрито через defineExpose.

Коли НЕ треба ref: змінювати вміст, класи, атрибути напряму через DOM (el.textContent = ..., el.classList.add). Наступний рендер Vue перезапише зміни, а стан стане неузгодженим. Усе, що можна виразити через стан і шаблон, - через стан і шаблон.

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

Модальне вікно логічно належить компоненту, що його відкриває (кнопка, стан, обробники - поруч). Але візуально воно має бути поверх усієї сторінки. Якщо розмітка вікна лишається глибоко в DOM усередині батьків, CSS батьків ламає позиціонування:

  • transform, filter, perspective на предку роблять position: fixed відносним до цього предка, а не до вікна;
  • overflow: hidden обрізає вікно;
  • z-index обмежений контекстом накладання (stacking context) батька.

<Teleport> рендерить вміст в інше місце DOM, лишаючи його логічно частиною компонента:

<script setup>
const open = ref(false);
</script>

<template>
  <button @click="open = true">Видалити акаунт</button>

  <Teleport to="body">
    <div v-if="open" class="fixed inset-0 z-50 grid place-items-center bg-black/50">
      <div role="dialog" aria-modal="true" class="rounded bg-white p-6">
        <p>Ви впевнені?</p>
        <button @click="open = false">Скасувати</button>
      </div>
    </div>
  </Teleport>
</template>

Що зберігається: реактивність, props, події, provide/inject - вміст лишається дочірнім у дереві компонентів. Змінюється лише місце в DOM.

Параметри:

  • to - CSS-селектор чи елемент; ціль має існувати на момент монтування;
  • :disabled="isMobile" - рендерити на місці (наприклад, на мобільних вікно стає звичайним блоком);
  • defer (Vue 3.5+) - відкласти пошук цілі до кінця поточного монтування, щоб ціллю міг бути елемент, який Vue рендерить пізніше в тому самому дереві.

Кілька Teleport в одну ціль дописують вміст по черзі - зручно для стеку сповіщень.

Що ще треба для якісного модального вікна (Teleport цього не робить):

  • фокус: перевести фокус у вікно при відкритті, втримувати його всередині (focus trap) і повертати на кнопку після закриття;
  • закриття по Escape і кліку на підкладку;
  • блокування прокрутки сторінки під вікном;
  • role="dialog", aria-modal, aria-labelledby для зчитувачів екрана.

Нативна альтернатива - елемент <dialog> з showModal(): браузер сам дає верхній шар (top layer, без проблем із z-index), фокус і Escape. У сучасних застосунках це часто простіше за Teleport.

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

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

<Transition> анімує один елемент чи компонент, коли той з'являється або зникає через v-if, v-show, зміну динамічного компонента чи key.

<Transition name="fade">
  <p v-if="visible">Збережено</p>
</Transition>

<style>
.fade-enter-active,
.fade-leave-active { transition: opacity 0.2s ease; }
.fade-enter-from,
.fade-leave-to { opacity: 0; }
</style>

Класи, які Vue додає по черзі (name - префікс):

  • -enter-from → -enter-active → -enter-to - поява;
  • -leave-from → -leave-active → -leave-to - зникнення.

Vue сам визначає, коли анімація закінчилася (за transitionend/animationend), і лише тоді прибирає елемент з DOM.

З Tailwind - класи напряму через props:

<Transition
  enter-active-class="transition duration-200 ease-out"
  enter-from-class="opacity-0 -translate-y-2"
  leave-active-class="transition duration-150 ease-in"
  leave-to-class="opacity-0"
>

Режими для заміни одного елемента іншим: mode="out-in" - спершу зникає старий, потім з'являється новий (без накладання).

<TransitionGroup> - для списків (v-for): анімує додавання, видалення й переміщення елементів:

<TransitionGroup name="list" tag="ul">
  <li v-for="item in items" :key="item.id">{{ item.title }}</li>
</TransitionGroup>

<style>
.list-move { transition: transform 0.3s; }   /* плавне переміщення при зміні порядку */
.list-leave-active { position: absolute; }  /* щоб сусіди плавно зсувалися */
</style>

Обов'язково :key на кожному елементі - без нього Vue не знає, що анімувати.

JavaScript-хуки (@before-enter, @enter, @leave з колбеком done) - для анімацій бібліотеками (GSAP, Web Animations API).

Пастки:

  • <Transition> очікує рівно один кореневий елемент - компонент з кількома коренями всередині не анімується;
  • анімація не запускається при першому рендері - для цього атрибут appear;
  • prefers-reduced-motion - користувачі, які вимкнули анімації в системі, мають їх не бачити: @media (prefers-reduced-motion: reduce) чи motion-reduce: у Tailwind;
  • для переходів між сторінками в сучасних браузерах можна використати View Transitions API.

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

Власні директиви - для повторного використання низькорівневої роботи з DOM на звичайних елементах: фокус, перехоплення кліків поза елементом, підказки, виділення тексту, ліниве завантаження.

У <script setup> будь-яка змінна з назвою vЩось стає директивою v-щось:

<script setup>
const vFocus = {
  mounted: (el) => el.focus(),
};
</script>

<template>
  <input v-focus>
</template>

Повний набір хуків (усі необов'язкові):

const vClickOutside = {
  mounted(el, binding) {
    el._onClick = (event) => {
      if (!el.contains(event.target)) binding.value(event);
    };
    document.addEventListener('click', el._onClick);
  },
  unmounted(el) {
    document.removeEventListener('click', el._onClick);
  },
};
<div v-click-outside="closeMenu">...</div>

Хуки: created, beforeMount, mounted, beforeUpdate, updated, beforeUnmount, unmounted. Аргумент binding містить value, oldValue, arg (v-tooltip:top → 'top') і modifiers (v-tooltip.delay → { delay: true }).

Скорочена форма - функція замість об'єкта: викликається в mounted і updated.

Глобальна реєстрація - app.directive('focus', vFocus).

Коли директива - правильний вибір, а коли ні:

  • так: логіка прив'язана до конкретного DOM-елемента й не має власного шаблону чи складного стану;
  • ні - краще компонент: є розмітка (підказка з вмістом, випадне меню);
  • ні - краще composable: логіка зі станом, яку зручно використати в setup (useClickOutside(target, handler), useIntersectionObserver). Composable краще типізується й тестується;
  • ні - краще вбудовані директиви: документація радить декларативні v-bind, v-show там, де можливо, - вони ефективніші й дружні до SSR.

Пастки:

  • на компонентах директиви не рекомендовані: застосовуються до кореневого елемента, а на компоненті з кількома коренями ігноруються з попередженням (на відміну від атрибутів, їх не передати через $attrs);
  • аргументи хуків - лише для читання, крім el. Спільні дані між хуками - через el.dataset чи WeakMap, а не довільні властивості на елементі (як el._onClick у прикладі - працює, але це компроміс);
  • прибирання в unmounted обов'язкове для глобальних слухачів і спостерігачів;
  • SSR: хуки директив на сервері не викликаються (крім getSSRProps для атрибутів).

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

За замовчуванням Vue екранує дані: інтерполяція {{ }} і прив'язки атрибутів вставляють значення як текст, а не як HTML.

<p>{{ comment.body }}</p>               <!-- <script> покажеться як текст -->
<a :title="comment.author">...</a>      <!-- значення атрибута екрановано -->

Де захист закінчується:

1. v-html вставляє рядок як HTML:

<div v-html="comment.body"></div>   <!-- XSS, якщо body від користувача -->

Для контенту від користувачів - лише після санітизації (DOMPurify на клієнті чи HTML Purifier на сервері), а краще - зберігати Markdown і рендерити безпечним рендерером з білим списком тегів.

2. URL у атрибутах: екранування не захищає від схеми javascript::

<a :href="user.website">Сайт</a>   <!-- javascript:alert(1) виконається при кліку -->

Перевіряйте протокол (http:/https:) перед виведенням посилань з даних користувача.

3. :style з даних користувача - можливі атаки через CSS (підміна інтерфейсу, витік через url()).

4. Шаблон з даних користувача. Найнебезпечніший випадок - компіляція рядка, що містить ввід користувача, як шаблону Vue:

createApp({ template: `<div>${userInput}</div>` });   // виконання довільних виразів

Шаблон Vue - це код. Будь-що, що компілюється як шаблон, має повністю контролюватися розробником.

5. Серверний рендер + Vue в браузері (Blade, Laravel). Якщо Vue монтується на розмітку, яку сервер згенерував з даними користувача (in-DOM шаблон), то {{ }} у даних користувача Vue виконає як вираз. Користувач пише коментар {{ constructor.constructor('alert(1)')() }} - Blade екранує HTML, але не фігурні дужки, і Vue їх обчислить. Захист: не монтувати Vue на області з користувацьким вмістом, позначати такі ділянки v-pre, передавати дані через props/JSON, а не через розмітку.

Вирази шаблонів у «пісочниці»: у шаблоні доступний лише обмежений набір глобальних об'єктів (Math, Date тощо), а не window. Але це не межа безпеки - через ланцюжок конструкторів можна дістатися до Function, тому вимога «шаблони лише від розробника» лишається.

Що ще: Content Security Policy знижує наслідки XSS. Повна збірка Vue з компілятором шаблонів у браузері потребує unsafe-eval; збірка лише з runtime (шаблони скомпільовані Vite) - ні.

Докладніше в документації: Безпека

Три етапи роботи Vue:

  1. компіляція - шаблон перетворюється на рендер-функцію (зазвичай під час збирання, через Vite і @vitejs/plugin-vue);
  2. монтування - рендер-функція повертає дерево віртуальних вузлів (VNode), з якого створюється DOM. Під час виконання реактивна система запам'ятовує, які дані читалися;
  3. оновлення (patch) - змінилися дані - рендер-функція виконується знову, нове віртуальне дерево порівнюється зі старим, і в DOM застосовуються лише відмінності.

Чому шаблон - це не просто «зручний синтаксис». Компілятор бачить структуру шаблону цілком і додає підказки, яких немає в рукописній рендер-функції. Vue називає це «віртуальний DOM з підказками компілятора»:

  • піднімання статичного (static hoisting): вузли без динамічних прив'язок створюються один раз і використовуються в усіх рендерах. Великі статичні фрагменти можуть бути перетворені на рядок HTML;
  • прапорці оновлення (patch flags): на кожен динамічний вузол компілятор записує, що саме в ньому може змінитися - лише текст, лише class, лише конкретні props. При оновленні Vue перевіряє тільки це;
  • блоки й «сплощення дерева» (tree flattening): кожен блок зберігає плоский список своїх динамічних нащадків. При оновленні Vue проходить лише цей список, а не все дерево. Межі блоків - структурні директиви (v-if, v-for), де форма дерева може змінитися.

Результат: вартість оновлення залежить від кількості динамічних частин, а не від розміру шаблону.

Можна подивитися самому: Vue Template Explorer показує, у що компілюється шаблон, з підсвіченими прапорцями.

Дві збірки Vue:

  • runtime-only - без компілятора (менша й без eval). Підходить, коли всі шаблони - .vue-файли, скомпільовані Vite;
  • повна збірка (vue/dist/vue.esm-bundler.js з компілятором) - потрібна, якщо шаблон компілюється в браузері: опція template у компоненті чи in-DOM шаблон (Vue монтується на розмітку, згенеровану Blade). Вона більша й потребує unsafe-eval у CSP.

Практичні висновки:

  • шаблони ефективніші за рукописні рендер-функції для звичайних компонентів;
  • розділення статичного й динамічного має сенс: великий шаблон з кількома динамічними місцями оновлюється дешево;
  • v-once і v-memo - ручні підказки для випадків, де компілятор не може знати, що частина не змінюється.

Докладніше в документації: Механізм рендерингу

Vue можна використовувати не лише як SPA, а й «острівцями» на серверних сторінках Laravel: змонтувати компонент на елемент Blade-шаблону. Тут є кілька пасток.

1. Конфлікт фігурних дужок. Blade і Vue обидва використовують {{ }}. Blade обробить їх першим і спробує виконати PHP:

<div id="app">
    @{{ message }}          {{-- @ - Blade лишить {{ message }} для Vue --}}

    @verbatim
        <p>{{ user.name }}</p>   {{-- цілий блок без обробки Blade --}}
    @endverbatim
</div>

2. Шаблони в HTML (in-DOM) розбирає браузер, а не компілятор Vue:

  • регістр не враховується: <BlogPost :postTitle="t"> браузер перетворить на <blogpost :posttitle="t">. У HTML-шаблонах - лише kebab-case: <blog-post :post-title="t">;
  • самозакривних тегів немає: <my-component /> браузер прочитає як відкритий тег, і наступні елементи стануть його дітьми. Потрібно <my-component></my-component>;
  • обмеження вкладеності: у <table>, <ul>, <select> браузер викидає «чужі» теги. Компонент-рядок таблиці - через <tr is="vue:table-row">.

У .vue-файлах цих обмежень немає - їх компілює Vite.

3. Потрібна повна збірка Vue з компілятором шаблонів, якщо шаблон береться з DOM сторінки: псевдонім vue → vue/dist/vue.esm-bundler.js. Збірка більша і вимагає unsafe-eval у Content Security Policy.

4. Безпека - найважливіше. Якщо в області, на яку змонтовано Vue, є дані користувача, виведені Blade, Vue скомпілює їх як частину шаблону. Коментар із текстом {{ ... }} стане виразом Vue - Blade екранує HTML, але не фігурні дужки. Захист:

  • не монтувати Vue на області з користувацьким вмістом;
  • позначати такі ділянки v-pre (Vue не компілюватиме вміст);
  • передавати дані в компоненти через props з JSON, а не вбудовувати в розмітку.

5. Передача даних з Laravel у компонент:

<user-profile :user='@json($user)'></user-profile>
{{-- або --}}
<script>window.App = @js(['user' => $user->only('id', 'name')]);</script>

@json/Js::from безпечно екранують значення. Передавайте лише потрібні поля - усе, що в JSON, видно в коді сторінки.

Альтернативи «острівцям»: Inertia (повноцінні Vue-сторінки з маршрутизацією Laravel) або Livewire з Alpine, якщо інтерактивність помірна.

Докладніше в документації: Особливості in-DOM шаблонів