Наводим порядок в конфигах Webpack
Всем привет. Меня зовут Евгений Чернышев, и я возглавляю фронтенд-разработку в одном из направлений деятельности Домклик. Хочу поделиться своими мыслями о том, как управлять сложными конфигурациями Webpack. Сразу «проведу черту», чтобы предотвратить возможные холивары: сравнение Webpack с другими бандлерами (Rollup, Vite и прочими) выходит за рамки статьи.
Де-факто, Webpack является основным сборщиком фронтенд-проектов. Это зрелый продукт, который до сих пор развивается и повсеместно используется. Но, как и любой инструмент, он имеет свои слабые стороны. Я считаю что основной недостаток Webpack — это сложность его конфигурации. На крупных долгоживущих проектах конфигурационные файлы становятся слишком большими и нечитаемыми, превращаясь в мешанину вложенных объектов и spread-операторов. Чтобы показать, что я имею в виду, рассмотрим стадии развития проекта.
Рождение
Наверное, каждый из нас ощущал то воодушевляющее волнение, когда приходил тимлид и говорил: «У нас будет новый проект». Помним мы и свои мысли в тот момент: «Вот с сегодняшнего дня я буду писать код аккуратно, буду применять лучшие практики и теперь уж точно-точно не стану плодить тоннами техдолг!» Ну что же, готов и простенький конфигурационный файл Webpack:
Первый вариант конфигурации
const path = require('path');
const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const htmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: path.resolve('public', 'index.tsx'),
output: {
path: path.resolve('dist'),
clean: true
},
resolve: {
modules: ['node_modules'],
extensions: ['.js', '.ts', '.tsx'],
plugins: [new TsconfigPathsPlugin()]
},
stats: {
preset: 'normal',
modules: false
},
performance: {
hints: false
},
devtool: 'eval-cheap-module-source-map',
devServer: {
historyApiFallback: true,
port: 3000,
allowedHosts: 'all'
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
loader: 'ts-loader',
options: {
transpileOnly: true
},
exclude: /node_modules/
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(svg|gif|png|jpg|jpeg|webp)$/i,
type: 'asset',
exclude: /node_modules/
}
]
},
plugins: [
new ReactRefreshWebpackPlugin(),
new ForkTsCheckerWebpackPlugin()
new htmlWebpackPlugin({
filename: 'index.html',
template: path.resolve('public', 'index.html')
})
]
};
Всё чистенько, понятно, удобочитаемо. Сразу видно, где загрузчик для React-компонентов, какой плагин отвечает за настройку псевдонимов (aliases) для путей модулей проекта. Разве может что-то пойти не так?
Развёртывание в эксплуатацию
Спустя месяц интенсивной разработки, соблюдения лучших практик, применения самых новых версий ${packageName}
и самого React наш проект готов к промышленной эксплуатации. Мы всё проверили, написали приличное количество unit-тестов, даже заморочились на пару-тройку e2e-тестов. Нас переполняет чувство гордости за достойно проделанную работу. А ещё мы испытываем лёгкое волнение перед первым развёртыванием нашего newawesomerealestateservice.domclick.ru.
Если внимательно присмотреться, то можно обнаружить, что бессмысленно оставлять для prod-сборки ReactRefreshWebpackPlugin. А ведь есть ещё стандарты компании и требования бизнеса. Бизнес хочет добавить на сайт Яндекс-метрику, а стандарты компании требуют использование Sentry в проектах. Кроме того, у нас есть CI/CD, а также несколько контуров приложения (тестовый и эксплуатационный). По сути, нам требуется передавать в сборку некоторый набор переменных окружения отдельно для каждого контура. Webpack, конечно же, это делать умеет. Нужно взять тот же DefinePlugin и чуть докрутить конфигурационный файл:
Вариант конфигурации во время первой публикации сервиса
const path = require('path');
const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const htmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
entry: path.resolve('public', 'index.tsx'),
output: {
path: path.resolve('dist'),
clean: true
},
resolve: {
modules: ['node_modules'],
extensions: ['.js', '.ts', '.tsx'],
plugins: [new TsconfigPathsPlugin()]
},
stats: {
preset: 'normal',
modules: false
},
performance: {
hints: false
},
devtool: 'eval-cheap-module-source-map',
devServer: {
historyApiFallback: true,
port: 3000,
allowedHosts: 'all'
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
loader: 'ts-loader',
options: {
transpileOnly: true
},
exclude: /node_modules/
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(svg|gif|png|jpg|jpeg|webp)$/i,
type: 'asset',
exclude: /node_modules/
}
]
},
plugins: [
process.env.NODE_ENV === 'development' && new ReactRefreshWebpackPlugin(),
new ForkTsCheckerWebpackPlugin(),
new htmlWebpackPlugin({
filename: 'index.html',
template: path.resolve('public', 'index.html')
}),
process.env.NODE_ENV === 'production' && new webpack.DefinePlugin({
YM_ID: process.env.YANDEX_METRICA_ID,
SENTRY_DSN: process.env.SENTRY_DSN,
}),
].filter(Boolean),
};
Код усложнился, но совсем не критично. Одним условием больше, один условием меньше — принципиальной разницы тут нет.
Жизнь в эксплуатации
Долгожданный релиз состоялся, всё прошло даже лучше, чем мы ожидали. Никаких падений, хотфиксов и прочих неприятных вещей: просто раскатили и оно заработало. Опытные разработчики тут усмехнутся, но на самом деле так оно и было. Мы с уважаемыми коллегами отметили успешный релиз в баре, потом в караоке, потом… Но это уже совсем другая история.
Мало по малу, конфигурация Webpack начала усложняться. Добавили поддержку SVGR, потом мы затащили CSS-модули, до кучи докинули Bundle Analyzer. Добавили в конфигурационный файл забытую впопыхах секцию optimization
. А ещё много раз приходил бизнес и просил новые фичи, менял требования.
Спустя год конфигурация Webpack претерпела сильные изменения:
Конфигурация спустя год жизни проекта
const path = require('path');
const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const htmlWebpackPlugin = require('html-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const config = {
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
entry: path.resolve('public', 'index.tsx'),
output: {
path: path.resolve('dist'),
clean: true
},
resolve: {
modules: ['node_modules'],
extensions: ['.js', '.ts', '.tsx'],
plugins: [new TsconfigPathsPlugin()]
},
stats: {
preset: 'normal',
modules: false
},
performance: {
hints: false
},
devtool: 'eval-cheap-module-source-map',
devServer: {
historyApiFallback: true,
port: 3000,
allowedHosts: 'all'
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
loader: 'ts-loader',
options: {
transpileOnly: true
},
exclude: /node_modules/
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.module\.scss$/,
sideEffects: true,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 3,
modules: {
localIdentName: '[local]_[hash:base64:5]'
}
}
},
'postcss-loader',
'resolve-url-loader',
'sass-loader'
],
exclude: /node_modules/
},
{
test: /\.scss$/,
sideEffects: true,
use: [
'style-loader',
{
loader: 'css-loader',
options: {}
},
'postcss-loader',
'resolve-url-loader',
'sass-loader'
],
exclude: [/node_modules/, /\.module\.scss$/]
},
{
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
use: ['@svgr/webpack'],
},
{
test: /\.(gif|png|jpg|jpeg|webp)$/i,
type: 'asset',
exclude: /node_modules/
}
]
},
plugins: [
process.env.NODE_ENV === 'development' && new ReactRefreshWebpackPlugin(),
new ForkTsCheckerWebpackPlugin(),
new htmlWebpackPlugin({
filename: 'index.html',
template: path.resolve('public', 'index.html')
}),
process.env.NODE_ENV === 'production' && new webpack.DefinePlugin({
YM_ID: process.env.YANDEX_METRICA_ID,
SENTRY_DSN: process.env.SENTRY_DSN,
}),
process.env.NODE_ENV === 'development' && new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-analysis.html',
generateStatsFile: true,
openAnalyzer: false,
}),
].filter(Boolean),
};
module.exports = process.env.NODE_ENV === 'development' ? config : {
...config,
...{ optimization: {
minimize: true,
moduleIds: 'deterministic',
minimizer: [
new TerserPlugin({
terserOptions: {
warnings: false,
compress: {
comparisons: false,
},
parse: {},
mangle: true,
output: {
comments: false,
ascii_only: true,
},
},
parallel: true,
}),
],
nodeEnv: process.env.NODE_ENV,
sideEffects: true,
concatenateModules: true,
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
cacheGroups,
},
},
},
};
Прочитать такой код и разобраться в нём уже непросто, но мы можем взять хотя бы тот же console.log или пройтись отладчиком, если сходу не поймём что тут происходит.
А не прикрутить ли нам SSR?
Использование SSR (Server Side Rendering) позволит нам улучшить клиентский опыт за сайта в выдаче поисковиков. Изначально на проекте мы не использовали фреймворки серверного рендеринга вроде Next.js, а всё решили сделать сами. По сути, сборка для SSR — это та же самая production-сборка, только с определёнными изменениями. Нужно учесть, что здесь сборщик собирает серверное node.js-приложение. Попробуем отразить доработку в файле конфигурации Webpack:
Конфигурация после дальнейшего усложнения
const path = require('path');
const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const htmlWebpackPlugin = require('html-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const config = {
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
entry: path.resolve('public', 'index.tsx'),
output: {
path: path.resolve('dist'),
clean: true
},
resolve: {
modules: ['node_modules'],
extensions: ['.js', '.ts', '.tsx'],
plugins: [new TsconfigPathsPlugin()]
},
stats: {
preset: 'normal',
modules: false
},
performance: {
hints: false
},
devtool: 'eval-cheap-module-source-map',
devServer: {
historyApiFallback: true,
port: 3000,
allowedHosts: 'all'
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
loader: 'ts-loader',
options: {
transpileOnly: true
},
exclude: /node_modules/
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.module\.scss$/,
sideEffects: true,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 3,
modules: {
localIdentName: '[local]_[hash:base64:5]'
}
}
},
'postcss-loader',
'resolve-url-loader',
'sass-loader'
],
exclude: /node_modules/
},
{
test: /\.scss$/,
sideEffects: true,
use: [
'style-loader',
{
loader: 'css-loader',
options: {}
},
'postcss-loader',
'resolve-url-loader',
'sass-loader'
],
exclude: [/node_modules/, /\.module\.scss$/]
},
{
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
use: ['@svgr/webpack'],
},
{
test: /\.(gif|png|jpg|jpeg|webp)$/i,
type: 'asset',
exclude: /node_modules/
}
]
},
plugins: [
process.env.NODE_ENV === 'development' && new ReactRefreshWebpackPlugin(),
new ForkTsCheckerWebpackPlugin(),
new htmlWebpackPlugin({
filename: 'index.html',
template: path.resolve('public', 'index.html')
}),
process.env.NODE_ENV === 'production' && new webpack.DefinePlugin({
YM_ID: process.env.YANDEX_METRICA_ID,
SENTRY_DSN: process.env.SENTRY_DSN,
}),
process.env.NODE_ENV === 'development' && new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-analysis.html',
generateStatsFile: true,
openAnalyzer: false,
}),
].filter(Boolean),
};
module.exports = process.env.NODE_ENV === 'development' ? config : {
...config,
...{ optimization: process.env.IS_SSR === 'true' ? {
minimize: false,
} : {
minimize: true,
moduleIds: 'deterministic',
minimizer: [
new TerserPlugin({
terserOptions: {
warnings: false,
compress: {
comparisons: false,
},
parse: {},
mangle: true,
output: {
comments: false,
ascii_only: true,
},
},
parallel: true,
}),
],
nodeEnv: process.env.NODE_ENV,
sideEffects: true,
concatenateModules: true,
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
cacheGroups,
},
},
},
};
Хьюстон, ви хэв э проблем
Когда с коллегой сели отлаживать конфиг Webpack.
Вот и настал момент осознания неприятного факта: наш некогда новый проект, сотканный из последних версий библиотек и best practices, почему-то превратился в тухлую легасятину. Конфигурация Webpack стала крайне запутанной и совершенно неподдерживаемой. Релизы проходят с проблемами, участники команды выгорают.
И бизнес, и разработка понимают, что с этим срочно нужно что-то сделать. Пришло время рефакторинга. Но куда же двигаться?
Возможное решение проблемы
На самом деле почти любую проблему, с которой сталкивается разработчик, уже кто-то решал раньше, и решал хорошо. Рецепты лаконичных и гибких решений типичных проблем структурированы, про них написаны классические книги. Это паттерны проектирования.
Под нашу задачу хорошо подходит порождающий паттерн «Строитель» (builder). Определение:
Строитель — порождающий паттерн, отделяет конструирование сложного объекта от его представления, так что в результате одного и того же процесса конструирования могут получаться разные представления.
Как это всё соотносится с нашей задачей? Как раз использование паттерна «Строитель» позволит нам «разложить по полочкам» все загрузчики, плагины и ветвления. Итак, приступим. У нас есть некая начальная заготовка конфигурации Webpack (я взял за основу Webpack 5-й версии).
Hidden text
const config = {
mode: 'development',
name: 'awesome-real-estate-config',
devtool: false,
bail: false,
entry: [],
output: {},
optimization: {
minimize: false,
moduleIds: 'named',
},
performance: false,
plugins: [],
module: {
strictExportPresence: true,
rules: [],
},
resolve: {
extensions: ['.tsx', '.jsx', '.ts', '.js'],
mainFields: ['browser', 'es2015', 'module', 'main'],
modules: ['node_modules'],
plugins: [new TsconfigPathsPlugin()],
},
};
Эту заготовку мы помещаем внутрь основного класса-строителя, реализующего три публичных метода:
addLoader
— добавляет в конфигурацию загрузчик;addPlugin
— добавляет в конфигурацию плагин;build
— выдаёт готовый конфигурационный объект Webpack.
Все эти публичные методы (кроме build
) способны объединяться в цепочки вызовов. Код строителя конфигурации выглядит как-то так:
Основной код строителя
const path = require('path');
const fs = require('fs');
const TerserPlugin = require('terser-webpack-plugin');
const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin');
const publicUrlOrPath = process.env.STATIC_PATH || '/';
const outputPath = path.resolve(`${__dirname}/dist/`);
class DefaultBuilder {
buildMode = 'development';
useSourcemap = false;
#loaders = [];
#plugins = [];
config = {
mode: 'development',
name: 'awesome-real-estate-config',
devtool: false,
bail: false,
entry: [],
output: {},
optimization: {
minimize: false,
moduleIds: 'named',
},
performance: false,
plugins: [],
module: {
strictExportPresence: true,
rules: [],
},
resolve: {
extensions: ['.tsx', '.jsx', '.ts', '.js'],
mainFields: ['browser', 'es2015', 'module', 'main'],
modules: ['node_modules'],
plugins: [new TsconfigPathsPlugin()],
},
};
/**
* Конструктор сборщика
* @param {string} buildMode - режим сборки ('development', 'production')
*
*/
constructor(buildMode) {
this.config.mode = buildMode;
this.buildMode = buildMode;
this.config.devtool =
buildMode === 'development' ? false : 'eval-cheap-module-source-map';
this.config.bail = buildMode === 'production';
this.#configureOutput();
}
/**
* Добавляет загрузчик в список загрузчиков Webpack
* @param {Object|Function} loaderConfig - конфигурация лоадера
*
* @returns {WebpackBuilder}
*
*/
addLoader(loaderConfig) {
let loader;
if (typeof loaderConfig === 'function') {
loader = loaderConfig(this.buildMode, this.#isDevelopment());
} else {
loader = loaderConfig;
}
this.#loaders = [...this.loaders, loader];
return this;
}
/**
* Добавляет плагин в список плагинов Webpack
* @param {Object|Function} pluginConfig - конфигурация плагина
*
* @returns {WebpackBuilder}
*
*/
addPlugin(pluginConfig) {
let plugin;
if (typeof pluginConfig === 'function') {
plugin = pluginConfig(this.buildMode, this.#isDevelopment());
} else {
plugin = pluginConfig;
}
this.#plugins = [...this.#plugins, plugin];
return this;
}
/**
* Собирает конфигурацию Webpack, основной метод
*
* @returns {Object}
*
*/
build() {
const loadersConfig = [{ oneOf: this.#loaders }];
this.config.module.rules = [...this.config.module.rules, ...loadersConfig];
this.config.plugins = [...this.config.plugins, ...this.#plugins];
return this.config;
}
/**
* Настраивает выходную конфигурацию
*
* @returns {Object}
*
*/
#configureOutput() {
const outputConfig = {
path: this.#isProduction() ? outputPath : undefined,
pathinfo: this.#isDevelopment(),
filename: this.#isDevelopment()
? '[name].js'
: '[name].[contenthash:8].js',
publicPath: publicUrlOrPath,
};
this.config.output = { ...this.config.output, ...outputConfig };
}
/**
* Очевидно определяет, является ли текущий режим работы
* "Строителя" development-режимом
*
* @returns {boolean}
*
*/
#isDevelopment() {
return this.buildMode === 'development';
}
/**
* Определяет, является ли текущий режим работы
* "Строителя" production-режимом
*
* @returns {boolean}
*
*/
#isProduction() {
return this.buildMode === 'production';
}
}
class DevelopmentBuilder extends DefaultBuilder {
#port;
#host;
/**
* Конструктор сборщика. Устанавливает хост и порт
* для webpack-dev-server
*
* @param {string} host - хост для dev-сервера
* @param {number} port - порт для dev-сервера
*
*/
constructor(host, port) {
super('development');
this.#host = host;
this.#port = port;
this.#configureWebServer();
}
#configureWebServer() {
this.config.devServer = {
port: this.#port,
host: this.#host,
historyApiFallback: true,
open: true,
liveReload: true,
hot: true,
server: {
type: 'spdy',
options: {
key: fs.readFileSync(path.join(__dirname, '.ssl/key.key')),
cert: fs.readFileSync(path.join(__dirname, '.ssl/cert.cert')),
},
},
};
return this;
}
}
class ProductionBuilder extends DefaultBuilder {
#useSourcemap;
constructor(useSourcemap = false) {
super('production');
// иногда бывает полезно собрать с сорсмапами для отладки
this.#useSourcemap = useSourcemap;
this.#configureDevtool();
this.#configureOptimization();
}
/**
* Настраиваем минификацию, а также разбиение бандла на чанки
*/
#configureOptimization() {
const vendorCacheGroups = {
axios: 'axios',
'date-fns': 'date-fns',
lodash: 'lodash.*?',
react: '(react|react-dom)',
rxjs: 'rxjs',
sentry: '@sentry',
};
const cacheGroups = {
...Object.fromEntries(
Object.entries(vendorCacheGroups).map(([name, libPath]) => [
name,
{
name,
test: new RegExp(`/node_modules/${libPath}/.*\\.js$`),
priority: 1,
enforce: true,
},
]),
),
vendors: {
name: 'vendors',
test: /\/node_modules\//,
priority: 0,
enforce: true,
},
};
const optimization = {
minimize: true,
moduleIds: 'deterministic',
minimizer: [
new TerserPlugin({
terserOptions: {
warnings: false,
compress: {
comparisons: false,
},
parse: {},
mangle: true,
output: {
comments: false,
ascii_only: true,
},
},
parallel: true,
}),
],
nodeEnv: this.buildMode,
sideEffects: true,
concatenateModules: true,
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
cacheGroups,
},
};
this.config.optimization = { ...this.config.optimization, ...optimization };
}
/**
* Включаем в бандл генерацию сорсмапов, если это нужно
*/
#configureDevtool() {
if (this.#useSourcemap) {
this.config.devtool = 'eval-cheap-module-source-map';
}
}
}
class ProductionSSRBuilder extends DefaultBuilder {
constructor() {
super('production');
}
}
module.exports = {
DevelopmentBuilder,
ProductionBuilder,
ProductionSSRBuilder
};
Кажется, что кода много, но если вглядеться, то у нас есть три строителя: DevelopmentBuilder
— для локальной разработки, ProductionBuilder
— для создания эксплуатационных сборок, а также ProductionSSRBuilder
для сборки SSR-проекта. Сложность скрывается внутри этих классов, но их использование довольно простое и лаконичное. Сами конфигурации загрузчиков и плагинов также положим в отдельные модули:
Конфигурация загрузчиков Webpack
module.exports = {
tsLoader: {
test: /\.(ts|tsx)$/,
loader: 'ts-loader',
options: { transpileOnly: true },
exclude: /node_modules/
},
svgLoader: {
test: /\.svg$/,
use: [{
loader: '@svgr/webpack',
options: { babel: false }
}]
},
cssLoader: {
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
scssModuleLoader: {
test: /\.module\.scss$/,
sideEffects: true,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 3,
modules: { localIdentName: '[local]_[hash:base64:5]' }
}
},
'postcss-loader',
'resolve-url-loader',
'sass-loader'
],
exclude: /node_modules/
},
scssLoader: {
test: /\.scss$/,
sideEffects: true,
use: [
'style-loader',
{
loader: 'css-loader',
options: {}
},
'postcss-loader',
'resolve-url-loader',
'sass-loader'
],
exclude: [
/node_modules/,
/\.module\.scss$/
]
},
assets: {
test: /\.(gif|png|jpg|jpeg|webp)$/i,
type: 'asset',
exclude: /node_modules/
}
};
Конфигурация плагинов Webpack
const path = require('path');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const htmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
reactRefreshPlugin: new ReactRefreshWebpackPlugin(),
forkTsCheckerPlugin: new ForkTsCheckerWebpackPlugin(),
htmlWebpackPlugin: new htmlWebpackPlugin({
filename: 'index.html',
template: path.resolve('public', 'index.html')
}),
definePlugin: new webpack.DefinePlugin({
YM_ID: process.env.YANDEX_METRICA_ID,
SENTRY_DSN: process.env.SENTRY_DSN
}),
bundleAnalyzerPlugin: new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-analysis.html',
generateStatsFile: true,
openAnalyzer: false
})
};
Да, строк кода получается больше. Зато теперь мы можем удобно набирать конфигурации. Глаз радуется от такой красоты:
Пример development-конфигурации
const { DevelopmentBuilder } = require('./webpackBuilder');
const {
tsLoader,
svgLoader,
cssLoader,
scssModuleLoader,
scssLoader,
assets,
} = require('./loaders');
const {
reactRefreshPlugin,
forkTsCheckerPlugin,
htmlWebpackPlugin,
bundleAnalyzerPlugin,
} = require('./plugins');
const builder = new DevelopmentBuilder('localhost', 8282);
const webpackConfig = builder
.addLoader(tsLoader)
.addLoader(svgLoader)
.addLoader(cssLoader)
.addLoader(scssModuleLoader)
.addLoader(scssLoader)
.addLoader(assets)
.addPlugin(reactRefreshPlugin)
.addPlugin(forkTsCheckerPlugin)
.addPlugin(htmlWebpackPlugin)
.addPlugin(bundleAnalyzerPlugin)
.build();
module.exports = webpackConfig;
Пример production-конфигурации
const { ProductionBuilder } = require('./webpackBuilder');
const {
tsLoader,
svgLoader,
cssLoader,
scssModuleLoader,
scssLoader,
assets,
} = require('./loaders');
const {
forkTsCheckerPlugin,
htmlWebpackPlugin,
definePlugin,
} = require('./plugins');
const builder = new ProductionBuilder();
const webpackConfig = builder
.addLoader(tsLoader)
.addLoader(svgLoader)
.addLoader(cssLoader)
.addLoader(scssModuleLoader)
.addLoader(scssLoader)
.addLoader(assets)
.addPlugin(forkTsCheckerPlugin)
.addPlugin(htmlWebpackPlugin)
.addPlugin(definePlugin)
.build();
module.exports = webpackConfig;
Пример production-ssr-конфигурации
const { ProductionSSRBuilder } = require('./webpackBuilder');
const {
tsLoader,
svgLoader,
cssLoader,
scssModuleLoader,
scssLoader,
assets,
} = require('./loaders');
const {
forkTsCheckerPlugin,
htmlWebpackPlugin,
definePlugin,
} = require('./plugins');
const builder = new ProductionSSRBuilder();
const webpackConfig = builder
.addLoader(tsLoader)
.addLoader(svgLoader)
.addLoader(cssLoader)
.addLoader(scssModuleLoader)
.addLoader(scssLoader)
.addLoader(assets)
.addPlugin(forkTsCheckerPlugin)
.addPlugin(htmlWebpackPlugin)
.addPlugin(definePlugin)
.build();
module.exports = webpackConfig;
Теперь вся конфигурация у нас «разложена по полочкам», такой код легко читать. Мы избавились от ветвлений, spread-операторов и прочего визуального мусора. Любой разработчик в команде (в том числе и новичок) легко разберётся, как устроена конфигурация Webpack и как в неё вносить изменения.
Вывод
Хоть JS и не является классическим ООП-языком, однако многие шаблоны объектно-ориентированного проектирования вполне к нему применимы. Закончу очевидной мыслью: знание фундаментальных дисциплин, таких как шаблоны проектирования, алгоритмы и структуры данных всегда полезно и стоит потраченного на изучение времени.