Вивчай

ES6 Modules

JavaScript ES6 Modules — кольорові контейнери на порту як модульні блоки кодуJavaScript ES6 Modules — кольорові контейнери на порту як модульні блоки коду

До появи модулів весь JavaScript-код жив в одному глобальному просторі. Уяви: 10 файлів підключено через <script>, і кожен може перезаписати змінні іншого. Хаос! Модулі вирішують цю проблему -- кожен файл має власну область видимості, а ми явно вказуємо, що експортувати та імпортувати.


Навіщо потрібні модулі?

Без модулів:

// file1.js
var userName = "Олексій";

// file2.js
var userName = "Марія"; // перезаписує змінну з file1.js!

З модулями:

// file1.js
export const userName = "Олексій"; // доступна тільки при явному імпорті

// file2.js
export const userName = "Марія"; // жодних конфліктів

Переваги модулів:

  • Ізоляція -- змінні не потрапляють у глобальну область
  • Явні залежності -- видно, що від чого залежить
  • Перевикористання -- легко використати код в іншому проекті
  • Зручність -- великий проект розбивається на маленькі файли

Підключення модулів в HTML

Щоб браузер розумів import/export, потрібен атрибут type="module":

<script type="module" src="main.js"></script>
Увага

Модулі працюють тільки через HTTP-сервер (навіть локальний). Якщо відкрити HTML-файл напряму (file://), браузер заблокує імпорти через CORS. Використовуй npx serve або Live Server у VS Code.

У Node.js модулі теж працюють — потрібно додати "type": "module" в package.json:

Запуск ES-модулів у Node.js — помилка без type: module та виправленняЗапуск ES-модулів у Node.js — помилка без type: module та виправлення

Інфо

Модулі автоматично працюють у strict mode ("use strict"), тому помилки на кшталт використання незадекларованої змінної одразу спричинять помилку.


Named exports -- іменовані експорти

Файл може експортувати кілька значень по імені:

// math.js
export const PI = 3.14159;

export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

Або зібрати все в одному місці:

// math.js
const PI = 3.14159;

function add(a, b) {
  return a + b;
}

function multiply(a, b) {
  return a * b;
}

export { PI, add, multiply };

Named imports

Імпортуємо потрібне по імені у фігурних дужках:

// main.js
import { add, multiply } from './math.js';

console.log(add(2, 3));      // 5
console.log(multiply(4, 5)); // 20

Можна імпортувати з перейменуванням:

import { add as sum, PI as pi } from './math.js';

console.log(sum(2, 3)); // 5
console.log(pi);         // 3.14159

Або імпортувати все як об'єкт:

import * as math from './math.js';

console.log(math.add(2, 3)); // 5
console.log(math.PI);        // 3.14159

Default export -- експорт за замовчуванням

Кожен модуль може мати один default export. Це головне значення модуля:

// User.js
export default class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  greet() {
    return `Привіт, я ${this.name}!`;
  }
}

При імпорті default export не потрібні фігурні дужки, і можна дати будь-яке ім'я:

// main.js
import User from './User.js';
// або навіть:
import MyUser from './User.js'; // те саме!

const user = new User("Олексій", "alex@test.com");
console.log(user.greet());

Комбінування named і default

// api.js
const BASE_URL = "https://api.example.com";

async function fetchUsers() {
  const res = await fetch(`${BASE_URL}/users`);
  return res.json();
}

async function fetchPosts() {
  const res = await fetch(`${BASE_URL}/posts`);
  return res.json();
}

export { BASE_URL, fetchPosts };
export default fetchUsers;
// main.js
import fetchUsers, { BASE_URL, fetchPosts } from './api.js';
Порада

Загальноприйнята конвенція: один клас або одна головна функція на файл -- використовуй export default. Якщо файл містить набір утиліт чи констант -- використовуй named exports.


Re-export -- повторний експорт

Зручний спосіб зібрати всі експорти з кількох файлів в одному:

// utils/format.js
export function formatDate(date) { /* ... */ }
export function formatPrice(price) { /* ... */ }

// utils/validate.js
export function validateEmail(email) { /* ... */ }
export function validatePhone(phone) { /* ... */ }

// utils/index.js -- збираємо все
export { formatDate, formatPrice } from './format.js';
export { validateEmail, validatePhone } from './validate.js';

Тепер можна імпортувати з одного місця:

// main.js
import { formatDate, validateEmail } from './utils/index.js';

Організація коду по модулях

Типова структура проекту:

src/
  constants.js     -- константи (URL, налаштування)
  utils.js         -- допоміжні функції
  api.js           -- робота з API
  dom.js           -- маніпуляція DOM
  main.js          -- точка входу

Приклад: калькулятор по модулях

// operations.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => {
  if (b === 0) throw new Error("Ділення на нуль!");
  return a / b;
};
// history.js
const history = [];

export function addToHistory(entry) {
  history.push(entry);
}

export function getHistory() {
  return [...history]; // повертаємо копію
}

export function clearHistory() {
  history.length = 0;
}
// calculator.js
import { add, subtract, multiply, divide } from './operations.js';
import { addToHistory, getHistory } from './history.js';

export function calculate(a, operator, b) {
  let result;

  switch (operator) {
    case '+': result = add(a, b); break;
    case '-': result = subtract(a, b); break;
    case '*': result = multiply(a, b); break;
    case '/': result = divide(a, b); break;
    default: throw new Error(`Невідомий оператор: ${operator}`);
  }

  addToHistory(`${a} ${operator} ${b} = ${result}`);
  return result;
}

export { getHistory };
// main.js
import { calculate, getHistory } from './calculator.js';

console.log(calculate(10, '+', 5));  // 15
console.log(calculate(10, '*', 3));  // 30
console.log(getHistory());
// ["10 + 5 = 15", "10 * 3 = 30"]
Порада

Зверни увагу: масив history в history.js -- приватний. Ніхто не може змінити його напряму, тільки через експортовані функції. Це принцип інкапсуляції -- модулі приховують деталі реалізації.


Named vs Default: коли що?

СитуаціяТип експортуПриклад
Файл з однією сутністю (клас, компонент)export defaultexport default class User
Файл з набором функцій / константnamed exportsexport { add, subtract }
Бібліотека (bagato утиліт)named exportsexport { formatDate, parseDate }
Головна функція + допоміжніdefault + namedexport default fetch; export { BASE_URL }

Підсумок

  • Модулі ізолюють код -- кожен файл має свою область видимості
  • export робить значення доступним для інших модулів
  • Named export: export { a, b } -- імпорт у { }: import { a, b } from
  • Default export: export default X -- імпорт без { }: import X from
  • Re-export: export { func } from './module.js' -- для зручності
  • import * as name -- імпортувати все як об'єкт
  • Розділяй код по файлах: utils.js, api.js, constants.js

Що далі?

У наступному уроці вивчимо замикання (closures) -- одну з найважливіших концепцій JavaScript, яка пояснює, як функції "запам'ятовують" змінні.

Інфо

Корисні посилання: