Senior: питання на співбесіді з теми «Шаблони й директиви»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Власні директиви - для повторного використання низькорівневої роботи з 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:
- компіляція - шаблон перетворюється на рендер-функцію (зазвичай під час збирання, через Vite і
@vitejs/plugin-vue); - монтування - рендер-функція повертає дерево віртуальних вузлів (VNode), з якого створюється DOM. Під час виконання реактивна система запам'ятовує, які дані читалися;
- оновлення (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, якщо інтерактивність помірна.