Externals: исключение зависимостей из бандла

Параметр externals в Webpack используется для исключения определённых зависимостей из итогового бандла. Вместо включения кода зависимости внутрь сборки Webpack оставляет обращение к внешнему источнику.

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

  • использование библиотек через CDN;
  • создание библиотек и SDK;
  • уменьшение размера бандла;
  • исключение серверных модулей Node.js;
  • предотвращение дублирования зависимостей;
  • разделение ответственности между приложениями.

Без externals Webpack по умолчанию включает импортируемые зависимости в бандл.

Пример:

import React from 'react';

Обычная сборка встроит React внутрь бандла. При использовании externals React останется внешней зависимостью.


Базовый синтаксис

module.exports = {
    externals: {
        react: 'React'
    }
};

Здесь:

  • react — имя модуля в import или require;
  • React — глобальная переменная, существующая во внешней среде.

Webpack заменит импорт на обращение к глобальному объекту:

const React = window.React;

или аналогичный механизм в зависимости от платформы.


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

Один из самых популярных вариантов — подключение библиотек через CDN.

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

module.exports = {
    externals: {
        jquery: 'jQuery'
    }
};

HTML

<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
<script src="bundle.js"></script>

Код приложения

import $ from 'jquery';

$('.menu').addClass('active');

Внутри bundle.js jQuery отсутствует. Предполагается, что библиотека уже загружена до выполнения бандла.


Экономия размера бандла

Крупные библиотеки значительно увеличивают размер итоговой сборки:

  • React;
  • Vue;
  • Angular;
  • lodash;
  • moment;
  • chart.js.

При подключении через CDN:

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

Пример:

externals: {
    react: 'React',
    'react-dom': 'ReactDOM'
}

Externals и библиотеки

При разработке библиотеки включение зависимостей внутрь бандла часто считается ошибкой.

Например, создаётся UI-библиотека для React.

Неправильный подход:

my-library
 └── React внутри бандла

Если приложение пользователя уже использует React, возникнут:

  • дублирование кода;
  • увеличение размера;
  • конфликт версий;
  • проблемы с hooks;
  • ошибки контекста React.

Правильный подход — вынести React во внешнюю зависимость.

module.exports = {
    externals: {
        react: 'react'
    }
};

Теперь библиотека требует React от конечного проекта.


Разница между dependency и peerDependency

Обычно externals используется вместе с peerDependencies.

package.json

{
  "peerDependencies": {
    "react": "^18.0.0"
  }
}

webpack.config.js

module.exports = {
    externals: {
        react: 'react'
    }
};

Смысл:

  • peerDependencies сообщает npm о внешней зависимости;
  • externals сообщает Webpack не включать зависимость в бандл.

Externals в CommonJS

Для Node.js-проектов часто используется CommonJS-формат.

module.exports = {
    target: 'node',

    externals: {
        express: 'commonjs express'
    }
};

Webpack не встроит Express в бандл.

Вместо этого останется:

require('express');

Форматы externals

Webpack поддерживает несколько типов внешних зависимостей.

Глобальная переменная

externals: {
    lodash: '_'
}

CommonJS

externals: {
    lodash: 'commonjs lodash'
}

CommonJS2

externals: {
    lodash: 'commonjs2 lodash'
}

AMD

externals: {
    lodash: 'amd lodash'
}

UMD

externals: {
    lodash: 'umd lodash'
}

SystemJS

externals: {
    lodash: 'system lodash'
}

Различие между commonjs и commonjs2

commonjs

module.exports = {
    externals: {
        lib: 'commonjs lib'
    }
};

Генерируется:

exports["lib"] = require("lib");

commonjs2

module.exports = {
    externals: {
        lib: 'commonjs2 lib'
    }
};

Генерируется:

module.exports = require("lib");

Разница особенно важна при разработке библиотек.


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

Можно задавать несколько внешних зависимостей.

module.exports = {
    externals: {
        react: 'React',
        jquery: 'jQuery',
        vue: 'Vue'
    }
};

Использование массива форматов

module.exports = {
    externals: {
        react: [
            'React',
            'react'
        ]
    }
};

Используется редко и преимущественно в сложных multi-target сборках.


Использование функции

externals может быть функцией.

module.exports = {
    externals: [
        ({ request }, callback) => {
            if (/^@app\//.test(request)) {
                return callback(null, 'commonjs ' + request);
            }

            callback();
        }
    ]
};

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

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

Исключение всех node_modules

Популярный серверный сценарий.

Установка

npm install webpack-node-externals --save-dev

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

const nodeExternals = require('webpack-node-externals');

module.exports = {
    target: 'node',

    externals: [nodeExternals()]
};

Теперь:

  • все node_modules исключаются из бандла;
  • остаются обычные require;
  • сборка становится компактнее.

Почему это важно для Node.js

Node.js уже умеет загружать зависимости через файловую систему.

Встраивание модулей:

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

Поэтому серверные сборки почти всегда используют externals.


Native-модули и externals

Некоторые пакеты не могут корректно работать после бандлинга:

  • bcrypt;
  • sqlite3;
  • sharp;
  • node-sass.

Их рекомендуется исключать:

externals: {
    sharp: 'commonjs sharp'
}

ExternalsType

Webpack 5 ввёл параметр externalsType.

Пример

module.exports = {
    externalsType: 'commonjs',

    externals: {
        express: 'express'
    }
};

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


Поддерживаемые externalsType

Основные варианты:

var
module
assign
this
window
self
global
commonjs
commonjs2
commonjs-module
amd
umd
system
promise
import
script
node-commonjs

externalsType: ‘module’

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

module.exports = {
    experiments: {
        outputModule: true
    },

    externalsType: 'module',

    externals: {
        lodash: 'lodash'
    }
};

Webpack создаст внешний ESM-импорт:

import lodash from 'lodash';

externalsType: ‘script’

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

module.exports = {
    externalsType: 'script',

    externals: {
        jquery: [
            'https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js',
            '$'
        ]
    }
};

Webpack:

  1. загрузит внешний скрипт;
  2. дождётся инициализации;
  3. использует глобальную переменную.

Различие между alias и externals

resolve.alias и externals решают совершенно разные задачи.

alias

Меняет путь модуля.

resolve: {
    alias: {
        '@': path.resolve(__dirname, 'src')
    }
}

externals

Исключает модуль из бандла.

externals: {
    react: 'React'
}

Различие между splitChunks и externals

splitChunks

Разделяет код на чанки.

Зависимость остаётся частью сборки.


externals

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


Проверка результата

После сборки можно проверить содержимое бандла.

Если dependency встроена:

React source code...

Если используется externals:

module.exports = React;

или:

require("react")

Проблемы порядка загрузки

При использовании CDN порядок подключения критически важен.

Неправильно:

<script src="bundle.js"></script>
<script src="react.js"></script>

Ошибка:

React is not defined

Правильно:

<script src="react.js"></script>
<script src="bundle.js"></script>

Конфликт версий

При внешних зависимостях особенно важно контролировать версии.

Например:

Library built for React 18
App uses React 16

Возможны:

  • несовместимость hooks;
  • ошибки hydration;
  • проблемы context API;
  • падения runtime.

Externals и Module Federation

externals и Module Federation решают похожие задачи, но разными способами.

externals

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

Module Federation

  • динамический runtime;
  • shared dependencies;
  • version negotiation;
  • удалённая загрузка модулей.

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

Можно исключать зависимости по шаблону.

module.exports = {
    externals: [
        /^@company\//
    ]
};

Все пакеты:

@company/ui
@company/core
@company/utils

станут внешними.


Комбинирование правил

module.exports = {
    externals: [
        {
            react: 'React'
        },

        /^@company\//,

        function ({ request }, callback) {
            if (request.includes('legacy')) {
                return callback(null, 'commonjs ' + request);
            }

            callback();
        }
    ]
};

Webpack обработает правила последовательно.


externalsPresets

Webpack 5 добавил externalsPresets.

Node.js

module.exports = {
    externalsPresets: {
        node: true
    }
};

Webpack автоматически корректно обрабатывает встроенные Node-модули:

  • fs;
  • path;
  • os;
  • crypto;
  • stream.

Исключение встроенных модулей Node.js

Без externals:

import fs from 'fs';

Webpack попытается обработать модуль.

С externals:

externals: {
    fs: 'commonjs fs'
}

останется:

require('fs')

Ошибки при использовании externals

Отсутствие глобальной переменной

Uncaught ReferenceError

Причина:

  • библиотека не подключена;
  • неправильное имя глобального объекта.

Несовпадение имени

externals: {
    react: 'React'
}

Но CDN экспортирует:

window.react

Возникнет ошибка.


Использование externals в SPA без CDN

Если dependency исключена, но нигде не подключена, приложение не запустится.


Случайное исключение внутреннего модуля

externals: [
    /^src\//
]

Внутренние модули перестанут попадать в сборку.


Практический пример React-библиотеки

webpack.config.js

module.exports = {
    mode: 'production',

    entry: './src/index.js',

    output: {
        filename: 'index.js',
        library: {
            type: 'umd'
        }
    },

    externals: {
        react: {
            commonjs: 'react',
            commonjs2: 'react',
            amd: 'react',
            root: 'React'
        },

        'react-dom': {
            commonjs: 'react-dom',
            commonjs2: 'react-dom',
            amd: 'react-dom',
            root: 'ReactDOM'
        }
    }
};

Объект с несколькими форматами

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

externals: {
    lodash: {
        commonjs: 'lodash',
        commonjs2: 'lodash',
        amd: 'lodash',
        root: '_'
    }
}

Это особенно важно для UMD-библиотек.


Влияние на tree shaking

externals не участвует в tree shaking.

Webpack не анализирует внешний модуль, поскольку он отсутствует в графе зависимостей.

Следствия:

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

Externals и source maps

Если библиотека исключена из бандла:

  • её source maps не включаются;
  • дебаг зависит от CDN;
  • стек ошибок может быть менее информативным.

Когда externals использовать нежелательно

Не рекомендуется:

  • для небольших библиотек;
  • при офлайн-режиме;
  • в полностью автономных приложениях;
  • при сложном контроле версий;
  • в проектах без CDN;
  • когда требуется гарантированная изоляция окружения.

Типичные сценарии применения

Frontend SPA

externals: {
    react: 'React',
    vue: 'Vue'
}

Node.js API

externals: [nodeExternals()]

UI-kit

externals: {
    react: 'react',
    'react-dom': 'react-dom'
}

Microfrontend

externals: {
    react: 'React'
}

Архитектурное значение externals

externals фактически изменяет границу ответственности системы.

Без externals:

Приложение полностью владеет зависимостями

С externals:

Часть зависимостей передаётся внешнему окружению

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

  • деплой;
  • кэширование;
  • обновление библиотек;
  • совместимость;
  • безопасность;
  • воспроизводимость сборок.

Поведение Webpack при обработке externals

Во время построения dependency graph Webpack:

  1. обнаруживает импорт;
  2. проверяет правила externals;
  3. прекращает обработку совпавшего модуля;
  4. не анализирует зависимости пакета;
  5. генерирует внешний reference.

Именно поэтому externals существенно ускоряет сборку крупных проектов.