Spec-Zone.ru › webpack 5

Разделение кода

подсказка

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

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

Существует три основных подхода к разделению кода:

  • Точки входа: Ручное разделение кода с помощью конфигурации entry.
  • Предотвращение дублирования: Используйте зависимости входа Зависимости входа или SplitChunksPlugin для дедупликации и разделения фрагментов.
  • Динамические импорты: Разделение кода с помощью вызовов функций встроеных в модули.

Точки входа

Это, пожалуй, самый простой и интуитивно понятный способ разделения кода. Однако он более ручный и имеет некоторые недостатки, которые мы рассмотрим. Давайте посмотрим, как мы можем разделить другой модуль из основного пакета:

project

webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
+ |- another-module.js
|- /node_modules

another-module.js

import _ from 'lodash';

console.log(_.join(['Another', 'module', 'loaded!'], ' '));

webpack.config.js

 const path = require('path');

 module.exports = {
-  entry: './src/index.js',
+  mode: 'development',
+  entry: {
+    index: './src/index.js',
+    another: './src/another-module.js',
+  },
   output: {
-    filename: 'main.js',
+    filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
 };

Это приведет к следующему результату сборки:

...
[webpack-cli] Compilation finished
asset index.bundle.js 553 KiB [emitted] (name: index)
asset another.bundle.js 553 KiB [emitted] (name: another)
runtime modules 2.49 KiB 12 modules
cacheable modules 530 KiB
  ./src/index.js 257 bytes [built] [code generated]
  ./src/another-module.js 84 bytes [built] [code generated]
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 245 ms

Как уже упоминалось, этот подход имеет некоторые недостатки:

  • Если между фрагментами входа есть какие-либо дублированные модули, они будут включены в оба пакета.
  • Он не такой гибкий и не может использоваться для динамического разделения кода с основной логикой приложения.

Первый из этих двух пунктов определенно является проблемой для нашего примера, так как lodash также импортируется в ./src/index.js и, следовательно, будет дублироваться в обоих пакетах. Давайте устраним это дублирование в следующем разделе.

Предотвращение дублирования

Зависимости входа

Опция dependOn позволяет совместно использовать модули между фрагментами:

webpack.config.js

 const path = require('path');

 module.exports = {
   mode: 'development',
   entry: {
-    index: './src/index.js',
-    another: './src/another-module.js',
+    index: {
+      import: './src/index.js',
+      dependOn: 'shared',
+    },
+    another: {
+      import: './src/another-module.js',
+      dependOn: 'shared',
+    },
+    shared: 'lodash',
   },
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
 };

Если мы собираемся использовать несколько точек входа на одной HTML-странице, optimization.runtimeChunk: 'single' также необходимо, в противном случае мы могли столкнуться с проблемами, описанными здесь.

webpack.config.js

 const path = require('path');

 module.exports = {
   mode: 'development',
   entry: {
     index: {
       import: './src/index.js',
       dependOn: 'shared',
     },
     another: {
       import: './src/another-module.js',
       dependOn: 'shared',
     },
     shared: 'lodash',
   },
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
+  optimization: {
+    runtimeChunk: 'single',
+  },
 };

И вот результат сборки:

...
[webpack-cli] Compilation finished
asset shared.bundle.js 549 KiB [compared for emit] (name: shared)
asset runtime.bundle.js 7.79 KiB [compared for emit] (name: runtime)
asset index.bundle.js 1.77 KiB [compared for emit] (name: index)
asset another.bundle.js 1.65 KiB [compared for emit] (name: another)
Entrypoint index 1.77 KiB = index.bundle.js
Entrypoint another 1.65 KiB = another.bundle.js
Entrypoint shared 557 KiB = runtime.bundle.js 7.79 KiB shared.bundle.js 549 KiB
runtime modules 3.76 KiB 7 modules
cacheable modules 530 KiB
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
  ./src/another-module.js 84 bytes [built] [code generated]
  ./src/index.js 257 bytes [built] [code generated]
webpack 5.4.0 compiled successfully in 249 ms

Как вы видите, помимо shared.bundle.js, index.bundle.js и another.bundle.js был сгенерирован еще один файл runtime.bundle.js.

Хотя использование нескольких точек входа на странице разрешено в webpack, его следует избегать, когда это возможно, в пользу точки входа с несколькими импортами: entry: { page: ['./analytics', './app'] }. Это приводит к лучшей оптимизации и согласованному порядку выполнения при использовании тегов async.

SplitChunksPlugin

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

webpack.config.js

  const path = require('path');

  module.exports = {
    mode: 'development',
    entry: {
      index: './src/index.js',
      another: './src/another-module.js',
    },
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
+   optimization: {
+     splitChunks: {
+       chunks: 'all',
+     },
+   },
  };

С опцией конфигурации optimization.splitChunks, теперь мы должны увидеть, что дублируемая зависимость удалена из наших index.bundle.js и another.bundle.js. Плагин должен заметить, что мы отделили lodash в отдельный фрагмент и удалили избыточность из нашего основного пакета. Однако важно отметить, что общие зависимости извлекаются только в отдельный фрагмент, если они соответствуют порогам размера, заданным webpack, в пороговые значения размера.

Давайте проведем npm run build , чтобы проверить, сработало ли это:

...
[webpack-cli] Compilation finished
asset vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB [compared for emit] (id hint: vendors)
asset index.bundle.js 8.92 KiB [compared for emit] (name: index)
asset another.bundle.js 8.8 KiB [compared for emit] (name: another)
Entrypoint index 558 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB index.bundle.js 8.92 KiB
Entrypoint another 558 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB another.bundle.js 8.8 KiB
runtime modules 7.64 KiB 14 modules
cacheable modules 530 KiB
  ./src/index.js 257 bytes [built] [code generated]
  ./src/another-module.js 84 bytes [built] [code generated]
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 241 ms

Вот некоторые другие полезные плагины и загрузчики, предоставленные сообществом для разделения кода:

  • mini-css-extract-plugin: Полезно для разделения CSS из основного приложения.

Динамические импорты

Webpack поддерживает две похожие техники, когда речь идет о динамическом разделении кода. Первый и рекомендуемый подход заключается в использовании import() синтаксиса, который соответствует предложению ECMAScript для динамических импортов. Старый, специфичный для webpack подход заключается в использовании require.ensure. Давайте попробуем использовать первый из этих двух подходов...

предупреждение

import() вызовы используют обещания в режиме внутренней работы. Если вы используете import() со старыми браузерами (например, IE 11), не забудьте подменить Promise с помощью такого полифилла, как es6-promise или promise-polyfill.

Прежде чем начать, давайте удалим лишние entry и optimization.splitChunks из нашей конфигурации в приведенном выше примере, так как они нам не понадобятся для следующей демонстрации:

webpack.config.js

 const path = require('path');

 module.exports = {
   mode: 'development',
   entry: {
     index: './src/index.js',
-    another: './src/another-module.js',
   },
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
-  optimization: {
-    splitChunks: {
-      chunks: 'all',
-    },
-  },
 };

Мы также обновим наш проект, чтобы удалить теперь неиспользуемые файлы:

project

webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
- |- another-module.js
|- /node_modules

Теперь, вместо статического импорта lodash, мы будем использовать динамический импорт для разделения фрагмента:

src/index.js

-import _ from 'lodash';
-
-function component() {
+function getComponent() {
-  const element = document.createElement('div');

-  // Lodash, now imported by this script
-  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+  return import('lodash')
+    .then(({ default: _ }) => {
+      const element = document.createElement('div');
+
+      element.innerHTML = _.join(['Hello', 'webpack'], ' ');

-  return element;
+      return element;
+    })
+    .catch((error) => 'An error occurred while loading the component');
 }

-document.body.appendChild(component());
+getComponent().then((component) => {
+  document.body.appendChild(component);
+});

Причина, по которой нам нужен default, заключается в том, что с версии webpack 4 при импорте модуля CommonJS импорт больше не будет разрешаться в значение module.exports, а вместо этого будет создан искусственный объект пространства имен для модуля CommonJS. Для получения дополнительной информации о причинах этого, прочтите webpack 4: import() и CommonJs.

Запустим webpack, чтобы увидеть lodash отделённым в отдельный пакет:

...
[webpack-cli] Compilation finished
asset vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB [compared for emit] (id hint: vendors)
asset index.bundle.js 13.5 KiB [compared for emit] (name: index)
runtime modules 7.37 KiB 11 modules
cacheable modules 530 KiB
  ./src/index.js 434 bytes [built] [code generated]
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 268 ms

Так как import() возвращает обещание, его можно использовать с async функциями. Вот как это упростило бы код:

src/index.js

-function getComponent() {
+async function getComponent() {
+  const element = document.createElement('div');
+  const { default: _ } = await import('lodash');

-  return import('lodash')
-    .then(({ default: _ }) => {
-      const element = document.createElement('div');
+  element.innerHTML = _.join(['Hello', 'webpack'], ' ');

-      element.innerHTML = _.join(['Hello', 'webpack'], ' ');
-
-      return element;
-    })
-    .catch((error) => 'An error occurred while loading the component');
+  return element;
 }

 getComponent().then((component) => {
   document.body.appendChild(component);
 });
подсказка

Можно предоставить динамическое выражение в import() случае необходимости импорта конкретного модуля, основанного на вычисленной переменной позднее.

Предварительная загрузка/Предварительная загрузка модулей

Webpack 4.6.0+ добавляет поддержку предварительной загрузки и предварительной загрузки.

Использование этих встроенных директив при объявлении импортов позволяет webpack выводить «указатели ресурсов», которые сообщают браузеру, что для:

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

Например, у нас есть компонент HomePage, который отображает компонент LoginButton, а затем по требованию загружает компонент LoginModal после нажатия.

LoginButton.js

//...
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');

Это приведет к тому, что <link rel="prefetch" href="login-modal-chunk.js"> будет добавлен в заголовок страницы, что укажет браузеру на предварительную загрузку файла login-modal-chunk.js в режиме ожидания.

подсказка

webpack добавит подсказку предварительной загрузки, как только родительский фрагмент будет загружен.

Директива предварительной загрузки имеет ряд различий по сравнению с предварительной загрузкой:

  • Загруженный по запросу фрагмент начинает загружаться параллельно с родительским фрагментом. Предварительно загруженный фрагмент запускается после завершения загрузки родительского фрагмента.
  • Загруженный по запросу фрагмент имеет средний приоритет и немедленно загружается. Предварительно загруженный фрагмент загружается, когда браузер простаивает.
  • Фрагмент, загруженный по запросу, должен быть немедленно запрошен родительским фрагментом. Предварительно загруженный фрагмент можно использовать в любое время в будущем.
  • Поддержка браузеров отличается.

Пример этого может быть компонент Component, который всегда зависит от большой библиотеки, которая должна быть в отдельном фрагменте.

Представим компонент ChartComponent, которому необходима большая библиотека ChartingLibrary. Он отображает LoadingIndicator при отрисовке и сразу же выполняет импорт по требованию ChartingLibrary.

ChartComponent.js

//...
import(/* webpackPreload: true */ 'ChartingLibrary');

Когда запрашивается страница, использующая ChartComponent, фрагмент charting-library также запрашивается через <link rel="preload">. При предположении, что фрагмент страницы меньше и загружается быстрее, страница будет отображена с LoadingIndicator, пока не завершится уже запрошенный charting-library-chunk. Это немного ускорит время загрузки, так как требуется только один обмен, а не два. Особенно в средах с высокой задержкой.

подсказка

Неправильное использование webpackPreload может на самом деле навредить производительности, поэтому будьте осторожны при его использовании.

Иногда вам нужно иметь собственный контроль над предварительной загрузкой. Например, предварительная загрузка любого динамического импорта может быть выполнена с помощью асинхронного скрипта. Это может быть полезно в случае потоковой передачи рендеринга на стороне сервера.

const lazyComp = () =>
  import('DynamicComponent').catch((error) => {
    // Do something with the error.
    // For example, we can retry the request in case of any net error
  });

Если загрузка скрипта завершится ошибкой до того, как webpack начнет загрузку этого скрипта самостоятельно (webpack создает тег script для загрузки его кода, если этот скрипт не находится на странице), обработчик ошибок не запустится, пока не будет достигнут chunkLoadTimeout. Такое поведение может быть неожиданным. Но это объяснимо — webpack не может выдать ошибку, так как не знает, что скрипт завершился ошибкой. Webpack добавит обработчик ошибок в скрипт сразу после возникновения ошибки.

Чтобы предотвратить такую проблему, вы можете добавить собственный обработчик ошибок, который удаляет скрипт в случае возникновения ошибки:

<script
  src="https://example.com/dist/dynamicComponent.js"
  async
  onerror="this.remove()"
></script>

В этом случае ошибочный скрипт будет удален. Webpack создаст свой собственный скрипт, и любая ошибка будет обработана без каких-либо таймаутов.

Анализ пакета

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

  • webpack-chart: Интерактивная круговая диаграмма для статистики webpack.
  • webpack-visualizer: Визуализируйте и проанализируйте свои пакеты, чтобы увидеть, какие модули занимают много места и какие могут быть дубликатами.
  • webpack-bundle-analyzer: Плагин и утилита командной строки, представляющая содержимое пакета в удобном интерактивном масштабируемом древовидном отображении.
  • webpack bundle optimize helper: Этот инструмент проанализирует ваш пакет и предложит конкретные рекомендации по улучшению, чтобы уменьшить его размер.
  • bundle-stats: Создайте отчет о пакете (размер пакета, активы, модули) и сравните результаты между различными сборками.
  • webpack-stats-viewer: Плагин со сборкой для статистики webpack. Показать больше информации о деталях пакета webpack.

Следующие шаги

См. Ленивую загрузку для более конкретного примера использования import() в реальном приложении и Кэширование, чтобы узнать, как эффективнее разделить код.

© JS Foundation and other contributors
Licensed under the Creative Commons Attribution License 4.0.
https://webpack.js.org/guides/code-splitting

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API