Исследование плагинов Webpack SplitChunksPlugin

Webpack

Первый взгляд на плагин SplitChunks

Начиная с Webpack4, плагин для разделения кода изменился с CommonsChunkPlugin на SplitChunksPlugin, и на него не нужно ссылаться отдельно, он интегрирован в Webpack и может управляться в конфигурации с помощью оптимизации.splitChunks и оптимизации.runtimeChunk.

Исследовательский опыт плагина

Два дня назад я изучал плагин CommonsChunkPlugin, и у меня появился опыт, то есть разобраться в этом плагине, если просто посмотреть как его настроить, то не поймешь.

Знать его дизайнерские идеи, научиться его настраивать.

Недостатки CommonsChunkPlugin

Схема разделения кода Webpack4 полностью заменила плагин, видимо будет большая разница в дизайнерских идеях и использовании, что на самом деле и есть.

Если это какое-то простое разделение повторяющегося кода, CommonsChunkPlugin подходит. Но для некоторых сложных сценариев CommonsChunkPlugin не подойдет.

Примеры сложных сценариев

Структура нашего проекта такова:

  • Есть входной файл: index.js
  • Три файла для асинхронной загрузки: Greeter1.js, Greeter2.js, Greeter3.js
  • Greeter1.js, ссылка Greeter2.js React.js, ссылка Greeter3.js Vue.js

Случай использования CommonsChunkPlugin

В настоящее время используйте Webpack для упаковки этого проекта.Если используется CommonsChunkPlugin, библиотеки React.js Vue.js будут упакованы в vendor.js.

Проблема с этим:

  1. У нас очень большой публичный кусок. Когда браузер загружает наш проект вверху страницы, он загружает запись Chunk index.js и общедоступный Chunk vendor.js. Это губительно сказывается на скорости загрузки первого экрана.
  2. Пользователь просто просматривает содержимое двух фрагментов, Greeter1.js и Greeter2.js, для него загрузка библиотеки, которая вообще не используется, такой как Vue.js, является пустой тратой трафика.

Идеально подходит для сплитов и вариантов использования

Вместо того, чтобы упаковывать React.js и Vue.js в один и тот же чанк поставщика, Webpack упаковывает React.js в поставщика~Greeter1~Greeter2.js посредством анализа и упаковывает Vue.js в поставщика~Greeter3.js, общедоступный код упакованы отдельно таким образом.

Затем, когда загружается первый экран, загружается только запись Chunk index.js. Когда пользователь просматривает Greeter1.js, загрузите Chunk Greeter1.js и Chunk vendor~Greeter1~Greeter2.js параллельно. При просмотре Greeter3.js загрузите Chunk Greeter3.js и Chunk vendor~Greeter3.js параллельно.

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

Решено с помощью SplitChunksPlugin

SplitChunksPlugin — это идеальный код разделения, который может справиться со сложной ситуацией разделения, описанной выше.

Создайте экспериментальный проект

Как описано выше, мы создаем новый файл со следующей структурой каталогов:

image

Содержимое файла следующее:

// index.js 
// 这样就是异步加载Greeter1、2、3 三个Chunk

import(/* webpackChunkName: "Greeter1" */'./Greeter1').then(module => {
  const greeter = module.default
  document.querySelector("#root").appendChild(greeter());
})

import(/* webpackChunkName: "Greeter2" */'./Greeter2').then(module => {
  const greeter = module.default
  document.querySelector("#root").appendChild(greeter());
})

import(/* webpackChunkName: "Greeter3" */'./Greeter3').then(module => {
  const greeter = module.default
  document.querySelector("#root").appendChild(greeter());
})
// Greeter1.js / Greeter2.js
// 我们就这样模拟引用了React。Greeter2.js和Greeter1.js长得一样。

import React from 'react';
console.log(React);

export const greeter = function () {
  var greet = document.createElement('div');
  greet.textContent = "Hi there and greetings!";
  return greet;
};
// Greeter3.js
// Greeter3中引用的是Vue

import Vue from 'vue';
console.log(Vue);

export const greeter = function () {
  var greet = document.createElement('div');
  greet.textContent = "Hi there and greetings!";
  return greet;
};

Давайте посмотрим на ключевую конфигурацию Webpack.

module.exports = {
  entry: {
    index: __dirname + "/app/index.js",
  },
  output: {
    path: __dirname + "/public",//打包后的文件存放的地方
    filename: "[name].js", //打包后输出文件的文件名
    chunkFilename: '[name].js',
  },
  mode: 'development',
  devtool: false,

  optimization: {
    splitChunks: {
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
        },
      }
    }
  },
}

Ключом является оптимизируемая конфигурация.Во-первых, мы сосредоточимся только на конфигурации поставщика в cacheGroups. Среди них test — правило разделения кода, означающее, что код в папке node_modules должен быть извлечен. Запустим Webpack и посмотрим на результат:

image

Я проверил файл vendor~Greeter1~Greeter2.js, который представляет собой код, упакованный React. Я проверил файл vendor~Greeter3.js, который содержит упакованный код библиотеки Vue.

Снова используя анализ упаковки, мы видим:

image

Это не идеальная ситуация, которую мы себе представляем. Если это CommonsChunkPlugin, то после настройки он поможет нам упаковать общедоступный фрагмент vendor.js, а для SplitChunksPlugin мы просто скажем ему извлечь файлы в node_modules, и Webpack автоматически разделит их в соответствии со ссылкой Project.public Chunk vendor~Greeter1~Greeter2.js и vendor~Greeter3.js

Резюме дизайнерских идей SplitChunksPlugin

Решите проблему разделения кода в сложных сценариях. Для разделения общедоступных модулей при асинхронной загрузке нам нужно только установить код, который необходимо упаковать публично, и SplitChunksPlugin автоматически поможет нам разделить общедоступный чанк на несколько небольших общедоступных чанков в соответствии с потребностями каждого асинхронного модуля. Используется всеми асинхронными модулями. И эти общедоступные чанки не будут загружаться выше сгиба, а будут загружаться параллельно с асинхронными модулями, которые их используют.

Основная идея: разделить код в соответствии с правилами, которые мы даем, а затем снова разделить для разделения общедоступного фрагмента.

Что делать, если слишком много разделенных фрагментов?

На данный момент все еще существует экстремальная ситуация, то есть слишком много публичных чанков, которые были разделены. Первоначальным намерением Webpack было объединить код, который снова не работает.

Проблема, вызванная слишком большим количеством фрагментов, заключается в том, что браузеру необходимо одновременно запрашивать слишком много js.

Тот же SplitChunksPlugin разобрался за нас.

Проведем еще один эксперимент.

Примеры сложных сценариев:

  • Есть входной файл: index.js
  • Есть четыре файла для асинхронной загрузки: Greeter1.js, Greeter2.js, Greeter3.js, Greeter4.js.
  • Есть три файла, на которые ссылаются указанные выше четыре файла: helper1.js, helper2.js, helper3.js (обратите внимание, что helper*.js здесь должен быть больше 30 КБ, если он слишком мал, Webpack не извлечет его из Кусок).
  • Greeter1.js относится к helper1.js, Greeter2.js относится к helper2.js, Greeter3.js относится к helper3.js, а Greeter4.js относится к helper1.js, helper2.js и helper3.js одновременно.

Я рисую картинку, чтобы проиллюстрировать ситуацию:

image
G1 означает Greeter1.js, а h1 — helper.js. Так далее и тому подобное. Такие ссылки делают модули helper1-3 общедоступными. Нам нужно извлечь его из Chunk. Скомпилируйте с помощью Wepback и посмотрите результат:

image

Как вы и ожидали, есть три публичных чанка, то есть публичная часть нашей картинки, которая содержит коды header1~3 соответственно. Посмотрите на более интуитивные рендеры:

image

Проблемы с Greeter4.js

Когда браузер загружает Greeter4.js, ему необходимо одновременно загрузить три блока: default~Greeter1~Greeter4.js, default~Greeter2~Greeter4.js и default~Greeter3~Greeter4.js. То есть, когда пользователь видит Greeter4.js, ему нужно параллельно запросить 4 файла js.

Проблема в том, что мы слишком мелко дизассемблируем публичный пакет, может показаться, что при загрузке асинхронного чанка нам нужно запрашивать много публичных чанков одновременно, а это не то, что мы хотим видеть.По этой причине SplitChunksPlugin предоставляет нам атрибут maxAsyncRequests, который ограничивает максимальное количество параллельных запросов.

Текущее максимальное количество параллельных запросов при загрузке Greeter4.js равно 4. Давайте установим его на 3 и посмотрим, каков эффект:

optimization: {
    splitChunks: {
      maxAsyncRequests: 3, // 在此设置
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  },

Запустив webpack, эффект следующий:

image

Greeter4 помещает helper3.js в Chunk, а затем отдельно упаковывает helper1 и helper2, так что количество параллельных запросов Greeter4 равно 3, что соответствует ожиданиям.

Точно так же на helper3.js также ссылается Greeter3.js, поэтому он также упакован в Greeter3, что приводит к повторной упаковке helper3. Чтобы уменьшить количество запросов, это приведет к определенной степени повторной упаковки.Что нам нужно сделать, так это сбалансировать количество запросов и скорость повторной упаковки с помощью конфигурации.

В общем, SplitChunksPlugin еще очень шустрый, мы просто делаем запрос (количество параллельных запросов должно быть меньше или равно 3), и он будет распаковывать и объединять пакеты для нас исходя из этого условия.

Конфигурация SplitChunksPlugin по умолчанию

Даже если мы не будем писать оптимизацию, Webpack поможет нам с разбиением кода, что эквивалентно написанию следующей конфигурации:

splitChunks: {
    chunks: "async", // 默认只处理异步chunk的配置
    minSize: 30000, // 如果模块的最小体积小于30,就不拆分它
    minChunks: 1, // 模块的最小被引用次数
    maxAsyncRequests: 5, // 异步加载Chunk时的最大并行请求数
    maxInitialRequests: 3, // 入口Chunk的最大并行请求数
    automaticNameDelimiter: '~', // 文件名的连接符
    name: true, // 此处写成false,公共块就不会是default~Greeter1~Greeter4.js了,而是0.js这样命名Chunk。
    cacheGroups: { // 缓存组,拆分Chunk的规则
        vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10, // 此数越大,越优先匹配
        },
        default: {
            minChunks: 2, // CommonsChunkPlugin的minChunks既可以传方法,也可以传数字,现在只可以传数字了,如果你想传方法,用test属性
            priority: -20,
            reuseExistingChunk: true //  配置项允许重用已经存在的代码块而不是创建一个新的代码块。这句我不懂,有知道的小伙伴麻烦告诉我一下
        }
    }
}

Конфигурацию по умолчанию можно увидеть, только асинхронно загруженный чанк эффективен, потому что конфигурация чанков: «асинхронная».

Ниже приводится описание конфигурации по умолчанию:

  • Блоки общего кода или из папки node_modules
  • Разделенный блок кода должен быть больше 30 КБ (до min+giz).
  • Количество запросов на загрузку блоков кода по запросу должно быть
  • Количество запросов на загрузку блоков кода при инициализации страницы должно быть

Эти описания соответствуют какой конфигурации выше, думаю всем понятно. Без анализа эти описания действительно сбивают с толку.

maxInitialRequests

Мы еще не объяснили поле maxInitialRequests, так как имя поля должно быть инициализировано, то есть разделение записи Chunk, поэтому я сделал следующую конфигурацию:

module.exports = {
  entry: {
    Greeter1: __dirname + "/app/Greeter1.js",
    Greeter2: __dirname + "/app/Greeter2.js",
    Greeter3: __dirname + "/app/Greeter3.js",
    Greeter4: __dirname + "/app/Greeter4.js",
  },
  output: {
    path: __dirname + "/public",//打包后的文件存放的地方
    filename: "[name].js", //打包后输出文件的文件名
    chunkFilename: '[name].js',
  },
  mode: 'development',
  devtool: false,

  optimization: {
    splitChunks: {
      chunks: "initial", // 默认只处理异步chunk的配置
      maxInitialRequests: 3, // 一个入口最大并行请求数
      cacheGroups: { // 缓存组
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  },
}

Давайте посмотрим на эффект упаковки:

image

Я установил maxInitialRequests на 5, а затем посмотрел на эффект упаковки:

image

Результат упаковки согласуется с нашим анализом стратегии асинхронного извлечения фрагментов.При ограничении 5, даже если максимальное количество Greeter4.js и количество запросов равно 4, вы можете распаковывать столько, сколько хотите. Но когда ограничение равно 3, Webpack не будет выделять helper3.js в общедоступный фрагмент, а упаковывает его в Greeter4.js и Greeter3.js, которые ссылаются на него, чтобы ограничить загрузку записи Greeter4. , параллельный запрос это 3. Допустим, maxInitialRequests maxAsyncRequests, чтобы ограничить количество распаковок для нескольких записей.

Разделить код времени выполнения

Сказав так много, я еще не упомянул о разделении среды выполнения. Среде выполнения SplitChunksPlugin нужно настроить только одно свойство, как показано ниже:

 optimization: {
    runtimeChunk: true,
    splitChunks: {
      chunks: "initial", // 默认只处理异步chunk的配置
      minSize: 30000, // 如果模块的最小体积小于30,就不拆分它
      minChunks: 1, // 模块的最小被引用次数
      maxAsyncRequests: 5, // 按需加载的最大并行请求数
      maxInitialRequests: 5, // 一个入口最大并行请求数
      automaticNameDelimiter: '~', // 文件名的连接符
      name: true,
      cacheGroups: { // 缓存组
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  },

Я по-прежнему использую здесь предыдущий пример, и результаты упаковки выглядят следующим образом:

image

Очень интересно, для наших четырех входных файлов было сгенерировано четыре файла, время выполнения ~Greeter1 to 4. Это также соответствует ожиданиям.Код используемой записи также загрузит соответствующий файл времени выполнения.

Вернемся к началу

Теперь давайте вернемся к исходному простому примеру и закончим наше исследование на сегодня.

основная сцена

Не рассматривайте асинхронную загрузку модулей, просто разделите бизнес-код, код сторонних библиотек и код среды выполнения.

настроить

Входной файл index.js реагирует только на ссылки. Конфигурация выглядит следующим образом:


module.exports = {
  entry: {
    index: __dirname + "/app/index.js",
  },
  output: {
    path: __dirname + "/public",//打包后的文件存放的地方
    filename: "[name].js", //打包后输出文件的文件名
    chunkFilename: '[name].js',
  },
  mode: 'development',
  devtool: false,

  optimization: {
    runtimeChunk: true,
    splitChunks: {
      chunks: "initial", 
      automaticNameDelimiter: '~',  
      name: true,
      cacheGroups: { // 缓存组
        vendors: {
          test: /[\\/]node_modules[\\/]/,
        },
      }
    }
  },
}

Результаты упаковки

image

Мы видим, что react — это сторонняя библиотека, извлеченная в vendors~index.js, код среды выполнения, извлеченный в runtime-index.js, бизнес-код — index.js.

заключительные замечания

Официальная документация Webpack не так ясна, как объяснение. Изучение Webpack требует много практических занятий. На практике это помогает нам изучить и испытать Webpack.

SplitChunksPlugin немного сложнее для понимания, и он предназначен для обработки сложных ситуаций разделения. Но разобравшись с его принципом, я обнаружил, что он по-прежнему очень мощный, с несколькими конфигурациями можно выполнить разбиение кода в сложных ситуациях.