Обработка CommonJS, ESM и UMD модулей

Webpack поддерживает несколько систем модулей одновременно. Это необходимо для совместимости со старым кодом, библиотеками npm, браузерными сборками и современными стандартами JavaScript.

Наиболее распространённые форматы:

  • CommonJS
  • ESM (ECMAScript Modules)
  • UMD
  • AMD
  • SystemJS

Основная задача Webpack — привести все типы модулей к единому внутреннему графу зависимостей, после чего выполнить оптимизацию и сформировать итоговые бандлы.


CommonJS

Общая характеристика

CommonJS — модульная система, появившаяся в экосистеме Node.js задолго до стандартизации ES-модулей.

Главные особенности:

  • синхронная загрузка модулей;
  • использование require;
  • экспорт через module.exports или exports;
  • ориентированность на серверную среду.

Пример модуля:

// math.js
function sum(a, b) {
    return a + b;
}

module.exports = {
    sum
};

Импорт:

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

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

Как Webpack обрабатывает CommonJS

Webpack умеет анализировать вызовы require() и строить граф зависимостей.

Пример:

const logger = require('./logger');
const config = require('./config');

Во время сборки Webpack:

  1. анализирует вызовы require;
  2. находит соответствующие файлы;
  3. включает их в граф модулей;
  4. преобразует всё во внутренний runtime.

После сборки код превращается в структуру вида:

(() => {

    const __webpack_modules__ = {

        './src/logger.js': (module) => {
            module.exports = function () {
                console.log('log');
            };
        }

    };

})();

Ограничения статического анализа CommonJS

Webpack эффективно работает только со статически определяемыми путями.

Корректно:

require('./utils');

Проблемно:

require(moduleName);

или:

require('./modules/' + name);

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

Результат:

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

Context-модули

При динамическом require Webpack создаёт специальный context-модуль.

Пример:

const lang = require('./lang/' + locale + '.json');

Webpack может включить:

./lang/en.json
./lang/fr.json
./lang/de.json

Вместо одного файла в бандл попадёт целая директория.


require.resolve

Webpack поддерживает require.resolve.

Пример:

const path = require.resolve('./config');

Webpack вычисляет путь во время сборки.

Это полезно:

  • для плагинов;
  • систем конфигурации;
  • определения путей ресурсов.

module.exports и exports

module.exports

Основной механизм экспорта:

module.exports = function () {
    console.log('hello');
};

exports

Сокращённая ссылка:

exports.sum = sum;
exports.sub = sub;

Эквивалентно:

module.exports.sum = sum;

Ошибка при переназначении exports

Некорректный вариант:

exports = {
    sum
};

Проблема заключается в потере связи с module.exports.

Webpack сохраняет это поведение, поскольку полностью совместим со спецификой CommonJS.


ESM (ECMAScript Modules)

Стандартизированная система модулей

ESM — официальный модульный стандарт JavaScript.

Использует:

  • import
  • export

Пример:

// math.js
export function sum(a, b) {
    return a + b;
}

Импорт:

import { sum } from './math';

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

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

Статическая структура

ESM анализируется ещё до выполнения кода.

Это позволяет:

  • выполнять tree shaking;
  • удалять неиспользуемый код;
  • оптимизировать зависимости;
  • разделять чанки;
  • улучшать минификацию.

Tree Shaking

Одно из важнейших преимуществ ESM.

Модуль:

export function used() {
    console.log('used');
}

export function unused() {
    console.log('unused');
}

Импорт:

import { used } from './module';

Webpack способен удалить unused из production-сборки.

Для CommonJS такая оптимизация значительно сложнее.


Живые привязки (Live Bindings)

ESM использует live bindings.

Экспорт:

export let counter = 0;

export function increment() {
    counter++;
}

Импорт:

import { counter, increment } from './store';

increment();

console.log(counter);

Значение counter обновляется автоматически.


Обработка ESM в Webpack

Webpack распознаёт:

import
export
import()

Пример:

import userService from './services/userService';

После анализа создаётся граф зависимостей.

Webpack сохраняет информацию:

  • какие сущности импортируются;
  • какие экспорты используются;
  • какие модули являются side-effect free.

Side Effects

Для tree shaking важно наличие побочных эффектов.

package.json

{
    "sideEffects": false
}

Это сообщает Webpack:

  • модули можно безопасно удалять;
  • неиспользуемые импорты не нужны в финальном бандле.

Проблема побочных эффектов

Некоторые модули выполняют код при импорте:

import './styles.css';

или:

console.log('init');

Webpack не может удалить такие модули автоматически.


Dynamic Import

ESM поддерживает динамический импорт.

import('./admin')
    .then(module => {
        module.init();
    });

Webpack интерпретирует это как точку разделения кода.

Результат:

  • создаётся отдельный chunk;
  • модуль загружается по требованию;
  • уменьшается initial bundle size.

Асинхронная природа ESM

Dynamic import всегда возвращает Promise:

const module = await import('./feature');

Webpack строит runtime-загрузчик чанков.


Interop между CommonJS и ESM

В реальных проектах часто смешиваются оба формата.


Импорт CommonJS из ESM

CommonJS:

module.exports = {
    hello() {
        console.log('hello');
    }
};

ESM:

import lib from './lib';

lib.hello();

Webpack создаёт совместимый слой interoperability.


Импорт ESM из CommonJS

ESM:

export default function () {
    console.log('hello');
}

CommonJS:

const mod = require('./module');

Результат может содержать:

mod.default

Поскольку ESM использует default export как отдельную сущность.


__esModule

Webpack часто добавляет специальный флаг:

exports.__esModule = true;

Он нужен для определения:

  • является ли модуль ES-модулем;
  • как корректно обрабатывать default export.

Harmony Modules

Внутри Webpack ES-модули исторически назывались Harmony Modules.

В runtime можно встретить:

__webpack_require__.d

или:

__webpack_require__.r

Эти функции помогают:

  • создавать live bindings;
  • маркировать ES-модули;
  • поддерживать interop.

UMD

Универсальный формат модулей

UMD (Universal Module Definition) создавался как способ поддержки:

  • CommonJS;
  • AMD;
  • браузерных глобальных переменных.

Типичный UMD-шаблон:

(function (root, factory) {

    if (typeof exports === 'object' && typeof module === 'object') {
        module.exports = factory();
    }

    else if (typeof define === 'function' && define.amd) {
        define([], factory);
    }

    else {
        root.MyLibrary = factory();
    }

})(this, function () {

    return {
        hello() {
            console.log('hello');
        }
    };

});

Как Webpack распознаёт UMD

Webpack анализирует шаблоны:

  • module.exports
  • define.amd
  • глобальные переменные

После этого модуль включается в граф зависимостей.


output.libraryTarget

Webpack умеет генерировать UMD-бандлы.

Пример:

module.exports = {

    output: {
        filename: 'library.js',
        library: 'MyLibrary',
        libraryTarget: 'umd'
    }

};

Результат:

  • библиотека работает в Node.js;
  • поддерживается AMD;
  • возможна работа через глобальную переменную.

Варианты libraryTarget

commonjs

libraryTarget: 'commonjs'

Экспорт:

module.exports = ...

commonjs2

libraryTarget: 'commonjs2'

Использует полноценный module.exports.


amd

libraryTarget: 'amd'

Формирует AMD-модуль.


umd

libraryTarget: 'umd'

Создаёт универсальный формат.


var

libraryTarget: 'var'

Экспортирует библиотеку как глобальную переменную.


output.module

Webpack поддерживает генерацию настоящих ES-модулей.

Конфигурация:

module.exports = {

    experiments: {
        outputModule: true
    },

    output: {
        module: true
    }

};

Результат:

  • выходной файл использует import/export;
  • бандл становится ESM-совместимым.

experiments.outputModule

Флаг включает экспериментальный режим ESM-output.

Особенности:

  • отсутствует UMD-обёртка;
  • используется нативный module syntax;
  • необходим <script type="module">.

externals и системы модулей

Webpack умеет исключать зависимости из сборки.

Пример:

module.exports = {

    externals: {
        react: 'React'
    }

};

В этом случае:

  • React не попадёт в bundle;
  • библиотека ожидается во внешней среде.

externalsType

Позволяет определить формат внешней зависимости.

Пример:

module.exports = {

    externalsType: 'commonjs'

};

Возможные значения:

  • commonjs
  • module
  • var
  • this
  • window
  • self
  • global

Babel и преобразование модулей

Babel способен преобразовывать ESM в CommonJS.

Пример:

import { sum } from './math';

После трансформации:

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

Проблемы при неправильной настройке Babel

Если Babel преобразует ESM слишком рано:

  • Webpack теряет информацию об импортах;
  • tree shaking перестаёт работать;
  • bundle становится больше.

Правильная настройка:

{
    modules: false
}

webpackMode в dynamic import

Webpack позволяет управлять режимом загрузки.

Пример:

import(
    /* webpackMode: "lazy" */
    './admin'
);

Режимы:

  • lazy
  • eager
  • weak
  • lazy-once

eager

import(
    /* webpackMode: "eager" */
    './module'
);

Модуль включается в основной bundle без отдельного chunk.


lazy

Стандартное поведение.

Создаётся отдельный chunk.


weak

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

Новый chunk не создаётся.


lazy-once

Несколько динамических импортов объединяются в один chunk.


AMD-модули

Webpack поддерживает AMD.

Пример:

define(['dep'], function(dep) {

    return {
        hello() {}
    };

});

Однако в современных проектах AMD практически вытеснен ESM.


SystemJS

Webpack может взаимодействовать с SystemJS.

Пример output:

output: {
    libraryTarget: 'system'
}

Используется редко, в основном:

  • в legacy-проектах;
  • микрофронтендах;
  • enterprise-системах.

package.json и тип модулей

type: module

{
    "type": "module"
}

Node.js интерпретирует .js как ESM.


type: commonjs

{
    "type": "commonjs"
}

Файлы считаются CommonJS.


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

.mjs

ES-модуль.

.cjs

CommonJS-модуль.

.js

Зависит от type в package.json.


mainFields

Webpack выбирает entry-файл пакета через resolve.mainFields.

Пример:

resolve: {
    mainFields: ['browser', 'module', 'main']
}

module field

Многие библиотеки публикуют:

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

Webpack предпочитает ESM-версию.

Причины:

  • лучший tree shaking;
  • более качественная оптимизация;
  • уменьшение bundle size.

Conditional Exports

Современные пакеты используют exports map.

{
    "exports": {
        "import": "./esm/index.js",
        "require": "./cjs/index.js"
    }
}

Webpack выбирает нужный вариант автоматически.


dual package hazard

Проблема возникает, когда пакет одновременно содержит:

  • CommonJS;
  • ESM.

Возможна ситуация двойной загрузки:

require('pkg')

и:

import 'pkg'

Могут создать два независимых экземпляра библиотеки.


Оптимизация модульных форматов

Для production-сборок предпочтительнее:

  • ESM;
  • sideEffects-free пакеты;
  • статические импорты.

Наименее оптимальны:

  • динамические require;
  • deeply dynamic imports;
  • legacy UMD;
  • runtime-generated paths.

Runtime Webpack и модульная система

Webpack генерирует собственный runtime.

Основные задачи runtime:

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

Кэширование модулей

Webpack использует внутренний cache:

var __webpack_module_cache__ = {};

После первого выполнения модуль сохраняется.

Повторный импорт использует кэшированный экземпляр.


Выполнение модуля один раз

Поведение совпадает с CommonJS:

console.log('init');

Будет выполнено только при первой загрузке модуля.


Namespace Object

ESM создаёт namespace object.

Пример:

import * as utils from './utils';

Webpack генерирует объект с live bindings.


Default Export

Пример:

export default class UserService {}

Импорт:

import UserService from './UserService';

Webpack хранит default export отдельно от named exports.


Named Exports

export const API_URL = '/api';
export function request() {}

Импорт:

import { API_URL, request } from './config';

Webpack может анализировать использование каждого экспорта отдельно.


Re-export

Пример:

export * from './math';

или:

export { sum } from './math';

Webpack объединяет цепочки экспортов в единый граф зависимостей.


Circular Dependencies

Webpack поддерживает циклические зависимости.

Пример:

// a.js
import { b } from './b';

// b.js
import { a } from './a';

Но возможны:

  • partially initialized exports;
  • undefined-значения;
  • ошибки порядка инициализации.

Отличие циклов в CommonJS и ESM

CommonJS

Экспортируется текущее состояние объекта.

ESM

Используются live bindings.

Поведение отличается при раннем обращении к значениям.


Production-режим и модули

В production Webpack активирует:

  • tree shaking;
  • scope hoisting;
  • minimization;
  • export mangling.

Все эти оптимизации особенно эффективны именно для ESM.


Scope Hoisting

Webpack объединяет модули в единый scope.

Технология называется:

Module Concatenation

Работает лучше со статическими ES-модулями.


Почему ESM предпочтительнее

ES-модули дают Webpack максимум информации:

  • статические зависимости;
  • точные импорты;
  • известные экспорты;
  • отсутствие runtime-динамики.

Это позволяет:

  • уменьшать bundle size;
  • ускорять загрузку;
  • улучшать tree shaking;
  • оптимизировать выполнение кода.