Поддержка ES-модулей и CommonJS

Модульность является одной из ключевых особенностей современной разработки. Разделение кода на независимые файлы позволяет упрощать сопровождение проектов, повышать переиспользуемость компонентов и уменьшать связанность между частями приложения.

В экосистеме JavaScript исторически сформировались две основные модульные системы:

  • CommonJS (CJS) — традиционный стандарт платформы Node.js.
  • ES Modules (ESM) — официальный стандарт ECMAScript, поддерживаемый браузерами и современными инструментами сборки.

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


ES-модули

ES-модули появились как часть стандарта ECMAScript и стали официальным способом организации кода в JavaScript.

Экспорт значений

Модуль может экспортировать переменные, функции, классы и другие сущности.

export const PI = 3.14159;

export function square(x) {
    return x * x;
}

Другой файл может импортировать эти значения:

import { PI, square } from "./math.js";

console.log(PI);
console.log(square(4));

Экспорт по умолчанию

Модуль может содержать один экспорт по умолчанию.

export default function greet(name) {
    return `Hello, ${name}`;
}

Импорт выполняется следующим образом:

import greet from "./greet.js";

console.log(greet("John"));

Переименование импортируемых сущностей

При необходимости импортируемые элементы могут получать другие имена.

import { square as calcSquare } from "./math.js";

console.log(calcSquare(5));

Импорт всех экспортов

Для получения объекта со всеми экспортируемыми значениями используется специальный синтаксис.

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

console.log(math.PI);
console.log(math.square(10));

CommonJS

До появления ES-модулей экосистема Node.js использовала CommonJS.

Экспорт через module.exports

function square(x) {
    return x * x;
}

module.exports = {
    square
};

Импорт:

const math = require("./math");

console.log(math.square(4));

Экспорт через exports

Сокращённая форма:

exports.square = function (x) {
    return x * x;
};

exports.cube = function (x) {
    return x * x * x;
};

Использование:

const math = require("./math");

console.log(math.cube(3));

Поддержка ES-модулей в Parcel

Одной из сильных сторон Parcel является встроенная поддержка современных стандартов JavaScript без необходимости дополнительной настройки.

Достаточно использовать стандартный синтаксис:

import { calculateTotal } from "./utils.js";

console.log(calculateTotal());

Parcel автоматически:

  • анализирует дерево зависимостей;
  • определяет точки входа;
  • объединяет модули в бандлы;
  • устраняет неиспользуемый код;
  • оптимизирует импортируемые зависимости.

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

Parcel самостоятельно определяет используемую модульную систему.

Например, следующий файл будет распознан как ES-модуль:

import { api } from "./api.js";

export function loadData() {
    return api.get();
}

Файл CommonJS:

const api = require("./api");

module.exports = {
    loadData() {
        return api.get();
    }
};

Дополнительная конфигурация обычно не требуется.


Смешивание ES Modules и CommonJS

Реальные проекты часто содержат обе модульные системы одновременно.

Parcel умеет корректно связывать такие зависимости.

Импорт CommonJS-модуля в ES-модуль

Файл CommonJS:

module.exports = function sum(a, b) {
    return a + b;
};

ES-модуль:

import sum from "./sum.js";

console.log(sum(2, 3));

Parcel автоматически создаёт совместимый интерфейс.


Импорт ES-модуля в CommonJS

ES-модуль:

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

CommonJS:

const math = require("./math.js");

console.log(math.multiply(4, 5));

Во время сборки Parcel обрабатывает такую зависимость и обеспечивает корректную работу.


Статический анализ импортов

Одно из важных преимуществ ES-модулей заключается в возможности статического анализа.

Parcel способен определить используемые зависимости ещё до выполнения программы.

Пример:

import { formatDate } from "./date.js";

console.log(formatDate());

Сборщик заранее знает:

  • какой файл будет загружен;
  • какие экспортируемые значения используются;
  • какие части кода можно исключить из финальной сборки.

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


Tree Shaking и ES-модули

Что такое Tree Shaking

Tree Shaking — механизм удаления неиспользуемого кода из итоговой сборки.

Рассмотрим модуль:

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

export function subtract(a, b) {
    return a - b;
}

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

Использование:

import { add } from "./math.js";

console.log(add(5, 3));

Parcel обнаружит, что функции subtract и multiply нигде не используются, и удалит их из производственной сборки.


Почему Tree Shaking лучше работает с ESM

ES-модули имеют фиксированную структуру импортов и экспортов:

import { add } from "./math.js";

Зависимость известна заранее.

В CommonJS возможна динамическая загрузка:

const moduleName = "./math";
const math = require(moduleName);

В подобных случаях статический анализ существенно усложняется.


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

Parcel полностью поддерживает динамические импорты стандарта ECMAScript.

button.addEventListener("click", async () => {
    const module = await import("./dialog.js");

    module.showDialog();
});

Автоматическое разделение кода

При использовании динамического импорта Parcel создаёт отдельный бандл.

const editor = await import("./editor.js");

В результате:

  • основной пакет становится меньше;
  • модуль загружается только при необходимости;
  • ускоряется начальная загрузка приложения.

Работа с package.json

Многие библиотеки публикуют одновременно версии CommonJS и ES Modules.

Пример структуры:

{
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs"
}

Здесь:

  • main указывает на CommonJS-версию;
  • module содержит ESM-сборку.

Parcel анализирует эти поля и выбирает наиболее подходящий вариант.


Поле type

В Node.js тип модулей может определяться через поле type.

{
  "type": "module"
}

После этого файлы .js интерпретируются как ES-модули.

Пример:

import { api } from "./api.js";

Если указано:

{
  "type": "commonjs"
}

или поле отсутствует, используется поведение CommonJS.

Parcel учитывает эти настройки при обработке проекта.


Расширения файлов модулей

Для явного указания типа могут использоваться специальные расширения.

ES Modules

app.mjs
export const version = "1.0.0";

CommonJS

app.cjs
module.exports = {
    version: "1.0.0"
};

Parcel корректно обрабатывает оба варианта.


Реэкспорт модулей

ES-модули позволяют создавать промежуточные точки доступа к API.

Файл:

export { add } from "./add.js";
export { subtract } from "./subtract.js";
export { multiply } from "./multiply.js";

Использование:

import { add, multiply } from "./math/index.js";

Parcel анализирует такие цепочки экспортов и включает только реально используемые зависимости.


Импорт JSON

Parcel поддерживает импорт JSON как модулей.

Файл:

{
  "name": "Parcel Demo",
  "version": "1.0.0"
}

Импорт:

import data from "./package-info.json";

console.log(data.name);

Содержимое JSON автоматически становится частью графа зависимостей.


Импорт ресурсов через модульную систему

В Parcel модульная система распространяется не только на JavaScript.

Можно импортировать различные ресурсы:

import "./styles.css";
import logo from "./logo.svg";
import image from "./photo.png";
import config from "./config.json";

Каждый такой импорт рассматривается как зависимость и проходит через соответствующий пайплайн обработки.


Оптимизация библиотек с поддержкой ESM

При создании библиотек рекомендуется публиковать ESM-версии.

Причины:

  • эффективный Tree Shaking;
  • уменьшение размера сборок;
  • более качественный анализ зависимостей;
  • лучшая совместимость с современными инструментами.

Пример:

export function parse() {}
export function validate() {}
export function serialize() {}

Если приложение использует только:

import { parse } from "my-library";

Parcel сможет исключить остальные функции из итогового бандла.


Интероперабельность модульных систем

Parcel выполняет роль слоя совместимости между различными форматами модулей.

Поддерживаются:

Источник Получатель Поддержка
ESM ESM Полная
CommonJS CommonJS Полная
CommonJS ESM Полная
ESM CommonJS Полная

Благодаря этому возможно постепенно переводить крупные проекты с CommonJS на ES Modules без полного переписывания кодовой базы.


Практические рекомендации

Предпочтение ES-модулей

Для нового кода рекомендуется использовать:

import { createApp } from "./app.js";

вместо:

const app = require("./app");

Преимущества:

  • поддержка стандарта ECMAScript;
  • эффективный Tree Shaking;
  • улучшенная оптимизация сборки;
  • совместимость с браузерами.

Использование динамического импорта для тяжёлых модулей

const chartModule = await import("./charts.js");

Подобный подход уменьшает размер начального бандла и ускоряет загрузку страницы.


Избегание динамических require

Нежелательный вариант:

const moduleName = getModuleName();

const module = require(moduleName);

Такой код усложняет анализ зависимостей и снижает эффективность оптимизаций.

Предпочтительнее использовать явные импорты:

import analytics from "./analytics.js";

или динамический импорт ECMAScript:

const analytics = await import("./analytics.js");

Экспорт только необходимых сущностей

Вместо крупного объекта:

export default {
    add,
    subtract,
    multiply,
    divide
};

часто выгоднее использовать именованные экспорты:

export function add() {}
export function subtract() {}
export function multiply() {}
export function divide() {}

Так Parcel получает больше возможностей для удаления неиспользуемого кода и оптимизации итоговой сборки.