Наводим порядок в конфигах Webpack
- среда, 13 декабря 2023 г. в 00:00:14
Всем привет. Меня зовут Евгений Чернышев, и я возглавляю фронтенд-разработку в одном из направлений деятельности Домклик. Хочу поделиться своими мыслями о том, как управлять сложными конфигурациями 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 (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,
},
},
},
};
Вот и настал момент осознания неприятного факта: наш некогда новый проект, сотканный из последних версий библиотек и best practices, почему-то превратился в тухлую легасятину. Конфигурация Webpack стала крайне запутанной и совершенно неподдерживаемой. Релизы проходят с проблемами, участники команды выгорают.
И бизнес, и разработка понимают, что с этим срочно нужно что-то сделать. Пришло время рефакторинга. Но куда же двигаться?
На самом деле почти любую проблему, с которой сталкивается разработчик, уже кто-то решал раньше, и решал хорошо. Рецепты лаконичных и гибких решений типичных проблем структурированы, про них написаны классические книги. Это паттерны проектирования.
Под нашу задачу хорошо подходит порождающий паттерн «Строитель» (builder). Определение:
Строитель — порождающий паттерн, отделяет конструирование сложного объекта от его представления, так что в результате одного и того же процесса конструирования могут получаться разные представления.
Как это всё соотносится с нашей задачей? Как раз использование паттерна «Строитель» позволит нам «разложить по полочкам» все загрузчики, плагины и ветвления. Итак, приступим. У нас есть некая начальная заготовка конфигурации Webpack (я взял за основу Webpack 5-й версии).
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-проекта. Сложность скрывается внутри этих классов, но их использование довольно простое и лаконичное. Сами конфигурации загрузчиков и плагинов также положим в отдельные модули:
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/
}
};
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
})
};
Да, строк кода получается больше. Зато теперь мы можем удобно набирать конфигурации. Глаз радуется от такой красоты:
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;
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;
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 и не является классическим ООП-языком, однако многие шаблоны объектно-ориентированного проектирования вполне к нему применимы. Закончу очевидной мыслью: знание фундаментальных дисциплин, таких как шаблоны проектирования, алгоритмы и структуры данных всегда полезно и стоит потраченного на изучение времени.