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

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 сам додає попереднє завантаження залежностей для частин, що імпортуються динамічно.

Не варто дробити надто сильно: десятки дрібних частин - це десятки запитів і затримка «водоспадом», коли одна частина імпортує іншу. Розділяють за межами використання (маршрут, функція), а не кожен компонент окремо.

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

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-level await. Раніше лишався тільки асинхронний 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 - усі означають, що файл інтерпретується не в тій системі модулів, ніж написаний.

Докладніше в документації: Node.js: ES-модулі

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 віддає модулі як є - розмір там не показовий.

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

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 }) - тоді редактор підказує назви й ловить друкарські помилки.

Докладніше в документації: Vite: змінні оточення й режими