Настройка 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
          Автор, для тебя отдельное место в раю! Лучше всего ютуба вместе взятогО!
          *Комментарий будет опубликован после проверки модератором

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

          jQuery.Maskedinput js - документация на русском с примерами

          Маска для ввода телефона +7(___)___-__-__

          Отключить автозаполнение input

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

          Универсальная форма обратной связи — feedBackForm

          Слайдер Slick slider в контенте ресурса

          Как сделать переменную не реактивной в Vue

          Bxslider отображение картинок после полной загрузки слайдера

          Как обработать POST данные в PHP

          Примеры использования Vuex

          Выполнить код после асинхронного запроса axios во Vue

          Заготовки JavaScript

          Как вызвать метод из другого компонента Vue

          Связать значения инпутов через jQuery (биндинг)

          Рекурсивно вложенный компонент Vue

          Настроить Axios чтобы принимал только JSON

          Использование async/await в JavaScript с Vue.js

          Js-beautify - библиотека для форматирования HTML, CSS, JS

          Использование Promise.all с примерами на VueJs

          Создание цикла асинхронных вызовов во Vue.js

          Манипуляция с HTML во Vue.js и cash-dom

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