Middle: питання на співбесіді з теми «Модулі й збирання»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Статичний import завантажує модуль одразу разом з тим, хто його імпортує. Динамічний import(path) - це функція-вираз, що завантажує модуль на вимогу і повертає Promise з об'єктом модуля:
button.addEventListener('click', async () => {
const { default: Chart } = await import('chart.js/auto');
new Chart(canvas, config);
});
Бібліотека графіків (сотні кілобайтів) завантажиться, лише коли користувач натисне кнопку.
Розділення коду (code splitting). Збирач бачить import() і виносить модуль з його залежностями в окремий файл. Головний бандл стає меншим - сторінка швидше стає інтерактивною.
Типові застосування:
- маршрути SPA - кожна сторінка своїм файлом:
const routes = [
{ path: '/reports', component: () => import('./pages/Reports.vue') },
];
- важкі віджети - редактори тексту, карти, графіки, PDF-переглядачі;
- рідко потрібні функції - експорт у Excel, модальні вікна налаштувань;
- локалізація - завантажити лише потрібну мову:
await import(`./locales/${lang}.json`); - поліфіли лише для браузерів, яким вони потрібні.
Особливості:
- шаблонний шлях (
`./locales/${lang}.js`) збирач обробляє, лише якщо частину шляху відомо: він створить окремий файл для кожного відповідного модуля в папці. Повністю динамічний шлях (import(userInput)) зібрати неможливо. У Vite для таких випадків -import.meta.glob; - експорт за замовчуванням приходить як властивість
default; - помилки мережі: динамічний імпорт може не вдатися (офлайн, або після деплою старий файл зник з сервера). Потрібна обробка: повідомлення, повторна спроба, перезавантаження сторінки. Vite у такому разі генерує подію
vite:preloadError; - кешування: модуль завантажується один раз; повторні
import()повертають той самий екземпляр.
Попереднє завантаження. Щоб користувач не чекав на мережу після кліку, частини можна завантажити заздалегідь - при наведенні на посилання чи в простої: <link rel="modulepreload">. Vite сам додає попереднє завантаження залежностей для частин, що імпортуються динамічно.
Не варто дробити надто сильно: десятки дрібних частин - це десятки запитів і затримка «водоспадом», коли одна частина імпортує іншу. Розділяють за межами використання (маршрут, функція), а не кожен компонент окремо.
CommonJS - система модулів, з якою Node.js жив до ES-модулів:
// CommonJS
const path = require('node:path');
module.exports = { formatPrice };
// ES-модулі
import path from 'node:path';
export { formatPrice };
Ключові відмінності:
| CommonJS | ES-модулі | |
|---|---|---|
| завантаження | синхронне, під час виконання | асинхронне, граф будується до виконання |
require/import |
звичайна функція, будь-де в коді, шлях може бути змінною | лише на верхньому рівні, шлях - константа (крім import()) |
| що імпортується | копія значення module.exports на момент виклику |
живе посилання на експортовану змінну |
this на верхньому рівні |
module.exports |
undefined |
__dirname, __filename |
є | немає: import.meta.dirname, import.meta.filename |
top-level await |
ні | так |
| розширення в шляху | можна опустити | обов'язкове для відносних шляхів (./utils.js) |
Як Node.js вирішує, що це за модуль:
.mjs- завжди ES-модуль,.cjs- завжди CommonJS;.js- залежно від поля"type"у найближчомуpackage.json:"module"- ES-модулі,"commonjs"чи відсутність поля - CommonJS.
Laravel-проєкти мають "type": "module" у package.json, тож vite.config.js і скрипти пишуться як ES-модулі.
Взаємодія:
- ES-модуль може імпортувати CommonJS:
module.exportsстає експортом за замовчуванням; - CommonJS імпортує ES-модуль через
require()- з Node.js 22.12 це працює без прапорців, якщо модуль не використовує top-levelawait. Раніше лишався тільки асинхроннийawait import(); - пакети з подвійною збіркою містять обидва варіанти й вибирають потрібний через поле
exportsуpackage.json.
Навіщо ES-модулі, якщо CommonJS працював:
- один стандарт для браузера і сервера;
- статичний аналіз: збирачі й редактори знають залежності без виконання коду - tree shaking, точний автоімпорт, перевірка неіснуючих експортів;
- асинхронне завантаження підходить для браузера і дає змогу top-level
await.
Типові помилки при переході: require is not defined in ES module scope, __dirname is not defined, Cannot use import statement outside a module - усі означають, що файл інтерпретується не в тій системі модулів, ніж написаний.
Tree shaking - видалення з продакшен-збірки коду, який експортується, але ніде не імпортується. Назва - від образу «струсити дерево залежностей, щоб сухе листя впало».
// utils.js
export function formatPrice() { /* ... */ }
export function parseCsv() { /* 20 КБ коду */ }
// app.js
import { formatPrice } from './utils.js';
У зібраному файлі parseCsv не буде.
Чому це можливо лише з ES-модулями: статичні import/export відомі до виконання - збирач будує граф і бачить, що використовується. require() можна викликати динамічно, з обчисленим шляхом, а module.exports змінювати як завгодно - для CommonJS надійно визначити невикористаний код неможливо.
Чому невикористаний код лишається:
1. Побічні ефекти модуля. Якщо модуль при імпорті щось робить (змінює глобальні об'єкти, реєструє компоненти, додає стилі), збирач не може його викинути, навіть якщо експорти не використовуються:
// side-effects.js
window.analytics = createTracker(); // виконується при імпорті
export const unused = 1;
Пакет може оголосити, що його модулі побічних ефектів не мають: "sideEffects": false у package.json (або список файлів з ефектами - наприклад, CSS).
2. Імпорт «всього». import _ from 'lodash' і виклик _.debounce - збирач не знає, які методи об'єкта будуть потрібні. Краще import { debounce } from 'lodash-es' - пакет у форматі ES-модулів з окремими експортами.
3. Пакет у форматі CommonJS. Більшість збирачів обробляють його як єдине ціле.
4. Класи й об'єкти. Метод класу, який ніхто не викликає, не видаляється: збирач видаляє експорти, а не невикористані частини об'єктів.
5. Виклики на верхньому рівні, результат яких не використовується, але які можуть мати ефект: export const config = createConfig(). Позначка /* @__PURE__ */ перед викликом каже збирачу, що виклик чистий і його можна видалити, якщо результат не потрібен.
Як перевірити, що потрапило в бандл: rollup-plugin-visualizer для Vite показує розмір кожного модуля у збірці. Часто виявляється, що одна іконка тягне всю бібліотеку іконок або ціла локалізація моментів дат потрапила через один імпорт.
Tree shaking працює лише в продакшен-збірці. У режимі розробки Vite віддає модулі як є - розмір там не показовий.
Vite читає файли .env і робить змінні доступними в коді через import.meta.env:
# .env Laravel-проєкту
APP_NAME="Laravel Ukraine"
VITE_APP_NAME="${APP_NAME}"
VITE_PUSHER_APP_KEY=abc123
STRIPE_SECRET=sk_live_...
import.meta.env.VITE_APP_NAME; // 'Laravel Ukraine'
import.meta.env.VITE_PUSHER_APP_KEY; // 'abc123'
import.meta.env.STRIPE_SECRET; // undefined - без префікса не потрапляє
Правило префікса: у клієнтський код потрапляють лише змінні з префіксом VITE_. Це захист від випадкового витоку: .env Laravel містить паролі бази й ключі API, і без фільтра вони опинилися б у JavaScript, який завантажує кожен відвідувач.
Вбудовані змінні: import.meta.env.MODE (development/production), DEV, PROD (булеві), BASE_URL, SSR.
Як це працює: під час збирання Vite підставляє значення прямо в код як рядкові константи. Наслідки:
- змінна з префіксом
VITE_- публічна. Будь-хто відкриє зібраний файл і побачить значення. Туди можна класти лише те, що й так публічне: публічний ключ Pusher/Reverb, ідентифікатор аналітики, URL API; - значення фіксуються на момент збирання. Змінити
.envна сервері післяnpm run build- нічого не змінить у вже зібраних файлах. Потрібна нова збірка. Якщо один образ має працювати в різних середовищах, конфігурацію передають з бекенду під час виконання (наприклад, через<meta>-теги чи JSON у Blade); - код у гілках
if (import.meta.env.DEV)видаляється з продакшен-збірки.
Режими й файли: .env.local (не в Git), .env.production для vite build, .env.staging для vite build --mode staging. Пріоритет - від конкретнішого до загального.
Чому секрет у фронтенді - не секрет взагалі. Мініфікація, обфускація, «приховані» змінні - нічого не захищає від того, хто відкриє DevTools. Якщо фронтенду потрібно звернутися до API з секретним ключем (оплата, OpenAI, SMS), запит іде через власний бекенд: браузер звертається до Laravel, Laravel - до стороннього API з ключем із серверного .env.
TypeScript: типи власних змінних оголошують в env.d.ts (interface ImportMetaEnv { readonly VITE_APP_NAME: string }) - тоді редактор підказує назви й ловить друкарські помилки.