Экспорт массива конфигураций

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

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


Базовый экспорт массива

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

// webpack.config.js

module.exports = [
    {
        name: 'client',

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

        output: {
            filename: 'client.bundle.js'
        }
    },

    {
        name: 'server',

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

        output: {
            filename: 'server.bundle.js'
        }
    }
];

Webpack воспринимает каждый объект массива как самостоятельную конфигурацию.

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

npx webpack

Webpack последовательно обработает обе конфигурации.


Назначение поля name

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

{
    name: 'client'
}

Поле name:

  • упрощает отладку;
  • делает вывод сборки понятнее;
  • позволяет запускать конкретную конфигурацию через CLI;
  • используется некоторыми плагинами и инструментами анализа.

Пример:

npx webpack --config-name client

Webpack выполнит только конфигурацию с именем client.


Разделение клиентской и серверной сборки

Один из наиболее распространённых сценариев — отдельная сборка frontend и backend.

const path = require('path');

module.exports = [
    {
        name: 'frontend',

        target: 'web',

        mode: 'production',

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

        output: {
            path: path.resolve(__dirname, 'dist/frontend'),
            filename: 'bundle.js'
        }
    },

    {
        name: 'backend',

        target: 'node',

        mode: 'production',

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

        output: {
            path: path.resolve(__dirname, 'dist/backend'),
            filename: 'server.js'
        }
    }
];

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

  • target: 'web' собирает код для браузера;
  • target: 'node' адаптирует сборку под Node.js;
  • каждая сборка имеет собственную папку output.path;
  • можно использовать разные загрузчики и плагины.

Независимые настройки для каждой конфигурации

Каждый объект внутри массива полностью автономен.

Пример:

module.exports = [
    {
        name: 'modern',

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

        output: {
            filename: 'modern.js'
        },

        experiments: {
            outputModule: true
        }
    },

    {
        name: 'legacy',

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

        output: {
            filename: 'legacy.js'
        },

        module: {
            rules: [
                {
                    test: /\.js$/,
                    exclude: /node_modules/,
                    use: 'babel-loader'
                }
            ]
        }
    }
];

В данном случае:

  • современная сборка использует экспериментальные возможности Webpack;
  • legacy-сборка транспилируется через Babel;
  • обе сборки используют одинаковую точку входа;
  • результаты отличаются по возможностям и совместимости.

Экспорт массива для разных окружений

Webpack-массив часто применяется при генерации нескольких версий приложения:

module.exports = [
    {
        name: 'development',

        mode: 'development',

        devtool: 'eval-source-map',

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

        output: {
            filename: 'app.dev.js'
        }
    },

    {
        name: 'production',

        mode: 'production',

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

        output: {
            filename: 'app.prod.js'
        }
    }
];

Такой подход позволяет:

  • одновременно генерировать dev- и prod-версии;
  • сравнивать размеры сборок;
  • тестировать оптимизацию;
  • использовать разные стратегии минификации.

Общая конфигурация и переиспользование кода

Повторение одинаковых настроек быстро приводит к дублированию. Обычно создаётся базовая конфигурация.

const path = require('path');

const commonConfig = {
    entry: './src/index.js',

    output: {
        path: path.resolve(__dirname, 'dist')
    },

    module: {
        rules: [
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            }
        ]
    }
};

module.exports = [
    {
        ...commonConfig,

        name: 'development',

        mode: 'development',

        output: {
            ...commonConfig.output,
            filename: 'dev.bundle.js'
        }
    },

    {
        ...commonConfig,

        name: 'production',

        mode: 'production',

        output: {
            ...commonConfig.output,
            filename: 'prod.bundle.js'
        }
    }
];

Подобная структура:

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

Использование webpack-merge

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

npm install webpack-merge --save-dev

Пример:

const { merge } = require('webpack-merge');

const common = {
    entry: './src/index.js'
};

const development = merge(common, {
    mode: 'development',

    devtool: 'source-map',

    output: {
        filename: 'dev.js'
    }
});

const production = merge(common, {
    mode: 'production',

    output: {
        filename: 'prod.js'
    }
});

module.exports = [
    development,
    production
];

webpack-merge корректно объединяет:

  • объекты;
  • массивы;
  • правила loaders;
  • плагины;
  • вложенные настройки.

Параллельная обработка конфигураций

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

При большом количестве сборок важно учитывать:

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

Крупные проекты нередко разбивают конфигурации на отдельные команды вместо единого массива.


Зависимости между конфигурациями

Webpack поддерживает механизм зависимостей между сборками через dependencies.

module.exports = [
    {
        name: 'shared',

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

        output: {
            filename: 'shared.js'
        }
    },

    {
        name: 'app',

        dependencies: ['shared'],

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

        output: {
            filename: 'app.js'
        }
    }
];

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

  • конфигурация shared будет выполнена первой;
  • app начнёт сборку только после завершения shared;
  • удобно организовывать цепочки сборок.

Использование разных плагинов

Каждая конфигурация может иметь собственный набор плагинов.

const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = [
    {
        name: 'client',

        plugins: [
            new HtmlWebpackPlugin({
                template: './public/index.html'
            })
        ]
    },

    {
        name: 'styles',

        plugins: [
            new MiniCssExtractPlugin({
                filename: 'styles.css'
            })
        ]
    }
];

Плагины не разделяются автоматически между конфигурациями.

Каждый объект создаёт собственный экземпляр инфраструктуры Webpack:

  • отдельный compiler;
  • собственные hooks;
  • независимые assets;
  • собственный pipeline обработки.

Раздельная генерация source maps

Массив конфигураций позволяет гибко управлять source maps.

module.exports = [
    {
        name: 'debug',

        devtool: 'source-map'
    },

    {
        name: 'fast',

        devtool: false
    }
];

Это удобно при:

  • сравнении производительности;
  • анализе размера бандла;
  • CI/CD-сборках;
  • создании debug-версий.

Комбинирование CommonJS и ES-модулей

Разные конфигурации могут экспортировать различные форматы библиотек.

module.exports = [
    {
        name: 'commonjs',

        output: {
            filename: 'library.cjs.js',
            libraryTarget: 'commonjs2'
        }
    },

    {
        name: 'esm',

        experiments: {
            outputModule: true
        },

        output: {
            filename: 'library.esm.js',
            module: true
        }
    }
];

Такой подход часто используется при публикации npm-пакетов.


Конфигурации для нескольких точек развёртывания

Массив конфигураций подходит для создания нескольких версий приложения:

module.exports = [
    {
        name: 'desktop',

        output: {
            filename: 'desktop.js'
        }
    },

    {
        name: 'mobile',

        output: {
            filename: 'mobile.js'
        }
    }
];

Различия могут касаться:

  • polyfills;
  • CSS;
  • изображений;
  • оптимизации;
  • lazy loading;
  • разделения чанков.

Экспорт массива через функцию

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

module.exports = (env) => {
    const isProduction = env.production;

    return [
        {
            name: 'client',

            mode: isProduction
                ? 'production'
                : 'development'
        },

        {
            name: 'server',

            target: 'node',

            mode: isProduction
                ? 'production'
                : 'development'
        }
    ];
};

Запуск:

npx webpack --env production=true

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

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

Ошибки при использовании массива конфигураций

Конфликт output.path

Ошибка:

output: {
    path: path.resolve(__dirname, 'dist')
}

Если несколько сборок пишут одинаковые файлы в одну директорию, возможно:

  • перезаписывание assets;
  • удаление файлов;
  • конфликт manifest;
  • повреждение сборки.

Лучше использовать отдельные папки:

dist/client
dist/server
dist/legacy

Конфликт имён файлов

Проблема:

filename: 'bundle.js'

во всех конфигурациях.

Результат:

  • последняя сборка перезапишет предыдущую.

Правильный вариант:

filename: '[name].[contenthash].js'

или:

filename: 'client.bundle.js'
filename: 'server.bundle.js'

Дублирование тяжёлых плагинов

Некоторые плагины потребляют много ресурсов:

  • TerserPlugin;
  • ImageMinimizerPlugin;
  • ForkTsCheckerWebpackPlugin.

Если они присутствуют во всех конфигурациях массива, время сборки может резко увеличиться.


Организация структуры файлов

В крупных проектах конфигурации обычно разделяются.

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

webpack/
├── common.js
├── client.js
├── server.js
├── production.js
└── development.js

Главный файл:

const client = require('./webpack/client');
const server = require('./webpack/server');

module.exports = [
    client,
    server
];

Такой подход:

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

Использование CLI с массивом конфигураций

Webpack CLI поддерживает выбор отдельных конфигураций.

Список имён:

npx webpack --config-name client

Несколько конфигураций:

npx webpack --config-name client --config-name server

Это особенно удобно:

  • в CI/CD;
  • при раздельной сборке frontend/backend;
  • в monorepo;
  • при частичной пересборке проекта.

Поведение watch-режима

При использовании:

npx webpack --watch

Webpack отслеживает изменения для всех конфигураций массива.

Каждый compiler:

  • имеет собственный watcher;
  • отдельно отслеживает зависимости;
  • независимо инициирует пересборку.

При большом количестве конфигураций это может заметно увеличить нагрузку на файловую систему.


MultiCompiler внутри Webpack

При экспорте массива Webpack создаёт внутренний объект MultiCompiler.

Схема работы:

Array configs
      ↓
MultiCompiler
      ↓
Compiler #1
Compiler #2
Compiler #3

Каждый compiler:

  • имеет собственный lifecycle;
  • управляет отдельным dependency graph;
  • выполняет собственную оптимизацию;
  • генерирует собственные assets.

Изоляция конфигураций — важная особенность массива.


Когда массив конфигураций особенно полезен

Экспорт массива конфигураций применяется в следующих случаях:

  • клиентская и серверная сборка;
  • modern/legacy bundles;
  • библиотеки с несколькими форматами;
  • monorepo;
  • мультибрендовые приложения;
  • white-label системы;
  • генерация debug/prod-сборок;
  • SSR-инфраструктура;
  • Electron-приложения;
  • микрофронтенды;
  • отдельная сборка workers;
  • создание независимых runtime-бандлов.