Модульная система JavaScript: от истоков до ES Modules
Основы модулей 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