Junior: питання на співбесіді з теми «Модулі й збирання»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
3 питання
ES-модуль - файл JavaScript з власною областю видимості, який явно експортує те, що дає назовні, й імпортує те, що йому потрібно.
// money.js
export function formatPrice(amount) {
return `${amount.toFixed(2)} грн`;
}
// app.js
import { formatPrice } from './money.js';
console.log(formatPrice(100));
<script type="module" src="/app.js"></script>
Як було до модулів: усі <script> на сторінці ділили одну глобальну область видимості. Функція formatPrice з одного файлу ставала глобальною і могла перезаписати однойменну з іншого. Порядок підключення скриптів доводилося вибудовувати вручну, а залежності між файлами ніде не були записані.
Чим модулі відрізняються:
- власна область видимості: змінні верхнього рівня модуля не потрапляють у
window; - явні залежності: з
importвидно, звідки що береться, - це читає і людина, і збирач; - строгий режим завжди (
'use strict'не потрібен); - виконуються один раз: скільки б модулів не імпортували
money.js, він завантажиться й виконається лише раз, а всі отримають той самий екземпляр; - відкладене виконання:
<script type="module">поводиться якdefer- виконується після розбору HTML і не блокує його; - top-level
awaitдозволений; thisна верхньому рівні -undefined, а неwindow;- завантаження з іншого домену підпорядковується CORS.
Імпорти статичні: import - лише на верхньому рівні модуля, шлях - рядок-константа. Завдяки цьому залежності відомі ще до виконання коду - звідси можливість tree shaking. Для завантаження «за потреби» є динамічний import().
Модулі в Laravel-проєкті: resources/js/app.js - точка входу ES-модуля. Vite збирає граф імпортів, а директива @vite в Blade підключає результат як <script type="module">. Пакети з npm імпортуються за назвою (import axios from 'axios') - браузер такого шляху не розуміє, тому розв'язує його збирач (або import map без збирача).
Іменований експорт - модуль може мати їх скільки завгодно, імпортують їх за точною назвою у фігурних дужках:
// validation.js
export function isEmail(value) { /* ... */ }
export const MAX_LENGTH = 255;
// інший файл
import { isEmail, MAX_LENGTH } from './validation.js';
import { isEmail as checkEmail } from './validation.js'; // перейменування
import * as validation from './validation.js'; // усе як об'єкт-простір імен
Експорт за замовчуванням - один на модуль, імпортується без дужок і під будь-якою назвою:
// Modal.vue, api.js
export default function createApi(baseUrl) { /* ... */ }
import createApi from './api.js';
import makeClient from './api.js'; // теж працює - та сама функція
Модуль може мати й те, й інше: import axios, { AxiosError } from 'axios'.
Аргументи на користь іменованих експортів:
- однакові назви всюди: не буває ситуації, коли в одному файлі функцію імпортували як
createApi, в іншому якapiFactory, - пошук і рефакторинг простіші; - помилка в назві помітна одразу:
import { isEmial }- збирач чи редактор скаже, що такого експорту немає. З експортом за замовчуванням будь-яка назва «правильна»; - автоімпорт у редакторі працює точніше;
- tree shaking простіше розібратися з окремими експортами, ніж з одним великим об'єктом за замовчуванням.
Через це багато команд мають правило лінтера «лише іменовані експорти» (import/no-default-export).
Коли експорт за замовчуванням природний:
- файл = одна сутність: компонент Vue (
.vue-файли експортують компонент за замовчуванням), сторінка в Next.js, конфіг (vite.config.js-export default defineConfig(...)); - вимога інструмента, що шукає саме
default.
Реекспорт - для «бочок» (index.js, що збирає експорти папки):
export { isEmail, MAX_LENGTH } from './validation.js';
export { default as Modal } from './Modal.vue';
export * from './formatters.js';
Великі «бочки» мають ціну: імпорт однієї функції з index.js змушує збирач обробити всі реекспортовані модулі, а в режимі розробки - ще й завантажити їх.
Браузер уміє виконувати ES-модулі, тож чому не віддати йому файли як є? Тому що код, який зручно писати, і код, який швидко завантажується, - різні.
Що робить збирач під час розробки (Vite npm run dev):
- розв'язує імпорти пакетів:
import axios from 'axios'браузер не розуміє - Vite перетворює на шлях до файлу вnode_modules; - перетворює те, що браузер не вміє: TypeScript,
.vue-файли, JSX, Tailwind і PostCSS; - гаряча заміна модулів (HMR): змінили компонент - оновився лише він, без перезавантаження сторінки й зі збереженням стану;
- віддає модулі браузеру майже без обробки, тож сервер стартує за мілісекунди незалежно від розміру проєкту - у цьому головна перевага Vite над старими збирачами.
Що робить збирач для продакшену (npm run build):
- об'єднує модулі в невелику кількість файлів - сотні дрібних запитів повільніші за кілька великих;
- мініфікує: прибирає пробіли й коментарі, скорочує імена змінних;
- tree shaking: викидає код, який ніде не імпортується;
- розділення коду: спільні залежності - в окремий файл, сторінки й важкі компоненти - в окремі частини, що завантажуються за потреби;
- хешовані імена файлів (
app-3f9a1c.js): вміст змінився - змінилася назва. Тоді файли можна кешувати в браузері «назавжди», а після деплою користувачі гарантовано отримають нову версію; - маніфест - відповідність вихідних файлів і зібраних. Саме з нього директива
@vite('resources/js/app.js')у Laravel дізнається, який файл підключити.
У Laravel-проєкті:
npm run dev- сервер розробки,@viteпідключає файли з нього (адресу записано уpublic/hot);npm run build- збірка вpublic/build,@viteбере шляхи зpublic/build/manifest.json;- помилка «Unable to locate file in Vite manifest» означає, що збірку не виконано або файл не вказано в
inputконфігураціїlaravel-vite-plugin.
Альтернативи: webpack (старіший, гнучкіший, повільніший), esbuild і Rollup (на яких побудовано Vite), Rolldown (новий швидкий збирач для майбутніх версій Vite). Можна й зовсім без збирача - import maps і нативні модулі, - для невеликих проєктів без TypeScript чи .vue-файлів.