Если вы когда-нибудь открывали чужой проект и видели один файл на 5000 строк, вы поймёте, о чём речь. Модули — это спасение от хаоса. Они позволяют разбить код на кусочки, которые легко читать, тестировать и переиспользовать. В этой статье разберём, как устроена модульная система в JavaScript, какие есть форматы и как применять их на практике.

Digital-студия WNDER

Что такое модуль и зачем он нужен

Модуль — это просто файл с кодом, который что-то экспортирует (отдаёт наружу) и/или импортирует (берёт из других). Представьте, что вы собираете мебель. У вас есть отдельные пакеты с болтами, гайками и инструкциями. Не нужно вываливать всё в одну кучу — вы берёте нужный пакет, когда он понадобится. Так и с модулями: каждый отвечает за свою задачу.

Главные плюсы модулей:

  • Изоляция — переменные внутри модуля не текут наружу, если их не экспортировать.
  • Переиспользование — один раз написали функцию, используем в десяти местах.
  • Удобство поддержки — проще найти баг в маленьком файле, чем в гигантском.

До появления официальных модулей в JavaScript народ выкручивался как мог: использовали IIFE (немедленно вызываемые функции), глобальные объекты, паттерн «модуль» через замыкания. Но это всё костыли. Сейчас есть стандарт ES Modules (ESM), который поддерживается во всех современных браузерах и Node.js.

Основы синтаксиса: export и import

В ES-модулях есть два ключевых слова: export и import. Экспортировать можно переменные, функции, классы — что угодно. Есть два вида экспорта: именованный и по умолчанию (default).

Именованный экспорт позволяет отдать несколько сущностей из одного файла. Например, создадим файл math.js:

// math.js
export const PI = 3.14;

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

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

Теперь в другом файле мы можем импортировать только то, что нужно:

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

console.log(sum(2, 3)); // 5
console.log(PI); // 3.14

Экспорт по умолчанию — это когда модуль отдаёт что-то одно, самое главное. Такой экспорт может быть только один на файл.

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

Импорт default-экспорта можно называть как угодно:

// app.js
import User from './user.js';

const user = new User('Alice');

Можно комбинировать: импортировать и default, и именованные экспорты. Например:

import User, { sum, PI } from './user.js';

Также можно импортировать всё содержимое модуля как объект:

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

console.log(math.sum(2, 3)); // 5
console.log(math.PI); // 3.14
Правило: Если модуль экспортирует что-то одно и это очевидно из названия файла — используйте default export. Если несколько сущностей — именованный экспорт. Не смешивайте без необходимости.

Подключение модулей в браузере

Чтобы браузер понял, что скрипт — это модуль, нужно указать атрибут type="module" в теге <script>. Например:

<script type="module" src="main.js"></script>

Или можно писать код прямо в HTML:

<script type="module">
  import { sum } from './math.js';
  console.log(sum(2, 3));
</script>

Важные особенности модулей в браузере:

  • Загрузка происходит отложенно (deferred) — модули не блокируют парсинг HTML.
  • Код выполняется в строгом режиме ('use strict') по умолчанию.
  • Модули имеют свою область видимости — переменные не попадают в window.
  • Для загрузки модулей с других доменов нужны CORS-заголовки.

Если открыть HTML-файл локально (через file://), модули не загрузятся из-за политики безопасности. Нужен локальный сервер. Например, можно запустить npx serve или использовать расширение Live Server в VS Code.

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

Иногда нужно загрузить модуль не сразу, а по требованию — например, при клике на кнопку. Для этого есть функция import(), которая возвращает промис.

button.addEventListener('click', async () => {
  const module = await import('./heavyModule.js');
  module.doSomething();
});

Это позволяет уменьшить начальный размер бандла и ускорить загрузку страницы. Динамический импорт часто используется в React.lazy, Vue Router и других фреймворках.

Сравнение форматов модулей

До ES Modules были и другие форматы: CommonJS (Node.js), AMD (RequireJS), UMD (универсальный). Сейчас стандарт — ESM, но в Node.js до сих пор можно встретить CommonJS. Давайте сравним.

Формат Синтаксис Среда Особенности
ES Modules import / export Браузер, Node.js (с флагом или .mjs) Статический анализ, асинхронная загрузка
CommonJS require / module.exports Node.js Синхронная загрузка, кэширование
AMD define / require Браузер (RequireJS) Асинхронная загрузка, сложный синтаксис
UMD Комбинация Браузер и Node.js Универсальный, но громоздкий

Сейчас рекомендуется использовать ES Modules везде, где возможно. В Node.js для этого нужно либо назвать файлы с расширением .mjs, либо указать "type": "module" в package.json.

Лайфхак: Если вы пишете на Node.js и хотите использовать ESM, но у вас много зависимостей на CommonJS, не бойтесь — Node.js поддерживает импорт CommonJS из ESM. Просто используйте import для всего.

Практические советы по организации модулей

Чтобы модули приносили пользу, а не боль, следуйте этим рекомендациям:

  • Один модуль — одна ответственность. Если файл делает слишком много, разбейте его.
  • Избегайте циклических зависимостей. Когда модуль A импортирует B, а B импортирует A, это может привести к неожиданностям. Старайтесь выносить общее в отдельный модуль.
  • Используйте index.js для реэкспорта. В папке создайте файл index.js, который экспортирует всё содержимое папки. Это упрощает импорты.
  • Группируйте по функциям, а не по типам. Например, папка user/ с файлами user.js, userService.js, userHelpers.js лучше, чем папки models/, services/, helpers/.

Пример реэкспорта через index.js:

// user/index.js
export { default as User } from './user.js';
export { getUser } from './userService.js';
export { formatName } from './userHelpers.js';

Теперь можно импортировать всё из одной точки:

import { User, getUser, formatName } from './user/index.js';

Это особенно удобно в больших проектах.

Что в итоге

Модульная система в JavaScript — это не просто синтаксис import/export. Это способ думать о коде как о наборе независимых блоков. Начните с малого: вынесите повторяющиеся функции в отдельный файл, попробуйте экспортировать и импортировать их. Постепенно вы почувствуете, как код становится чище и понятнее. И помните: модули — ваш друг, а не враг. Используйте их с умом.

Студия WNDER