
Всем привет. Меня зовут Евгений Чернышев, и я возглавляю фронтенд-разработку в одном из направлений деятельности Домклик. Хочу поделиться своими мыслями о том, как управлять сложными конфигурациями 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, }, }, }, };
Хьюстон, ви хэв э проблем
Вот и настал момент осознания неприятного факта: наш некогда новый проект, сотканный из последних версий библиотек и 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 и не является классическим ООП-языком, однако многие шаблоны объектно-ориентированного проектирования вполне к нему применимы. Закончу очевидной мыслью: знание фундаментальных дисциплин, таких как шаблоны проектирования, алгоритмы и структуры данных всегда полезно и стоит потраченного на изучение времени.
ссылка на оригинал статьи https://habr.com/ru/articles/779586/
Добавить комментарий