Несколько конфигурационных файлов в одном проекте

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

Наиболее распространённый подход — разделение конфигурации на базовую и окруженческие части.

Структура проекта:

webpack/
  webpack.common.js
  webpack.dev.js
  webpack.prod.js
webpack.config.js

Базовый файл содержит общие настройки, которые не зависят от окружения: входные точки, правила обработки модулей, базовые плагины.

Базовая конфигурация как фундамент

Общий конфиг обычно включает:

  • entry
  • output
  • module.rules
  • базовые plugins
  • resolve aliases

Пример:

// webpack.common.js
const path = require('path');

module.exports = {
  entry: './src/index.js',

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

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

  resolve: {
    extensions: ['.js', '.json'],
    alias: {
      '@': path.resolve(__dirname, '../src')
    }
  }
};

Этот файл не должен содержать логики, зависящей от режима сборки. Его задача — описать структуру проекта.

Конфигурация разработки

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

Ключевые особенности:

  • source map для отладки
  • dev server
  • hot module replacement
  • упрощённая обработка стилей
// webpack.dev.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common');

module.exports = merge(common, {
  mode: 'development',

  devtool: 'eval-source-map',

  devServer: {
    static: './dist',
    hot: true,
    port: 3000,
    open: true
  },

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

Главная цель этой конфигурации — скорость итераций, а не оптимизация результата.

Конфигурация продакшн-сборки

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

Типичные задачи:

  • минификация кода
  • извлечение CSS
  • оптимизация чанков
  • tree shaking
  • удаление dev-инструментов
// webpack.prod.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = merge(common, {
  mode: 'production',

  devtool: 'source-map',

  output: {
    filename: '[name].[contenthash].js'
  },

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

  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css'
    })
  ],

  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
});

Здесь появляется хеширование файлов, позволяющее эффективно использовать кэширование в браузере.

Объединение конфигураций через webpack-merge

Пакет webpack-merge используется для корректного объединения объектов конфигурации.

Особенность Webpack в том, что простое объединение через spread-операторы может привести к перезаписи сложных структур, таких как module.rules или plugins. webpack-merge решает эту проблему, глубоко объединяя структуры.

Установка:

npm install webpack-merge

Принцип работы:

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

const result = merge(configA, configB);

При конфликте правил loaders не затираются, а дополняются.

Выбор конфигурации через CLI

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

webpack --config webpack/webpack.prod.js

Это ключевой механизм переключения между режимами без изменения кода.

В package.json обычно описываются скрипты:

{
  "scripts": {
    "dev": "webpack serve --config webpack/webpack.dev.js",
    "build": "webpack --config webpack/webpack.prod.js"
  }
}

Такой подход делает процесс сборки предсказуемым и изолированным.

Функциональные конфигурации

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

module.exports = (env, argv) => {
  const isProd = argv.mode === 'production';

  return {
    mode: isProd ? 'production' : 'development',

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

    output: {
      filename: isProd ? '[name].[contenthash].js' : '[name].js'
    }
  };
};

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

Использование переменных окружения

Передача параметров через CLI:

webpack --env target=legacy

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

module.exports = (env) => {
  return {
    entry: env.target === 'legacy'
      ? './src/legacy.js'
      : './src/index.js'
  };
};

Это позволяет создавать вариативные сборки без увеличения числа файлов.

Множественные конфигурации как массив

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

module.exports = [
  {
    name: 'client',
    entry: './src/client.js',
    output: {
      filename: 'client.bundle.js'
    }
  },
  {
    name: 'admin',
    entry: './src/admin.js',
    output: {
      filename: 'admin.bundle.js'
    }
  }
];

Это используется для:

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

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

Разделение по функциональным зонам проекта

В крупных проектах конфигурации часто делятся не только по окружениям, но и по назначению:

  • frontend сборка
  • backend SSR сборка
  • библиотечная сборка
  • тестовая сборка

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

webpack/
  webpack.client.js
  webpack.server.js
  webpack.lib.js

SSR конфигурация:

module.exports = {
  target: 'node',

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

  output: {
    filename: 'server.js',
    libraryTarget: 'commonjs2'
  }
};

Переиспользование конфигураций через фабрики

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

const createConfig = ({ mode, target }) => ({
  mode,

  target,

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

  output: {
    filename: target === 'node' ? 'server.js' : 'client.js'
  }
});

module.exports = [
  createConfig({ mode: 'production', target: 'web' }),
  createConfig({ mode: 'production', target: 'node' })
];

Такой подход делает конфигурации предсказуемыми и легко расширяемыми.

Конфигурации и зависимости сборки

При разделении конфигураций важно учитывать:

  • общие loaders должны быть синхронизированы
  • плагины могут конфликтовать при одновременной сборке
  • output paths должны быть изолированы
  • кеширование требует уникальных имен файлов

Ошибки в этих областях приводят к перезаписи артефактов или некорректной сборке.

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

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

Однако это увеличивает нагрузку на CPU и память, особенно при использовании TypeScript, Babel и сложных loader цепочек.

Оптимизация включает:

  • разделение тяжелых сборок
  • использование кеширования
  • ограничение числа параллельных задач через thread-loader

Организация конфигураций в монорепозиториях

В монорепозиториях конфигурации часто привязаны к пакетам:

packages/
  app/
    webpack.config.js
  admin/
    webpack.config.js
  shared/

Общие настройки выносятся в корневой пакет:

webpack.base.js

И импортируются локально:

const base = require('../. ./webpack.base');

module.exports = {
  ...base,
  entry: './src/index.js'
};

Изоляция окружений и предсказуемость сборки

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

  • независимости сборок
  • повторяемости результата
  • упрощённой отладки
  • масштабируемости проекта

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