Урок 26: Модульность и ES Modules в JavaScript

Если вы мечтаете писать чистый, структурированный и легко поддерживаемый код, этот урок станет вашим верным помощником. Мы разберем, как разбивать код на модули, импортировать и экспортировать функциональность между файлами, а также как правильно организовывать проекты.

Представьте, что вы пишете приложение, где весь код находится в одном файле. Сотни, а то и тысячи строк кода. Найти нужную функцию? Исправить баг? Добавить новую фичу? Это превращается в кошмар. Модульность решает эту проблему, позволяя разбивать код на логические блоки, модули.

Модуль это отдельный файл, который содержит определенную функциональность. Например обработку данных, работу с API или UI-компоненты. Каждый модуль отвечает за свою задачу, а взаимодействие между ними происходит через четко определенные интерфейсы (экспорт и импорт).

История модульности в JavaScript

Раньше в JavaScript не было встроенной системы модулей. Разработчики использовали подходы вроде:

  • IIFE (Immediately Invoked Function Expression): (function() { ... })()
  • CommonJS (используется в Node.js): require() и module.exports
  • AMD (Asynchronous Module Definition)

С появлением ES6 (ES2015) в JavaScript добавили нативную поддержку модулей — ES Modules (ESM). Теперь мы можем использовать import и export прямо в браузере и Node.js (с некоторыми настройками).

Преимущества модульности

  1. Удобство поддержки: Легче находить и исправлять ошибки.
  2. Изоляция кода: Переменные и функции в модуле не попадают в глобальную область видимости.
  3. Переиспользование: Модули можно использовать в разных проектах.
  4. Четкие зависимости: Видно, какие модули от чего зависят.

Экспорт и импорт модулей

Экспорт: Делаем функциональность доступной

Чтобы использовать код из одного модуля в другом, его нужно экспортировать. Есть два типа экспорта: именованный (named) и по умолчанию (default).

Именованный экспорт

// math.js
export const sum = (a, b) => a + b;
export const multiply = (a, b) => a * b;

Или так:

javascript
// math.js
const sum = (a, b) => a + b;
const multiply = (a, b) => a * b;

export { sum, multiply };

Экспорт по умолчанию

Каждый модуль может иметь только один экспорт по умолчанию. Это удобно для главной функции или класса модуля.

// Logger.js
class Logger {
  log(message) {
    console.log(`[LOG]: ${message}`);
  }
}

export default Logger;

Импорт: Используем функциональность из других модулей

Импорт именованных экспортов

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

console.log(sum(2, 3)); // 5
console.log(multiply(2, 3)); // 6

Импорт с псевдонимом (alias)

Если имена конфликтуют, можно использовать as:

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

Импорт всего модуля

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

console.log(MathUtils.sum(1, 2));

Импорт экспорта по умолчанию

import Logger from './Logger.js';

const logger = new Logger();
logger.log('Hello!');

Комбинированный импорт

import Logger, { sum } from './combined.js';

Разделение кода на файлы

Давайте создадим простой проект и разберем его структуру. Представим, что мы пишем приложение для заметок.

project/
├── index.html
├── main.js
├── modules/
│   ├── Note.js
│   ├── storage.js
│   └── ui.js

1. Модуль Note (класс)

// modules/Note.js
export default class Note {
  constructor(title, content) {
    this.title = title;
    this.content = content;
    this.createdAt = new Date();
  }

  getPreview() {
    return `${this.title}: ${this.content.slice(0, 30)}...`;
  }
}

2. Модуль storage (работа с localStorage)

// modules/storage.js
const STORAGE_KEY = 'notes';

export const saveNotes = (notes) => {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(notes));
};

export const loadNotes = () => {
  const data = localStorage.getItem(STORAGE_KEY);
  return data ? JSON.parse(data) : [];
};

3. Модуль UI (отображение)

// modules/ui.js
import { loadNotes, saveNotes } from './storage.js';
import Note from './Note.js';

export function renderNotes() {
  const notes = loadNotes();
  const container = document.getElementById('notes-container');
  container.innerHTML = notes
    .map(note => `<div class="note">${note.getPreview()}</div>`)
    .join('');
}

export function handleAddNote(title, content) {
  const newNote = new Note(title, content);
  const notes = loadNotes();
  notes.push(newNote);
  saveNotes(notes);
  renderNotes();
}

4. main.js (точка входа)

// main.js
import { handleAddNote, renderNotes } from './modules/ui.js';

document.addEventListener('DOMContentLoaded', () => {
  renderNotes();

  document.getElementById('add-note-btn').addEventListener('click', () => {
    const title = prompt('Введите заголовок:');
    const content = prompt('Введите текст заметки:');
    if (title && content) {
      handleAddNote(title, content);
    }
  });
});

5. index.html

<!DOCTYPE html>
<html>
<head>
  <title>Заметки</title>
</head>
<body>
  <button id="add-note-btn">Добавить заметку</button>
  <div id="notes-container"></div>
  <script type="module" src="main.js"></script>
</body>
</html>

Обратите внимание на type="module",  без этого атрибута браузер не распознает ES Modules!

Динамический импорт

Иногда модули нужно загружать только по требованию, чтобы ускорить начальную загрузку. Для этого используют динамический импорт:

document.getElementById('settings-btn').addEventListener('click', async () => {
  const settingsModule = await import('./modules/settings.js');
  settingsModule.showSettings();
});

Практические задачи

Задача 1: Создайте модуль для валидации форм

  1. Создайте файл validation.js.
  2. Экспортируйте функции:
    • validateEmail(email) — проверяет, корректный ли email.
    • validatePassword(password) — проверяет длину пароля (минимум 8 символов).
  3. Импортируйте их в главный файл и используйте в форме регистрации.

Решение:

// validation.js
export const validateEmail = (email) => {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
};

export const validatePassword = (password) => {
  return password.length >= 8;
};

// main.js
import { validateEmail, validatePassword } from './validation.js';

document.getElementById('register-form').addEventListener('submit', (e) => {
  e.preventDefault();
  const email = e.target.email.value;
  const password = e.target.password.value;

  if (!validateEmail(email)) {
    alert('Некорректный email!');
    return;
  }

  if (!validatePassword(password)) {
    alert('Пароль должен быть не короче 8 символов!');
    return;
  }

  // Отправка формы...
});

Задача 2: Рефакторинг калькулятора

У вас есть код калькулятора в одном файле. Разбейте его на модули:

  • math.js — функции addsubtractmultiplydivide.
  • calculator.js — логика обработки ввода и вывода.
  • main.js — инициализация приложения.

Решение:

// math.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) => (b !== 0 ? a / b : NaN);

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

export function calculate(a, b, operator) {
  switch (operator) {
    case '+': return math.add(a, b);
    case '-': return math.subtract(a, b);
    case '*': return math.multiply(a, b);
    case '/': return math.divide(a, b);
    default: throw new Error('Неизвестный оператор');
  }
}

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

const result = calculate(10, 5, '+');
console.log(result); // 15

Частые ошибки и как их избежать

  1. Отсутствие type="module" в теге <script>: Браузер не распознает import/export.
  2. Попытка использовать модули локально без сервера: Запускайте код через локальный сервер (например, Live Server в VS Code).
  3. Циклические зависимости: Модуль A импортирует модуль B, который импортирует модуль A. Пересмотрите архитектуру.
  4. Ошибки в путях: Используйте относительные пути (./modules/file.js), а не абсолютные.

Модульность это основа современных JavaScript-приложений. Она делает код организованным, тестируемым и масштабируемым. Практикуйтесь, разбивайте свои проекты на модули и вы сразу заметите как улучшится ваш стиль программирования!

Полный курс «JavaScript для начинающих» с остальными уроками доступен по ссылке:
https://max-gabov.ru/javascript-dlya-nachinaushih