Олег Марков
Конфигурация Webpack webpack-config - практическое руководство
Введение
Webpack часто называют сборщиком модулей. На практике это означает, что вы описываете в конфигурации, как ваш проект нужно собрать, а Webpack по этим правилам превращает исходный код, стили и изображения в оптимизированный бандл для браузера.
Давайте разберемся, как устроен webpack-config, что в нем обязательно должно быть и как шаг за шагом превратить минимальный конфиг в гибкую систему, которую удобно поддерживать в реальном проекте.
Что такое webpack.config.js и как он подключается
Webpack по умолчанию ищет файл конфигурации с названием:
- webpack.config.js
- webpack.config.cjs
- webpack.config.mjs
- или файл, указанный через параметр командной строки
--config
Если вы запускаете:
npx webpack
и в корне проекта лежит webpack.config.js, Webpack автоматически возьмет настройки из этого файла.
Типичный минимальный конфиг выглядит так:
// webpack.config.js
// Модуль path нужен для корректной работы с путями к файлам
const path = require('path');
/** @type {import('webpack').Configuration} */
module.exports = {
// Точка входа приложения
entry: './src/index.js',
// Настройки вывода собранных файлов
output: {
// Абсолютный путь к папке для сборки
path: path.resolve(__dirname, 'dist'),
// Имя результирующего файла
filename: 'bundle.js'
},
// Режим сборки - влияет на оптимизацию и служебный код
mode: 'development'
};
Смотрите, здесь вы уже видите три ключевых элемента: entry, output и mode. На них и построена вся логика сборки.
Поле entry – откуда начинается сборка
Один входной файл
Поле entry говорит Webpack, с какого файла начинать анализ зависимостей.
module.exports = {
entry: './src/index.js',
// ...
};
Здесь Webpack возьмет ./src/index.js как корень графа зависимостей, найдет все import и require, и включит их в итоговый бандл.
Несколько точек входа
В реальных проектах часто нужны несколько независимых бандлов, например для разных страниц.
module.exports = {
entry: {
main: './src/index.js', // Главный бандл
admin: './src/admin.js' // Бандл для админки
},
output: {
// [name] будет заменен на название ключа из entry
filename: '[name].bundle.js'
},
mode: 'development'
};
Теперь Webpack соберет два файла:
- main.bundle.js
- admin.bundle.js
Если вы посмотрите на эту схему, то становится понятно, что объект в entry фактически задает "имена" будущих бандлов.
Расширенные варианты entry
Можно использовать массивы, если вам нужно подключить, например, полифилы:
module.exports = {
entry: {
main: [
'./src/polyfills.js', // Полифилы
'./src/index.js' // Основной код
]
},
// ...
};
Webpack соберет один бандл, в который сначала попадут polyfills, а потом основной код.
Поле output – куда и как складывать результат
Базовые настройки output
output описывает, где окажутся результаты сборки.
const path = require('path');
module.exports = {
// ...
output: {
path: path.resolve(__dirname, 'dist'), // Папка для сборки
filename: 'bundle.js' // Имя файла
}
};
Важно, чтобы path был абсолютным путем, поэтому чаще всего используют path.resolve.
Шаблоны имен файлов
Webpack поддерживает плейсхолдеры:
[name]– имя точки входа[contenthash]– хэш содержимого файла (для кэширования)[chunkhash]– хэш чанка[id]– идентификатор чанка
Давайте разберемся на примере:
module.exports = {
entry: {
main: './src/index.js',
admin: './src/admin.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
// Для каждого entry будет свой файл
filename: '[name].[contenthash].js',
clean: true // Очищает папку dist перед каждой сборкой
},
mode: 'production'
};
Как видите, Webpack создаст файлы с уникальными именами вида:
- main.2f3a1c9d.js
- admin.45aa8f01.js
При изменении содержимого меняется и хэш. Это удобно для кеширования в браузере.
Дополнительные поля output
Полезные настройки:
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
publicPath: '/',
// Здесь вы задаете базовый путь, по которому будут доступны файлы на сервере
assetModuleFilename: 'assets/[hash][ext][query]'
// Шаблон для файлов, обработанных через asset-модули
}
publicPath особенно важен, если у вас SPA с history API или файлы лежат не в корне домена.
Поле mode – development, production и none
mode переключает набор встроенных оптимизаций и дефолтных настроек.
module.exports = {
// ...
mode: 'development'
};
Основные режимы:
- development
- включены удобные имена модулей
- не минифицируются файлы
- включен полезный сервисный код для отладки
- production
- включена минификация
- удаляется мертвый код (tree shaking)
- включены различные оптимизации для размера и скорости
- none
- вообще без дополнительных оптимизаций
Часто удобно использовать разные конфиги для dev и prod. Чуть позже я покажу, как это организовать.
module.rules – работа с файлами и загрузчиками (loaders)
Webpack сам по себе "понимает" только JavaScript и JSON. Все остальное (CSS, картинки, TypeScript, JSX) обрабатывается через загрузчики.
Общая структура rules
module.exports = {
// ...
module: {
rules: [
{
test: /\.js$/, // Условие - какие файлы обрабатывать
exclude: /node_modules/, // Исключения
use: 'babel-loader' // Какой загрузчик применять
}
]
}
};
Смотрите, rules – это массив объектов. Каждый объект описывает:
- какие файлы нужно поймать (test, include, exclude)
- какими лоадерами их обрабатывать (
use)
Пример – работа с JavaScript и Babel
Если вам нужно поддерживать старые браузеры, вы подключаете Babel.
module.exports = {
// ...
module: {
rules: [
{
test: /\.m?js$/, // Обрабатываем .js и .mjs
exclude: /node_modules/, // Не трогаем зависимости
use: {
loader: 'babel-loader', // Лоадер
options: {
// Здесь вы можете задать пресеты
presets: [
['@babel/preset-env', {
// Настройка целевых браузеров
targets: '> 0.25%, not dead'
}]
]
}
}
}
]
}
};
Комментариями вы можете для себя пояснить, почему выбраны те или иные настройки, чтобы другим было легче понять конфигурацию.
Пример – CSS, style-loader и css-loader
CSS обрабатывается цепочкой загрузчиков: один читает файлы, другой внедряет их в DOM.
module.exports = {
// ...
module: {
rules: [
{
test: /\.css$/, // Все файлы .css
use: [
'style-loader', // Встраивает стили в DOM через тег <style>
'css-loader' // Позволяет импортировать CSS в JS
]
}
]
}
};
Важно, что порядок use идет справа налево: сначала css-loader, затем style-loader.
Пример – SASS/SCSS
module.exports = {
// ...
module: {
rules: [
{
test: /\.(scss|sass)$/, // Файлы .scss и .sass
use: [
'style-loader', // Встраиваем стили в DOM
'css-loader', // Интерпретируем @import и url()
'sass-loader' // Компилируем SCSS в CSS
]
}
]
}
};
Здесь я выстраиваю цепочку так, чтобы на вход в sass-loader попадал исходный SCSS код, а на выходе в style-loader – уже готовый CSS.
Пример – изображения и asset-модули
C Webpack 5 появились встроенные asset-модули, и в большинстве случаев можно не использовать file-loader и url-loader.
module.exports = {
// ...
module: {
rules: [
{
test: /\.(png|jpe?g|gif|svg)$/i, // Картинки
type: 'asset', // Автоматический выбор - inline или файл
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // Файлы до 8 KB зашиваются в код как base64
}
}
}
]
}
};
Как видите, один тип asset уже позволяет решить сразу несколько задач: мелкие картинки инлайнить, большие – складывать в файлы.
Другие варианты type:
- asset/resource – всегда файл
- asset/inline – всегда data URL (base64)
- asset/source – сырое содержимое (например, для текстовых файлов)
resolve – как Webpack ищет модули
resolve управляет тем, как Webpack обрабатывает import и require.
Расширения по умолчанию
module.exports = {
// ...
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx']
// Список расширений, которые подставляются автоматически
}
};
Теперь вы можете писать:
// Вместо './App.jsx' можно просто './App'
import App from './App';
Алиасы путей
Чтобы не писать длинные относительные пути, удобно использовать alias.
const path = require('path');
module.exports = {
// ...
resolve: {
alias: {
// Теперь вы можете импортировать из '@components/Button'
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils')
}
}
};
Покажу, как это выглядит в коде:
// Вместо '../../../components/Button'
import Button from '@components/Button';
// Вместо '../../utils/helpers'
import { formatDate } from '@utils/date';
Это сильно улучшает читаемость импортов в больших проектах.
devtool – source map для отладки
Source map связывает скомпилированный код с исходным, чтобы в браузере вы видели "настоящие" строки из своих файлов.
module.exports = {
// ...
devtool: 'source-map'
};
Полезные варианты:
eval-cheap-module-source-map– быстрый для разработкиsource-map– полный, но медленнее, чаще используют в продакшенеfalse– без source map
Обычно делают так:
module.exports = {
// ...
mode: 'development',
devtool: 'eval-cheap-module-source-map'
};
И в прод-конфиге:
module.exports = {
// ...
mode: 'production',
devtool: 'source-map' // или отключить, если вам это не нужно
};
plugins – расширение возможностей Webpack
Плагины позволяют делать то, чего не могут лоадеры: генерировать HTML, выносить CSS в отдельные файлы, чистить папки, анализировать бандлы и многое другое.
Общая структура
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html', // Шаблон
filename: 'index.html', // Имя выходного файла
inject: 'body' // Вставлять скрипты в конец body
})
]
};
Смотрите, в отличие от лоадеров, плагины создаются как экземпляры классов.
HtmlWebpackPlugin – автогенерация HTML
Частый сценарий – вы хотите, чтобы Webpack сам подключал собранные скрипты в HTML.
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
clean: true
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html', // Исходный HTML
minify: false // В dev можно не минифицировать
})
]
};
Теперь при каждой сборке в dist/index.html автоматически будут подставляться актуальные имена файлов с хэшами.
MiniCssExtractPlugin – вынос CSS в отдельные файлы
В production лучше не держать стили в JS, а вынести их.
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
// ...
module: {
rules: [
{
test: /\.css$/,
use: [
// В продакшене вместо style-loader используем плагин
MiniCssExtractPlugin.loader, // Выносит CSS в отдельные файлы
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css' // Имя css-файлов
})
]
};
Как видите, плагин работает в паре с лоадером: он перехватывает стили и складывает их в отдельный файл, а не встраивает в JS.
DefinePlugin – передача переменных окружения в код
Чтобы использовать переменные окружения в фронтенд-коде, применяют DefinePlugin.
const webpack = require('webpack');
module.exports = {
// ...
plugins: [
new webpack.DefinePlugin({
// Здесь мы определяем глобальную константу
'process.env.API_URL': JSON.stringify(process.env.API_URL || 'https://api.dev.local')
})
]
};
Теперь вы можете в коде писать:
// Здесь мы используем переменную окружения, подставленную Webpack
const apiUrl = process.env.API_URL;
Webpack на этапе сборки заменит это выражение на конкретную строку.
devServer – локальная разработка
webpack-dev-server поднимает локальный сервер, пересобирает проект при изменениях и может автоматически обновлять страницу.
Базовая конфигурация devServer
module.exports = {
// ...
devServer: {
static: {
directory: path.resolve(__dirname, 'dist') // Папка с собранными файлами
},
port: 3000, // Порт сервера
open: true, // Автооткрытие браузера
hot: true, // Горячая перезагрузка модулей (HMR)
historyApiFallback: true
// Важная настройка для SPA - перенаправляет 404 на index.html
}
};
Смотрите, при hot: true Webpack будет стараться обновлять только изменившиеся модули без полной перезагрузки страницы. Это ускоряет разработку.
Proxy для API
Часто на dev-сервере нужно проксировать запросы к backend.
module.exports = {
// ...
devServer: {
// ...
proxy: {
'/api': {
target: 'http://localhost:5000', // Адрес backend
changeOrigin: true, // Подменять Origin в запросах
pathRewrite: { '^/api': '' } // Удалять /api из пути
}
}
}
};
Теперь запрос из фронтенда к /api/users уйдет на http://localhost:5000/users.
Оптимизация – splitChunks и кэширование
Разделение кода – splitChunks
При большой кодовой базе хорошо отделить сторонние библиотеки от вашего кода. Это позволяет браузеру кэшировать vendor-часть.
module.exports = {
// ...
optimization: {
splitChunks: {
chunks: 'all', // Анализировать и синхронные, и асинхронные чанки
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/, // Все из node_modules
name: 'vendors', // Имя чанка
chunks: 'all'
}
}
}
}
};
Давайте посмотрим, как это помогает:
- меняется ваш код – перезагружается только main.[hash].js
- библиотеки из node_modules остаются в vendors.[hash].js, и его можно кэшировать дольше
Runtime chunk
Еще одна полезная настройка:
module.exports = {
// ...
optimization: {
runtimeChunk: 'single'
}
};
Webpack выносит служебный код (runtime) в отдельный файл, что уменьшает количество изменений в основных чанках и улучшает кэширование.
Разделение конфигураций на dev и prod
В одном файле неудобно держать и dev, и prod-настройки. Обычно конфиг разделяют.
Структура проекта:
- webpack.common.js – общие настройки
- webpack.dev.js – только для разработки
- webpack.prod.js – только для продакшена
Общий конфиг (webpack.common.js)
// webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
/** @type {import('webpack').Configuration} */
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
clean: true
},
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: 'babel-loader' // Подключаем Babel для JS
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
})
],
resolve: {
extensions: ['.js']
}
};
Dev-конфиг (webpack.dev.js)
// webpack.dev.js
const { merge } = require('webpack-merge');
const path = require('path');
const common = require('./webpack.common.js');
/** @type {import('webpack').Configuration} */
module.exports = merge(common, {
mode: 'development',
devtool: 'eval-cheap-module-source-map',
devServer: {
static: {
directory: path.resolve(__dirname, 'dist')
},
port: 3000,
open: true,
hot: true,
historyApiFallback: true
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader', // В dev стили встраиваем в DOM
'css-loader'
]
}
]
}
});
Prod-конфиг (webpack.prod.js)
// webpack.prod.js
const { merge } = require('webpack-merge');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const common = require('./webpack.common.js');
/** @type {import('webpack').Configuration} */
module.exports = merge(common, {
mode: 'production',
devtool: 'source-map',
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader, // В продакшене выносим стили в файлы
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css'
})
],
optimization: {
splitChunks: {
chunks: 'all'
},
runtimeChunk: 'single'
}
});
Теперь вы можете запускать:
# Сборка для разработки
npx webpack serve --config webpack.dev.js
# Сборка для продакшена
npx webpack --config webpack.prod.js
Смотрите, подход с разделением конфигов позволяет вам четко отделить "как мы собираем код" и "для какого окружения мы его собираем".
Типичные практические шаблоны конфигурации
Базовый конфиг для React-приложения
Здесь я покажу пример, который можно взять за основу для React.
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
/** @type {import('webpack').Configuration} */
module.exports = {
entry: './src/index.jsx', // Точка входа с React
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.[contenthash].js',
clean: true,
publicPath: '/'
},
mode: 'development',
devtool: 'eval-cheap-module-source-map',
module: {
rules: [
{
test: /\.(js|jsx)$/, // Поддерживаем JS и JSX
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
// Здесь мы настраиваем пресеты для React
presets: [
'@babel/preset-react',
'@babel/preset-env'
]
}
}
},
{
test: /\.css$/,
use: [
'style-loader', // Встраиваем стили
'css-loader'
]
},
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset', // Обработка картинок через asset-модули
}
]
},
resolve: {
extensions: ['.js', '.jsx']
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
title: 'React App'
})
],
devServer: {
static: {
directory: path.resolve(__dirname, 'dist')
},
historyApiFallback: true, // Для React Router
port: 3000,
open: true,
hot: true
}
};
Теперь вы увидите, как это выглядит в коде:
// src/index.jsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import './styles.css'; // Импорт стилей
// Здесь мы подключаем React-приложение к DOM
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />);
Такой конфиг уже достаточно удобен для учебных и небольших боевых проектов.
Заключение
Конфигурация Webpack – это описание того, как именно ваш проект превращается из набора исходников в итоговый бандл. Основные элементы, которые важно понимать:
entry– точки входа, с которых Webpack начинает построение графа модулейoutput– структура и расположение итоговых файловmode– набор дефолтных оптимизаций для dev или prodmodule.rules– правила обработки разных типов файлов через загрузчикиresolve– как находятся модули и как упрощаются путиdevtool– настройка source map для отладкиplugins– расширение возможностей сборкиdevServer– локальная разработка с автообновлениемoptimization– разделение кода и улучшение кэширования
Когда вы начинаете осмысленно собирать эти части, webpack-config из непонятного файла превращается в прозрачную схему сборки, которую можно адаптировать под любые требования проекта.
Главная идея в том, чтобы конфигурация была не "магией", а описанием понятного вам процесса: какие файлы есть в проекте, как их нужно преобразовать и в каком виде вы хотите получить результат.
Частозадаваемые технические вопросы по теме и ответы
Как передавать разные API URL для dev и prod через webpack-config
Используйте переменные окружения и DefinePlugin:
- Установите
cross-envдля кроссплатформенной установки переменных. - В package.json:
{
"scripts": {
"build:dev": "cross-env API_URL=https://dev.api.local webpack --config webpack.dev.js",
"build:prod": "cross-env API_URL=https://api.prod.local webpack --config webpack.prod.js"
}
}
- В webpack-конфиге:
const webpack = require('webpack');
plugins: [
new webpack.DefinePlugin({
'process.env.API_URL': JSON.stringify(process.env.API_URL)
})
]
Теперь вы можете читать process.env.API_URL в коде.
Как подключить TypeScript в webpack-config
- Установите зависимости:
npm install --save-dev typescript ts-loader
- Создайте tsconfig.json с нужными настройками.
- В webpack-конфиге:
module: {
rules: [
{
test: /\.tsx?$/, // Файлы .ts и .tsx
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: ['.ts', '.tsx', '.js']
}
Теперь Webpack будет собирать TypeScript-код.
Как настроить код-сплиттинг по роутам в SPA
Используйте динамические импорты:
// В React Router
const UsersPage = React.lazy(() => import('./pages/UsersPage'));
В webpack-конфиге достаточно иметь базовую настройку, Webpack сам создаст отдельные чанки. Для более тонкого контроля:
output: {
// ...
chunkFilename: '[name].[contenthash].js' // Имя для асинхронных чанков
}
Как настроить абсолютные импорты из src без alias
Один из вариантов – добавить src в resolve.modules:
resolve: {
modules: [
path.resolve(__dirname, 'src'),
'node_modules'
]
}
Теперь вы можете писать:
// Импорт из src/components/Button.jsx
import Button from 'components/Button';
Как сделать, чтобы Webpack не падал при ошибках TypeScript или ESLint, а только показывал их
Для TSC:
- Используйте
fork-ts-checker-webpack-pluginи задайтеasync: true, чтобы проверки шли параллельно и не блокировали сборку.
Для ESLint:
- Включите
eslint-webpack-pluginс опциейfailOnWarning: falseиfailOnError: false, чтобы ошибки не прерывали сборку, а только отображались в консоли.