Модульная система JavaScript: от истоков до ES Modules

Раздел: Node.js -> Основы модулей

Основы модулей JavaScript

Как организовать код с помощью ES модулей (ESM)?

ES модули - это официальный стандарт для модульной системы в JavaScript, поддерживаемый современными браузерами и Node.js. Каждый файл с расширением .mjs или .js (при указании type: module в package.json) становится модулем со своей областью видимости. Для экспорта используется ключевое слово export, для импорта - import.

// math.mjs
export const sum = (a, b) => a + b;
export const PI = 3.14159;
export default function multiply(a, b) { return a * b; }

Js modules (модули javascript)

// app.mjs
import multiply, { sum, PI } from './math.mjs';
console.log(sum(2, 3));      // 5
console.log(PI);              // 3.14159
console.log(multiply(3, 4)); // 12

Импорт с переименованием: import { sum as add } from './math.mjs';. Можно также импортировать все экспорты: import * as math from './math.mjs';.

Типичные проблемы и их решение:

1. Ошибка CORS. При загрузке модулей из локального файла (file://) браузер блокирует их из-за политики CORS. Решение: использовать локальный сервер (например, npx serve . или http-server).

2. Ошибка: Cannot use import statement outside a module. Возникает, если не указан type="module" в теге script или в package.json. Решение: добавить <script type="module" src="app.mjs"></script>.

3. Циклические зависимости. ESM решает их корректно, но при неправильном использовании можно получить ReferenceError из-за временных мертвых зон. Рекомендация: избегать взаимных импортов, использовать общие модули.

Как использовать CommonJS модули в Node.js?

До широкого внедрения ESM в Node.js основной системой была CommonJS. Она использует require для импорта и module.exports или exports для экспорта. Модули CommonJS загружаются синхронно, что подходит для серверных приложений, но не для браузера без сборщиков.

// math.js
module.exports.sum = (a, b) => a + b;
module.exports.PI = 3.14159;
// или для одного значения:
module.exports = function multiply(a, b) { return a * b; };
// app.js
const { sum, PI } = require('./math.js');
const multiply = require('./math.js'); // если export default
console.log(sum(2, 3));
console.log(PI);
console.log(multiply(3, 4));

Проблемы CommonJS:

1. Синхронность. require блокирует выполнение до завершения загрузки модуля. В серверной среде это приемлемо, но для браузера требуется асинхронная загрузка.

2. Циклические зависимости. В CommonJS модуль может получить частично инициализированный объект экспорта, что приводит к трудноуловимым ошибкам. Решение: реорганизовать код, выделив общие зависимости.

3. Отсутствие статического анализа. CommonJS не позволяет сборщикам (таким как Webpack) выполнять tree shaking, так как экспорт определятся динамически.

Как смешивать ESM и CommonJS в одном проекте?

В Node.js можно импортировать ESM модуль из CommonJS (с помощью import()) и наоборот - импортировать CommonJS из ESM (используя createRequire из модуля module). Важно: ESM по умолчанию работает в строгом режиме, а CJS - нет.

// index.mjs (ESM)
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const cjsModule = require('./common.cjs');
console.log(cjsModule);
// common.cjs (CJS)
module.exports.hello = 'world';

Ошибка при смешивании: попытка использовать require в ESM без createRequire вызывает ReferenceError. Решение описано выше.

Как использовать динамический импорт (import())?

Динамический импорт позволяет загружать модули по требованию, возвращая Promise. Это полезно для реализации ленивой загрузки, плагинов или маршрутов.

// main.js
const modulePath = './utils.js';
import(modulePath)
  .then(module => {
    module.default(); // экспорт по умолчанию
    module.namedExport();
  })
  .catch(err => console.error('Ошибка загрузки модуля', err));

С использованием async/await:

async function loadUtils() {
  try {
    const module = await import('./utils.js');
    module.default();
  } catch (err) {
    console.error(err);
  }
}

Проблемы:

Динамический импорт всегда асинхронен, поэтому его нельзя использовать для экспорта значений. Также важно помнить, что путь передается строкой, и если строка формируется динамически, это может усложнить статический анализ.

Расширенные примеры работы с модулями

Ниже представлены подробные примеры, демонстрирующие различные сценарии использования модульных систем JavaScript.

Реэкспорт модулей (barrel-файлы)

Удобный способ объединить несколько модулей в один экспорт.

Пример
// services/user.js
export function getUser(id) { return { id, name: 'Alice' }; }

// services/order.js
export function getOrder(id) { return { id, items: [] }; }

// services/index.js (реэкспорт)
export { getUser } from './user.js';
export { getOrder } from './order.js';
// или export * from './user.js'; // все именованные
Пример
// app.js (импорт из одного файла)
import { getUser, getOrder } from './services/index.js';
console.log(getUser(1));

Результат:

{ id: 1, name: 'Alice' }

Экспорт и импорт с псевдонимами

Пример
// constants.js
const PI = 3.14159;
const E = 2.71828;
export { PI as pi, E as euler };
Пример
// main.js
import { pi, euler } from './constants.js';
console.log(pi, euler);

Результат: 3.14159 2.71828

Динамический импорт с условиями

Пример
// platform.js
export const isBrowser = typeof window !== 'undefined';

// app.js
export async function loadPlatformModule() {
  if (isBrowser) {
    const browserModule = await import('./browser.js');
    return browserModule.default;
  } else {
    const nodeModule = await import('./node.js');
    return nodeModule.default;
  }
}

Плагинная архитектура с динамическим импортом

Пример
// plugins/pluginA.js
export function execute() { return 'Plugin A executed'; }

// plugins/pluginB.js
export function execute() { return 'Plugin B executed'; }

// pluginLoader.js
export async function loadPlugin(pluginName) {
  try {
    const plugin = await import(`./plugins/${pluginName}.js`);
    return plugin.execute;
  } catch (e) {
    console.error(`Plugin ${pluginName} not found`);
    return null;
  }
}

// main.js
import { loadPlugin } from './pluginLoader.js';
const exec = await loadPlugin('pluginA');
if (exec) console.log(exec()); // 'Plugin A executed'

Использование import.meta.url и createRequire

Для доступа к информации о текущем модуле в ESM.

Пример
// module.mjs
console.log(import.meta.url); // file:///path/to/module.mjs
console.log(import.meta.resolve('./other.mjs')); // полный URL до other.mjs

Создание require для подключения JSON (CommonJS):

Пример
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const config = require('./config.json');
console.log(config.database);

Tree shaking с ES модулями (пример)

Сборщики (Webpack, Rollup) удаляют неиспользуемый экспорт на основе статического анализа.

Пример
// utils.js
export function usedFunction() { return 'used'; }
export function unusedFunction() { return 'unused'; }

// main.js
import { usedFunction } from './utils.js';
console.log(usedFunction()); // после сборки unusedFunction будет удалён из бандла

Результат в бандле: присутствует только usedFunction.

Особенности модулей в Node.js: файлы .mjs и .cjs

Пример
// package.json
{
  "type": "module" // все .js файлы трактуются как ESM
  // для CommonJS используйте .cjs
}
Пример
// example.cjs (CommonJS)
module.exports = { a: 1 };

// example.mjs (ESM)
import example from './example.cjs';
console.log(example.a); // 1

Результат: 1

Модули JavaScript - comments

En
Js modules (javascript)