Настройка Webpack - несколько точек входа и разделение на чанки

Дисклеймер

Я недавно начал разбираться с Webpack, поэтому некоторые вещи могут показаться странными. Если вы нашли ошибку, или ugly-код, то напишите об этом в комментариях, я буду благодарен.

Описание задачи

Я работаю над небольшим дополнением для сайта. Дополнение включает в себя 2 практически независимые части — личный кабинет и общедоступную часть. По сути это два независимых SPA.

После базовой настройки webpack встал вопрос, на который я попробую ответить в этой заметке:

Как настроить webpack 4 чтобы одновременно собирать 2 страницы так, чтобы для каждой подгружались только необходимые бандлы?

Потребности такие:

  1. Личный кабинет — back
    • vue, vue-router, vuex
    • axios
    • @riophae/vue-treeselect
    • buefy
    • Собственные скрипты
  2. Общедоступная часть — front
    • vue, vue-router, vuex
    • axios
    • @riophae/vue-treeselect
    • Собственные скрипты

Конфиги с решением

И комментариями

webpack.base.conf.js Показать
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
//const CopyWebpackPlugin = require('copy-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader');

// Общие переменные для нескольких конфигов
const PATHS = {

  src_back: path.join(__dirname, '../src-back'),
  src_front: path.join(__dirname, '../src-front'),

  // Чанки для back. Вызываются в HtmlWebpackPlugin (в build и dev)
  back_chunks : ['common_vendors', 'buefy', 'back' ],
  front_chunks: ['common_vendors', 'front' ],


  dist: path.join(__dirname, '../docs'),
  assets: 'assets/',
}

module.exports = {

  // точки входа
  entry: {
    back: PATHS.src_back,
    front: PATHS.src_front,
  },

  // точки выхода
  output: {

    // Квадратные скобки означают, что берется файл с имеем точки входа https://youtu.be/JcKRovPhGo8?t=916
    filename: `js/[name].js?v=[hash]`,

    // папка назначения скомпилированных файлов https://nodejs.org/api/path.html#path_path_relative_from_to
    path: PATHS.dist,

    // Папка, которая отображается, может отличаться от реальной папки
    publicPath: '/'
  },

  // Разбиваем на отдельные файлы
  optimization: {
    splitChunks: {
      cacheGroups: {
        common_vendors: {
          test: /[\\/]node_modules[\\/](vue|vue-router|vuex|axios|@riophae[\\/]vue-treeselect)[\\/]/,
          name: 'common_vendors', // имя чанка
          chunks: 'initial',
          enforce: true,
        },
        buefy: {
          test: /[\\/]node_modules[\\/](buefy)[\\/]/,
          name: 'buefy', // имя чанка
          chunks: 'initial',
          enforce: true,
        },
      }
    }
  },
  resolve: {

    // Порядок обработки файлов.
    extensions: ['.js', '.vue', '.json'],
  },
  module: {

    // Определяем порядок обработки разных типов файлов.
    // Постпроцессоры, минификаторы и пр.
    rules: [{
        test: /\.js$/,
        loader: "babel-loader",

        // не включаем те файлы, которые содержет эта папка
        exclude: "/node_modules/",
      },
      {
        test: /\.vue$/,
        loader: "vue-loader",
        options: {
          loader: {

            // Определяем порядок обработки
            scss: 'vue-style-loader!css-loader!sass-loader'
          }
        }
      }, {
        test: /\.(png|jpg|jpeg|gif|svg)$/,
        loader: "file-loader",
        options: {
          name: '[name].[ext]'
        }
      }, {
        test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
        loader: "file-loader",
        options: {
          name: '[name].[ext]'
        }
      },
      {
        test: /\.scss$/,
        use: [
          'style-loader',
          MiniCssExtractPlugin.loader,
          {
            loader: "css-loader",
            options: {
              sourceMap: true
            }
          },
          {
            loader: "postcss-loader",
            options: {
              sourceMap: true,
              config: {
                path: `./js/postcss.config.js`
              }
            }
          },
          {
            loader: "sass-loader",
            options: {
              sourceMap: true
            }
          },
        ]
      },
      {
        test: /\.less$/,
        use: [
          'style-loader',
          MiniCssExtractPlugin.loader,
          {
            loader: "css-loader",
            options: {
              sourceMap: true
            }
          },
          {
            loader: "postcss-loader",
            options: {
              sourceMap: true,
              config: {
                path: `./js/postcss.config.js`
              }
            }
          },
          {
            loader: "less-loader",
            options: {
              sourceMap: true
            }
          },
        ]
      },
      {
        test: /\.css$/,
        use: [
          "style-loader",
          MiniCssExtractPlugin.loader,
          {
            loader: "css-loader",
            options: {
              sourceMap: true
            }
          },
          {
            loader: "postcss-loader",
            options: {
              sourceMap: true,
              config: {
                path: `./js/postcss.config.js`
              }
            }
          },
        ]
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin(),
    new MiniCssExtractPlugin({
      filename: `css/[name].css?h=[hash]`,
    }),

    //new CopyWebpackPlugin([]),
  ],
  node: {

    // Иначе как-то много кода лишнего добавляется. ХЗ:\
    Buffer: false
  },
  
  // Чтобы переменные были доступны в других файлах конфигураций (dev, build)
  externals: {
    paths: PATHS
  },
}
    
Скрыть
webpack.build.conf.js Показать
const merge = require('webpack-merge');
const baseWebpackConfig = require('./webpack.base.conf');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const buildWebpackConfig = merge(baseWebpackConfig, {
  mode: 'production',
  resolve: {
    alias: {
      
      // Для build используем минифицированный файл библиотеки
      'vue$': 'vue/dist/vue.min.js',
    }
  },
  plugins: [
  
    // Обработка шаблона точки входа "back"
    new HtmlWebpackPlugin({
      template: `${baseWebpackConfig.externals.paths.src_back}/index.html`,
      filename: `${baseWebpackConfig.externals.paths.dist}/mx_static/reception_points-vueapp-backend.php`,
      title: "mode_build", // Нужно для костыльного условия в шаблоне
      inject: false,
      chunks: baseWebpackConfig.externals.paths.back_chunks,
    }),
    
    // Обработка шаблона точки входа "front"
    new HtmlWebpackPlugin({
      template: `${baseWebpackConfig.externals.paths.src_front}/index.html`,
      filename: `${baseWebpackConfig.externals.paths.dist}/mx_static/reception_points-vueapp-front.php`,
      title: "mode_build", // Нужно для костыльного условия в шаблоне
      inject: false,
      chunks: baseWebpackConfig.externals.paths.front_chunks,
    }),
  ]
});
 module.exports = new Promise((resolve, reject) => {
   resolve(buildWebpackConfig);
 });
    
Скрыть
webpack.dev.conf.js Показать
const webpack = require('webpack');
const merge = require('webpack-merge');
const baseWebpackConfig = require('./webpack.base.conf');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const devWebpackConfig = merge(baseWebpackConfig, {
  mode: 'development',
  devtool: 'cheap-module-eval-source-map',
  resolve: {
    alias: {

      // Для dev используем не минифицированный файл библиотеки
      'vue$': 'vue/dist/vue.js',
    }
  },
  devServer: {
    contentBase: baseWebpackConfig.externals.paths.dist,
    
    // типа localhost (локальный домен настроен на Open Server)
    host: 'mysite.test',
    port: 8081,

    // Выводить ошибки компиляции в браузер
    overlay:{
      warnings: true,
      errors: true,
    },
    proxy: {
    
      // Для проксирования AJAX запросов, иначе будет ошибка политики доступа
      // С этой настройкой все запросы, содержащие '/api' будут переводиться с
      // 'http://mysite.test:8081 на http://mysite.test
      '/api': {
        target: 'http://mysite.test',
      }
    },
  },
  plugins:[
    new webpack.SourceMapDevToolPlugin({
      filename: '[file].map'
    }),
    
    // Обработка шаблона точки входа "back"
    new HtmlWebpackPlugin({
      template: `${baseWebpackConfig.externals.paths.src_back}/index.html`,
      filename: './index.html',
      title: "mode_development", // Нужно для костыльного условия в шаблоне
      inject: false,
      chunks: baseWebpackConfig.externals.paths.back_chunks,
    }),
    
    // Обработка шаблона точки входа "front"
    new HtmlWebpackPlugin({
      template: `${baseWebpackConfig.externals.paths.src_front}/index.html`,
      filename: './pp.html',
      title: "mode_development", // Нужно для костыльного условия в шаблоне
      inject: false,
      chunks: baseWebpackConfig.externals.paths.front_chunks,
    }),
  ]
});
module.exports = new Promise((resolve, reject) => {
 resolve(devWebpackConfig);
});
    
Скрыть
postcss.config.js Показать
module.exports = {
  plugins: [
    require('autoprefixer'),
    require('css-mqpacker'),
    require('cssnano')({
      preset: [
        'default', {
          discardComments: {
            removeAll: false,
          },
          //normalizeUrl: false
        }
      ]
    })
  ]
}
    
Скрыть
HTML Шаблон для 'back' Показать
<% if (htmlWebpackPlugin.options.title == 'mode_development') { %>
<!--
Условие
if (htmlWebpackPlugin.options.title == 'mode_development') {
Я использую для того, чтобы отобразить все эти элементы при dev-режиме,
но скрыть при build, т.к. на проекте мне нужен только элемент
<div class="body_content">...</div>
-->
<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title><%= htmlWebpackPlugin.options.title || 'Empty title' %></title>
</head>

<body>
<% } %>
  <div class="body_content">
    
    <!-- Подгружаем CSS Файлы -->
    <% for (var css in htmlWebpackPlugin.files.css) { %>
    <link rel="stylesheet" href="<%= htmlWebpackPlugin.files.css[css] %>">
    <% } %>

    <div id="app">
      <buefy-navbar></buefy-navbar>
      <router-view></router-view>
    </div>
    
    <!-- Подгружаем JS Файлы -->
    <% for (var js in htmlWebpackPlugin.files.js) { %>
    <script src="<%= htmlWebpackPlugin.files.js[js] %>"></script>
    <% } %>
    <script src="https://use.fontawesome.com/a304bad23c.js"></script>
  </div>
<% if (htmlWebpackPlugin.options.title == 'mode_development') { %>
</body>

</html>
<% } %>
    
Скрыть
package.json (на всякий случай) Показать
{
  "name": "server-test",
  "version": "1.0.0",
  "description": "test-descr",
  "main": "index.js",
  "devDependencies": {
    "@babel/core": "^7.6.0",
    "@babel/preset-env": "^7.6.0",
    "autoprefixer": "^9.6.1",
    "babel-loader": "^8.0.6",
    "copy-webpack-plugin": "^5.0.4",
    "css-loader": "^3.2.0",
    "css-mqpacker": "^7.0.0",
    "cssnano": "^4.1.10",
    "fibers": "^4.0.1",
    "file-loader": "^4.2.0",
    "fs": "0.0.1-security",
    "html-webpack-plugin": "^3.2.0",
    "mini-css-extract-plugin": "^0.8.0",
    "node-sass": "^4.12.0",
    "npm-git-install": "^0.3.0",
    "path": "^0.12.7",
    "postcss-loader": "^3.0.0",
    "pug": "^2.0.4",
    "pug-loader": "^2.4.0",
    "sass": "^1.22.12",
    "sass-loader": "^8.0.0",
    "style-loader": "^1.0.0",
    "vue-loader": "^15.7.1",
    "vue-style-loader": "^4.1.2",
    "vue-template-compiler": "^2.6.10",
    "webpack": "^4.40.2",
    "webpack-cli": "^3.3.9",
    "webpack-dev-server": "^3.8.1",
    "webpack-merge": "^4.2.2"
  },
  "scripts": {
    "dev": "webpack-dev-server --open --config ./build/webpack.dev.conf.js",
    "build": "webpack --config ./build/webpack.build.conf.js"
  },
  "browserslist": [
    "> 1%",
    "last 3 version"
  ],
  "author": "serg_x",
  "license": "MIT",
  "dependencies": {
    "@fortawesome/fontawesome-svg-core": "^1.2.25",
    "@fortawesome/free-solid-svg-icons": "^5.11.2",
    "@fortawesome/vue-fontawesome": "^0.1.7",
    "@riophae/vue-treeselect": "^0.4.0",
    "axios": "^0.19.0",
    "buefy": "^0.8.4",
    "vue": "^2.6.10",
    "vue-router": "^3.1.3",
    "vuex": "^3.1.1"
  }
}
    
Скрыть

Описание решения

В этом описании я буду оперировать тем кодом, который представлен выше в конфигах.
В разделе Конфиги с решением вы найдете полный код.

Используемая по коду константа PATHS (она же baseWebpackConfig) Показать
const PATHS = {

  src_back: path.join(__dirname, '../src-back'),
  src_front: path.join(__dirname, '../src-front'),

  // Чанки для back. Вызываются в HtmlWebpackPlugin (в build и dev)
  back_chunks : ['common_vendors', 'buefy', 'back' ],
  front_chunks: ['common_vendors', 'front' ],


  dist: path.join(__dirname, '../docs'),
  assets: 'assets/',
}

    
Скрыть

Указываем две точки входа entry, и сделать динамическим имя для исхзодящих файлов output

  // взято из webpack.base.conf.js
  // Константа PATHS есть в спойлере в этой статье
  entry: {
    back: PATHS.src_back,
    front: PATHS.src_front,
  },
  output: {
    filename: `js/[name].js?v=[hash]`,
    path: PATHS.dist,
    publicPath: '/'
  },

Вычленяем из output код для чанков common_vendors и buefy

  • В common_vendors записываем то, что нужно везде.
  • В buefy попадают файлы библиотеки Buefy, которая нужная только в back.

Параметр test — Это регулярное выражение, которому должен соответствовать буть к файлу

optimization: {
  splitChunks: {
    cacheGroups: {
      common_vendors: {
        test: /[\\/]node_modules[\\/](vue|vue-router|vuex|axios|@riophae[\\/]vue-treeselect)[\\/]/,
        name: 'common_vendors', // имя чанка
        chunks: 'initial',
        enforce: true,
      },
      buefy: {
        test: /[\\/]node_modules[\\/](buefy)[\\/]/,
        name: 'buefy',
        chunks: 'initial',
        enforce: true,
      },
    }
  }
},

Добавить обработку двух шаблонов

Ключевой момент — настройка chunks. Тут мы определяем, какие чанки подгружаем в шаблон.

common_vendors и buefy мы определили на предыдущем этапе, а чанки back и front формируются какбы автоматически по имени точки входа, указанной в параметре entry

  // взято из webpack.dev.conf.js и webpack.build.conf.js
  // baseWebpackConfig (PATHS) есть в спойлере в этой статье
  new HtmlWebpackPlugin({
    template: `${baseWebpackConfig.externals.paths.src_back}/index.html`,
    filename: './index.html',
    title: "mode_development", // Нужно для костыльного условия в шаблоне
    inject: false,
    chunks: baseWebpackConfig.externals.paths.back_chunks, // ['common_vendors', 'buefy', 'back' ]
  }),
  
  // Обработка шаблона точки входа "front"
  new HtmlWebpackPlugin({
    template: `${baseWebpackConfig.externals.paths.src_front}/index.html`,
    filename: './pp.html',
    title: "mode_development",
    inject: false,
    chunks: baseWebpackConfig.externals.paths.front_chunks, // ['common_vendors', 'front' ]
  }),

Инъекция файлов в HTML (pug/шмаг) шаблон

В предыдущем пункте мы указали настройку inject: false, поэтому должны сами подключить скрипты в шаблон.

В принципе, это можно сделать и вручную, но давайте не будем так делать:)

  <!-- Так не делаем -->
  <script src="/js/common_vendors.js"></script>
  <script src="/js/buefy.js"></script>
  <script src="/js/back.js"></script>

В шаблоне мы можем вызвать конструкцию для подключения JS и CSS:

  <!-- Подгружаем CSS Файлы -->
  <% for (var css in htmlWebpackPlugin.files.css) { %>
  <link rel="stylesheet" href="<%= htmlWebpackPlugin.files.css[css] %>">
  <% } %>

  <div id="app">
    ...
  </div>
  
  <!-- Подгружаем JS Файлы -->
  <% for (var js in htmlWebpackPlugin.files.js) { %>
  <script src="<%= htmlWebpackPlugin.files.js[js] %>"></script>
  <% } %>

На этом все

Пожалуйста, напишите в комментариях, получилось ли у вас все настроить, и была ли понятна и корректна та информация, которую вы нашли в этой статье.

Комментарии (4)

  1. serega_taturin 13 августа 2020, 19:01 # 0
    Статья хорошая! Но есть один момент, что по дефолту в splitChunks при разделении вендоров на файлы, если один из модулей содержится в каком-либо чанке, то в другой он не будет помещен. Управлять этим можно с помощью priority. Возможно, есть какой то параметр для Optimization, чтобы менять поведение.
    1. Maxim 19 октября 2020, 21:47 # 0
      Мне статья понравилась. Прояснила ряд моментов.
      1. И 10 января 2021, 21:29 # 0
        В дебрях webpack настроек очень тяжело копаться, сама логика работы сборщика удивительно непонятна. Даже документация не стремится объяснить всё и разжевать. Было бы славно получить от автора поста разъяснения по этой теме. Например, разница между лоадерами и плагинами. Зачем нужны чанки, как их прописывать. Оптимизации и тому подобное. И всё с пояснением по каждой строчке. Я был бы очень счастлив однажды найти такую статью!
        1. Данил Данил 06 сентября 2022, 06:19 # 0
          Автор, для тебя отдельное место в раю! Лучше всего ютуба вместе взятогО!
          *Комментарий будет опубликован после проверки модератором

          Похожие статьи

          Наш сайт использует куки, нажмите «ОК» если вы не против
          OK