Типы модульных систем в JavaScript

Предыстория

До 2009 года в JavaScript не было никакой стандартной модульной системы. Весь код писался в глобальном скоупе, а “модули” имитировались через IIFE (Immediately Invoked Function Expression):

// "Модуль" до модулей — IIFE паттерн
var MyModule = (function() {
  var private = 'secret';
  return {
    getPrivate: function() { return private; }
  };
})();

Это порождало конфликты имён, порядок подключения скриптов имел значение, и всё держалось на дисциплине разработчика.


1. CommonJS (CJS) — 2009

Появился: 2009, вместе с Node.js
Среда: Node.js (серверная)
Расширение файла: .js, .cjs

// Экспорт
const add = (a, b) => a + b;
module.exports = { add };
// или
module.exports.add = add;
 
// Импорт
const { add } = require('./math');
const fs = require('fs'); // встроенный модуль Node

Характеристики:

  • Синхронныйrequire() блокирует выполнение до загрузки модуля
  • Динамическийrequire() можно вызывать в любом месте кода, внутри условий
  • Кеширование — повторный require() возвращает закешированный результат
  • Не tree-shakable — из-за динамической природы сборщик не может статически определить что используется
// Динамический импорт — валидный CommonJS
if (isDev) {
  const debug = require('./debug'); // это легально
}
 
// Динамический экспорт — тоже валидный CommonJS
module.exports[condition ? 'a' : 'b'] = fn; // сборщик не может проанализировать

Проблема в браузере: синхронная загрузка блокировала бы UI — файлы грузятся по сети, а не с диска. Поэтому CJS не подходит для браузера напрямую.


2. AMD (Asynchronous Module Definition) — 2010

Появился: 2010, проект RequireJS
Среда: Браузер
Расширение файла: .js

// Определение модуля
define('myModule', ['dependency1', 'dependency2'], function(dep1, dep2) {
  return {
    doSomething: function() { return dep1.value + dep2.value; }
  };
});
 
// Использование
require(['myModule'], function(myModule) {
  myModule.doSomething();
});

Характеристики:

  • Асинхронный — специально разработан для браузера, не блокирует UI
  • Явное объявление зависимостей в массиве
  • Некрасивый синтаксис — много колбеков, трудно читать
  • Сегодня практически не используется

3. UMD (Universal Module Definition) — 2011

Появился: 2011
Среда: Браузер + Node.js (универсальный)

UMD — это не новая система, а паттерн-обёртка, которая определяет среду и использует нужный формат:

(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    define(['dependency'], factory);      // AMD
  } else if (typeof module === 'object' && module.exports) {
    module.exports = factory(require('dependency')); // CommonJS
  } else {
    root.MyLibrary = factory(root.Dependency);       // глобальная переменная
  }
}(typeof self !== 'undefined' ? self : this, function (dependency) {
  return {}; // публичный API
}));

Характеристики:

  • Работает везде: браузер, Node.js, AMD
  • Уродливый бойлерплейт
  • Был стандартом для npm-библиотек до ESM
  • Сегодня заменяется на ESM + CJS dual publish

4. SystemJS — 2013

Появился: 2013
Среда: Браузер (полифилл)

Универсальный загрузчик модулей — умеет загружать CJS, AMD, ESM прямо в браузере. Использовался как полифилл до нативной поддержки ESM в браузерах. Сегодня актуален только в специфичных Enterprise-окружениях и Angular (частично).


5. ES Modules (ESM) — 2015 (стандарт), 2017+ (браузеры)

Появился: Спецификация ES2015 (ES6), нативная поддержка браузеров с 2017-2018
Среда: Браузер + Node.js (с 12+)
Расширение файла: .js, .mjs (явное указание ESM для Node)

// Именованный экспорт
export const add = (a, b) => a + b;
export function multiply(a, b) { return a * b; }
 
// Экспорт по умолчанию
export default class Calculator { ... }
 
// Именованный импорт
import { add, multiply } from './math.js';
 
// Импорт по умолчанию
import Calculator from './calculator.js';
 
// Импорт всего
import * as MathUtils from './math.js';
 
// Динамический импорт (ES2020) — возвращает Promise
const { add } = await import('./math.js');

Характеристики:

  • Статический — структура импортов/экспортов фиксирована на этапе разбора, до выполнения кода
  • Асинхронный — браузер загружает файлы параллельно, не блокируя UI
  • Tree-shakable — сборщик статически знает что используется, остальное удаляет
  • Live bindings — импортируемые значения это ссылки, а не копии: если экспортирующий модуль изменил переменную — импортёр видит новое значение
  • Строгий режим — все ESM-файлы автоматически в 'use strict'
  • Top-level await — можно использовать await вне async-функции (ES2022)
// Live bindings — особенность ESM
// counter.js
export let count = 0;
export function increment() { count++; }
 
// main.js
import { count, increment } from './counter.js';
console.log(count); // 0
increment();
console.log(count); // 1 — видим изменение! (в CJS была бы копия — 0)

Сравнительная таблица

ХарактеристикаCJSAMDUMDESM
Год появления2009201020112015
СредаNode.jsБраузерВездеБраузер + Node
Синтаксисrequire/module.exportsdefine/requireОбёрткаimport/export
ЗагрузкаСинхроннаяАсинхроннаяЗависитАсинхронная
Tree-shaking
Динамический import✅ (везде)✅ (import())
Статический анализ
Нативно в браузере
Актуальность✅ (Node legacy)⚠️ (библиотеки)✅ Стандарт

.js vs .mjs vs .cjs

Node.js ввёл расширения для явного указания формата:

РасширениеФорматКогда использовать
.jsЗависит от "type" в package.jsonПо умолчанию
.mjsВсегда ESMЯвное указание ESM в любом контексте
.cjsВсегда CommonJSЯвное указание CJS в ESM-пакете

В package.json:

{ "type": "module" }   // все .js файлы = ESM
{ "type": "commonjs" } // все .js файлы = CJS (по умолчанию)

Dual publish — современный стандарт для npm-пакетов

Современные библиотеки публикуют оба формата, чтобы работать везде:

{
  "name": "my-library",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",   // ESM — для сборщиков и браузеров
      "require": "./dist/index.cjs"   // CJS — для старого Node.js кода
    }
  }
}

Сборщик (Vite, Webpack) автоматически выберет ESM-версию → tree-shaking работает. Node.js без сборщика возьмёт CJS-версию → совместимость сохраняется.


Итог: что использовать сегодня

  • Новый проект в браузере → ESM везде
  • Новый Node.js проект → ESM ("type": "module" в package.json)
  • npm-библиотека → dual publish (ESM + CJS)
  • Legacy Node.js код → CJS, постепенно мигрировать
  • AMD/UMD → не писать с нуля, только если поддерживаешь старый код

См. также

  • es-features — import/export среди ES-фич
  • gof — паттерн Module