Типы модульных систем в 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)Сравнительная таблица
| Характеристика | CJS | AMD | UMD | ESM |
|---|---|---|---|---|
| Год появления | 2009 | 2010 | 2011 | 2015 |
| Среда | Node.js | Браузер | Везде | Браузер + Node |
| Синтаксис | require/module.exports | define/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