Перейти до основного вмісту

Модульність коду

Концепція модулів як способу організації коду існувала давно. Зі збільшенням проекту і його кодової бази, її намагаються розбити на файли, у кожному з яких описується окремий функціонал.

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

Підтримка - добре розроблений модуль, покликаний максимально зменшити залежність від інших частин коду. Це дозволить розширювати функціонал застосунку, не побоюючись порушити його роботу в цілому. Оновлення одного модуля набагато простіше, якщо модуль - самодостатній.

Простір імен - змінні, що не входять в область видимості функції, є глобальними. Внаслідок цього, як правило, відбувається забруднення простору імен, де повністю непов'язаний код розділяє глобальні змінні. Модулі дозволяють уникнути забруднення простору імен, створюючи окрему область видимості для змінних.

Повторне використання - усі розробники копіювали готовий код у нові проекти, змінюючи його під специфіку проекту. Це, очевидно, величезна трата часу. Набагато краще, коли є модуль, який можна повторно використовувати знову і знову, без необхідності знати що-небудь про оточення, в якому він використовується.

Збирання модулів

Збирання модулів - це процес конкатенації групи модулів і їх залежностей в один або групу файлів.

Зазвичай код ділять на папки і файли, до того ж необхідно підключити зовнішні бібліотеки. В результаті кожен з цих файлів потрібно додати в основний HTML-файл в тезі <script>, який потім завантажується браузером.

Наявність окремих тегів <script> для кожного файлу означає, що браузер буде завантажувати кожен файл окремо, що негативно позначається на швидкості завантаження сторінки. Щоб обійти цю проблему, файли об'єднуються в один або пару файлів з метою зменшення кількості запитів. Але залишається проблема управління залежностями між модулями.

Якщо використовуються системи модулів, такі як CommonJS або ESM, необхідно використовувати інструмент для їх перетворення у правильно впорядкований і доступний для браузера код. Саме тут починають діяти Webpack та інші бандлери (від англійського bundle).

ECMAScript Modules (ESM)

Донедавна в мові не було вбудованої модульної системи. ESM мають компактний декларативний синтаксис і можливість асинхронного завантаження. ES-модуль - це фрагмент JS-коду, що використовується багато раз та експортує певні об'єкти, роблячи їх доступними для інших модулів.

greeter.js
const helloMessage = "hello!";
const goodbyeMessage = "goodbye!";

export const hello = () => helloMessage;
export const goodbye = () => goodbyeMessage;
index.js
import { hello, goodbye } from "./greeter";

console.log(hello()); // "hello!"
console.log(goodbye()); // "goodbye!"

Кожен JS-файл зберігає код в унікальному контексті модуля та імпортує необхідні йому залежності, і експортує все, що інші модулі повинні імпортувати. Операції експорту/імпорту реалізовані конструкціями import і export. Є дві очевидні переваги цього підходу - запобігання забрудненню глобального простору імен і явне зазначення залежностей.

Нова система модулів відрізняється від CommonJS та інших, перш за все тим, що це стандарт. А значить, з часом, буде повністю підтримуватися браузерами нативно, без додаткових інструментів. Однак, зараз браузерна підтримка - неповна, тому ESM використовуються разом з інструментами збірки модулів, такими як Webpack, Parcel та іншими.

Цікаво

ESM розроблені з урахуванням статичного аналізу. Це означає, що під час імпорту модулів, імпорт обробляється під час компіляції, тобто до запуску скрипту. Це дозволяє видаляти експорт, який не використовується іншими модулями, перш ніж запускати скрипт, що може призвести до значної економії ваги JS-файлу, зменшивши навантаження на браузер. Це називається tree shaking і виконується бандлерами автоматично під час збирання JS коду.

Named export

Модуль може експортувати декілька сутностей, які відрізняються своїми іменами і називаються іменованими експортами. Щоб імпортувати їх в інший модуль, необхідно знати імена експортованих сутностей, які ми хочемо імпортувати.

Перший спосіб - це використовувати ключове слово export перед усіма сутностями, які необхідно експортувати. Вони будуть додані як властивості в експортований об'єкт. Під час імпорту ми деструктуризуємо властивості з імпортованого об'єкта.

my-module.js
const sqrt = Math.sqrt;
export const square = x => x * x;
export const diag = (x, y) => sqrt(square(x) + square(y));
main.js
import { square, diag } from "./path/to/my-module";

console.log(square(11)); // 121
console.log(diag(4, 3)); // 5

Другий спосіб - це явно вказати об'єкт з властивостями для експорту.

my-module.js
const sqrt = Math.sqrt;
const square = x => x * x;
const diag = (x, y) => sqrt(square(x) + square(y));

export { square, diag };
main.js
import { square, diag } from "./path/to/myModule";

console.log(square(11)); // 121
console.log(diag(4, 3)); // 5

Наступний синтаксис імпортує всі експорти модуля як об'єкт із зазначеним ім'ям. Це називається namespace import.

main.js
import * as myModule from "./path/to/my-module";

console.log(myModule.square(11)); // 121
console.log(myModule.diag(4, 3)); // 5

Default export

Часто модуль експортує всього одну сутність - такий експорт зручний для імпорту. Експорт за замовчуванням - найголовніше експортоване значення, яке може бути чим завгодно: змінною, функцією, класом тощо.

my-func.js
export default function myFunc() {
// ...
}
my-class.js
export default class MyClass {
// ...
}
main.js
import myFunc from "./path/to/my-func";
import MyClass from "./path/to/my-class";

myFunc();

const inst = new MyClass();

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