Наводим порядок в конфигах 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.

Когда с коллегой сели отлаживать конфиг 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 и не является классическим ООП-языком, однако многие шаблоны объектно-ориентированного проектирования вполне к нему применимы. Закончу очевидной мыслью: знание фундаментальных дисциплин, таких как шаблоны проектирования, алгоритмы и структуры данных всегда полезно и стоит потраченного на изучение времени.


ссылка на оригинал статьи https://habr.com/ru/articles/779586/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *