Оценка начала работы с Webpack5

внешний интерфейс Webpack

Вы, должно быть, видели много тестов электронных устройств, сегодня мы также проведем практическую оценку новой версии программного обеспечения - Webpack 5!

С 2017 года, когда было объявлено голосование по v5, до первой бета-версии в октябре 2019 года, в настоящее время 5.0.0-beta.16. Сейчас, в процессе сбора отзывов пользователей и экологического обновления, я думаю, что оно скоро будет официально выпущено. этот разОбновление фокуса: 性能改进、Tree Shaking、Code Generation、Module Federation。

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

Оптимизировать постоянный кеш

Сначала кратко поговорим о концепции графа в Webpack:

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

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

Основное содержимое в кеше (в виде памяти/диска) — это объекты модуля, а при компиляции граф будет храниться на жестком диске в виде бинарного или json-файла. Всякий раз, когда изменяется код, меняются зависимости между модулями и меняется график, Webpack считывает записи и выполняет инкрементную компиляцию.

Кэш можно настроить с помощью загрузчика до:

  1. использоватьcache-loaderРезультат компиляции можно записать в кеш жесткого диска.При повторной сборке Webpack, если файл не изменился, он напрямую извлечет кеш
  2. Есть также некоторые загрузчики с собственной конфигурацией кеша, такие какbabel-loader, настраиваемые параметрыcacheDirectoryИспользуйте кеш для записи каждого результата компиляции на диск (по умолчанию в каталог node_modules/.cache/babel-loader)

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

module.exports = {
  cache: {
    type: 'filesystem',
    // cacheDirectory 默认路径是 node_modules/.cache/webpack
    cacheDirectory: path.resolve(__dirname, '.temp_cache')
  }
};

Примечание: для большинстваnode_modulesХеширование для построения зависимостей обходится дорого и замедляет выполнение Webpack. Чтобы избежать этой ситуации, Webpack добавил некоторые оптимизации, которые по умолчанию пропущены.node_modulesи использоватьpackage.jsonсерединаversionа такжеnameв качестве источника данных.

Оптимизируйте долгосрочное кэширование

Цели Webpack 5moduleIdа такжеchunkIdОптимизирован метод расчета, добавлена ​​детерминированная стратегия генерации moduleId и chunkId. moduleId вычисляется в соответствии с путем контекстного модуля, chunkId вычисляется в соответствии с содержимым блока, и, наконец, для moduleId и chunkId генерируется 3-4-значный идентификатор для реализации долгосрочного кэширования, которое включено по умолчанию в производственной среде.

  1. Сравните исходный moduleId

Исходное значение по умолчанию для moduleId — это автоматически увеличивающийся идентификатор, что может легко привести к аннулированию файлового кэша. До v4 можно было установитьHashedModuleIdsPluginПлагин переопределяет правило moduleId по умолчанию, которое использует хэш, сгенерированный путем к модулю, в качестве moduleId. В v4 можно настроитьoptimization.moduleIds = 'hashed'

  1. Сравните исходный chunkId

Значение по умолчанию исходного chunkId увеличивается на id. Например, в такой конфигурации при добавлении новой записи количество чанков также будет увеличиваться, а также увеличится chunkId. можно установить раньшеNamedChunksPluginплагин для стабилизации chunkId или настроитьoptimization.chunkIds = 'named'

Скрипт NodeJS polyfill удален.

Изначально целью Webpack было разрешить запуск модулей Node в браузере. Но теперь, по мнению Webpack, большинство модулей разрабатываются специально для внешнего интерфейса. В v4 и предыдущих версиях скрипты полифилла автоматически добавляются в большинство модулей Node, а полифиллы добавляются в окончательный бандл, что обычно не требуется. Это поведение будет прекращено в v5.

Например следующий код:

// index.js
import sha256 from 'crypto-js/sha256';
 
const hashDigest = sha256('hello world');
console.log(hashDigest);

В v4 активно добавляетсяcryptoполифилл, то естьcrypto-browserify. Код, который мы запущены, не требуется, но окончательный пакет становится больше, а результат компиляции417 kb:

В v5, если это так, вам будет предложено подтвердить. Если вы подтвердите, что вам не нужен полифилл узла, следуйте инструкциям, чтобы установить для псевдонима значение false. Конечным результатом компиляции является только5.69 kb:

настроитьresolve.alias: { crypto: false }:

Результат выполнения браузера:

Лучшее встряхивание дерева

Теперь есть этот кусок кода:

// inner.js
export const a = 'aaaaaaaaaa';
export const b = 'bbbbbbbbbb';

// module.js
import * as inner from "./inner";
export { inner };

// index.js
import * as module from "./module";
console.log(module.inner.a);

В v4 нет сомнений, что все переменные a и b приведенного выше кода упакованы:

но мы только что позвонилиaпеременная, в идеале она должна бытьbопределяется какunused, не упакован. Эта оптимизация была реализована в v5. Модули будут проанализированы в v5exportа такжеimportЗависимости между окончательным генерированием кода очень лаконичны:

существенное изменение

Если вышеперечисленные изменения и оптимизации являются обычными способами, то следующие функции немного неожиданны.

Module Federation

Позвольте Webpack достичь эффекта онлайн-среды выполнения, позволяя напрямую обмениваться кодом между независимыми приложениями с помощью CDN, больше не нужно устанавливать пакет NPM локально, создавать и публиковать его!

Первоначальный замысел дизайна

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

Прежде чем мы сможем надеяться, это как поделиться кодом?

NPM

Поддерживайте NPM-пакет CommonComponents для установки и использования в различных проектах. Если пакет NPM обновлен, соответствующий проект должен установить новую версию, скомпилировать ее локально и упаковать в пакет.

UMD

Преимущество UMD во время выполнения. Недостатки также очевидны, объемная оптимизация неудобна, возможны конфликты версий.

микро интерфейс

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

  1. Подприложения упаковываются независимо, а модули разъединяются, но общедоступные зависимости нелегко поддерживать и обрабатывать.
  2. Все приложение упаковано вместе, что может решить общие зависимости, однако большое количество проектов делает упаковку медленной, и ее нелегко расширять в будущем.

Webpack 5 реализует совершенно новое решение

Как видно из рисунка, эта схема заключается в прямом использовании связки одного приложения с другим.

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

Теория более абстрактна, давайте попробуем.

практический тест

Теперь есть два приложенияapp1(локальный хост: 3001),app2(локальный хост: 3002):

Входной файл:

// app1 & app2: index.js
import App from "./App";
import React from "react";
import ReactDOM from "react-dom";

ReactDOM.render(<App />, document.getElementById("root"));

app2произведеноButtonКомпоненты:

// app2: Button.js
import React from "react";

const Button = () => <button>App 2 Button</button>;

export default Button;

app2собственное потреблениеButtonКомпоненты:

// app2: App.js
import LocalButton from "./Button";
import React from "react";

const App = () => (
  <div>
    <h1>Basic Host-Remote</h1>
    <h2>App 2</h2>
    <LocalButton />
  </div>
);

export default App;

app1Цитироватьapp2изButtonКомпоненты:

// app1: App.js
import React from "react";
const RemoteButton = React.lazy(() => import("app2/Button"));

const App = () => (
  <div>
    <h1>Basic Host-Remote</h1>
    <h2>App 1</h2>
    <React.Suspense fallback="Loading Button">
      <RemoteButton />
    </React.Suspense>
  </div>
);

export default App;

Посмотрим на производствоButtonкомпонентapp2, его конфигурационный файл:

// app2: webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { ModuleFederationPlugin } = require("webpack").container;
const path = require("path");

module.exports = {
  entry: "./src/index",
  mode: "development",
  devServer: {
    contentBase: path.join(__dirname, "dist"),
    port: 3002,
  },
  output: {
    publicPath: "http://localhost:3002/",
  },
  module: {
    rules: [
      // ...
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: "app2Lib",
      library: { type: "var", name: "app2Lib" },
      filename: "app2-remote-entry.js",
      exposes: {
        Button: "./src/Button",
      },
      shared: ["react", "react-dom"],
    }),
    new HtmlWebpackPlugin({
      template: "./index.html",
    }),
  ],
};

Эта конфигурация описывает необходимость выставитьButtonкомпоненты, зависимостиreact,react-dom. управлятьexposesа такжеsharedМодульapp2Lib, сгенерированный файл записи называетсяapp-remote-entry.js.

app1файл конфигурации:

const HtmlWebpackPlugin = require("html-webpack-plugin");
const { ModuleFederationPlugin } = require("webpack").container;
const path = require("path");

module.exports = {
  entry: "./src/index",
  mode: "development",
  devServer: {
    contentBase: path.join(__dirname, "dist"),
    port: 3001,
  },
  output: {
    publicPath: "http://localhost:3001/",
  },
  module: {
    rules: [
      // ...
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: "app1",
      library: { type: "var", name: "app1" },
      remotes: {
        app2: "app2Lib",
      },
      shared: ["react", "react-dom"],
    }),
    new HtmlWebpackPlugin({
      template: "./index.html",
    }),
  ],
};

Эта конфигурация описывает использование удаленного модуляapp2Lib,полагатьсяreact,react-dom.

последний шаг: существуетapp1загрузить в htmlapp2-remote-entry.js:

// app1: index.html
<html>
  <head>
    <script src="http://localhost:3002/app2-remote-entry.js"></script>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

результат операции:

цитируетсяapp2/ButtonКак ты это нашел?

пройти черезapp1конфигурационный файл, знайapp2является удаленной нагрузкой. в сгенерированномapp1 main.jsописанный как:

посмотреть здесьdataмножество:

data[1]которыйwebpack/container/reference/app2, вот возвратapp2LibОбъект:

module.exports = app2Lib;

data[0]которыйwebpack/container/remote-overrides/a46c3e, который представлен здесьapp2потребностиreact,react-domзависит от и возвращаетсяapp2Lib:

module.exports = (external) => {
  if (external.override) {
    external.override(Object.assign({
      "react": () => {
        return Promise.resolve().then(() => {
          return () => __webpack_require__(/*! react */ "./node_modules/react/index.js")
        })
      },
      "react-dom": () => {
        return Promise.resolve().then(() => {
          return () => __webpack_require__(/*! react-dom */ "./node_modules/react-dom/index.js")
        })
      }
    }, __webpack_require__.O))
  }
  return external;
};

Итак, наконецpromiseЗадание становится:

var promise = app2Lib.get('Button');

Глядя на это таким образом,app2LibЭто глобальная переменная.

Продолжай читатьapp1загруженapp2-remote-entry.jsсодержание. Конечно, достаточно, глобальная переменная была сгенерированаapp2Lib:

app2LibОбъект имеет два метода, а именно:

var get = (module) => {
  return (
    __webpack_require__.o(moduleMap, module)
      ? moduleMap[module]()
      : Promise.resolve().then(() => {
        throw new Error('Module \"' + module + '\" does not exist in container.');
      })
  );
};

var override = (override) => {
  Object.assign(__webpack_require__.O, override);
};

так,app2/Buttonфактическиapp2Lib.get('Button'), затем найти модуль по отображению, затем__webpack_require__:

var moduleMap = {
  "Button": () => {
    return __webpack_require__.e("src_Button_js").then(() => 
      () => __webpack_require__(/*! ./src/Button */ "./src/Button.js")
    );
  }
};

Наконец сказатьshared: ['react', 'react-dom']:

app2Указывает на необходимость полагаться наreact,react-dom, и ожидайте, что приложение-потребитель предоставит его. еслиapp1Если указанная версия не указана или указанная версия не указана, код комментируется следующим образом:

plugins: [
  new ModuleFederationPlugin({
    name: "app1",
    library: { type: "var", name: "app1" },
    remotes: {
      'app2': "app2Lib",
    },
    // shared: ["react", "react-dom"],
    // 版本不一致同理
    // shared: {
    //   "react-15": "react",
    //   "react-dom": "react-dom",
    // },
  }),
  new HtmlWebpackPlugin({
    template: "./index.html",
  }),
]

Ну, только сейчасapp1 main.jsсерединаdata[0]которыйwebpack/container/remote-overrides/a46c3eстанет:

module.exports = (external) => {
  if (external.override) {
    external.override(__webpack_require__.O);
    // external.override(Object.assign({
    //   "react": () => {
    //     return Promise.resolve().then(() => {
    //       return () => __webpack_require__(/*! react */ "./node_modules/react/index.js")
    //     })
    //   },
    //   "react-dom": () => {
    //     return Promise.resolve().then(() => {
    //       return () => __webpack_require__(/*! react-dom */ "./node_modules/react-dom/index.js")
    //     })
    //   }
    // }, __webpack_require__.O))
  }
  return external;
};

app1затем изapp2нагрузкаreactполагаться:

резюме, согласноapp2настроенexposes & sharedконтент, генерировать соответствующие файлы модулей и отношения сопоставления модулей с помощью глобальных переменныхapp2Libполучить доступ;app1Глобальная переменнаяgetзнать, как загрузитьbutton.js,overrideМодуль, который знает об общих зависимостях.

Выше на первый взгляд Federation выглядит как DLL + External, но преимущество в том, что вам не нужно вручную поддерживать и упаковывать зависимости, а код загружается во время выполнения. В этом режиме отладка также проста, нет необходимости копировать-вставлять код илиnpm link, просто запустите приложение. Только здесьButtonКомпонент в качестве примера,ButtonЭто может быть компонент, страница или приложение. Внедрение федерации модулей в сочетании с рядом задач, таких как автоматизированные процессы, также требует от каждого попрактиковаться в соответствующих сценариях.

практика исследования сообщества

Другие преимущества

  • Top Level Await
  • SplitChunks поддерживает более гибкое разделение ресурсов.
  • Чанки, не содержащие JS-код, больше не будут генерировать JS-файлы.
  • Вывод генерирует код спецификации ES6 по умолчанию, а также поддерживает конфигурацию от 5 до 11.
  • ......

Пожалуйста, прочитайте подробностиChanglog

Вышеуказанная демонстрация также официально предоставлена ​​для ознакомления. Автор также обновил и протестировал свои собственные внутренние проекты, и в процессе этого может возникнуть несовместимость некоторых плагинов. В соответствии с официальными инструкциями журнала изменений вы можете найти ответ и временно изменить код соответствующего плагина. Если вы столкнулись с этим при попытке обновления, вы можете справиться с этим самостоятельно, а также сообщить об этом сообществу, чтобы совместно продвигать процесс выпуска новой версии.

В целом, после первоначального опыта Webpack 5 объем упаковки и скорость значительно улучшились, а настройка большинства функций также стала более удобной и ловкой.Федерация модулей бросается в глаза. Если вы заинтересованы, вы можете обменяться своими интерпретациями и исследованиями.

Если вам интересно узнать о новых вещах, например, специализироваться на технологиях, готовы поделиться и заинтересованы в продуктах для обмена мгновенными сообщениями, базовых механизмах настольных клиентов и базовой конструкции платформы, добро пожаловать к нам!

Автор статьи: Ван Синьюй
Бизнес ByteDance Feishu, массовый отклик, очень быстрый ответ, приходите и станьте моим коллегой~Введение в работу