Супер хардкор | Перенеси вас в мир разработчиков плагинов для Webpack

внешний интерфейс Webpack
Супер хардкор | Перенеси вас в мир разработчиков плагинов для Webpack

написать впереди

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

Но если думать о Webpack только на уровне конфигурации, на мой взгляд, для квалифицированного фронтенд-инженера этого далеко не достаточно.

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

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

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

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

Прежде чем я начну, что я хочу сказать

Если вы заинтересованы в изучении плагина Webpack, я настоятельно рекомендую следующие две статьи:

о Tapable , плагин Webpack полностью основан на нем.

С точки зрения непрофессионала, это библиотека публикации и подписки, похожая на EventEmitter в Node. Мы можем подписаться на соответствующие события через Tapable в плагине, а Webpack запускает разные Tapable Hooks в разное время упаковки, чтобы повлиять на результаты упаковки.

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

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

CompressAssetsPlugin

Давайте начнем с относительно простого плагина и поговорим о шагах основного процесса разработки плагина.

анализ спроса

Как мы все знаем, при использовании Webpack для упаковки проекта мы обычно упаковываем все ресурсы в каталог файлов dist и сохраняем соответствующие файлы html, css и js соответственно.

На данный момент, если мне нужно упаковать все ресурсы, сгенерированные этим пакетом, в zip-пакет после каждого пакета.

Позже я сохраню упакованный zip у себя на сервере в качестве функции резервного копирования, конечно, это не важно. Важно то, что у нас есть цель: мне нужно упаковать все ресурсы, сгенерированные этой компиляцией, в дополнительный zip-файл после каждой упаковки.

const path = require('path');
const CompressAssetsPlugin = require('./plugins/CompressAssetsPlugin');

module.exports = {
  mode: 'development',
  entry: {
    main: path.resolve(__dirname, './src/entry1.js'),
  },
  devtool: false,
  output: {
    path: path.resolve(__dirname, './build'),
    filename: '[name].js',
  },
  plugins: [
    new CompressAssetsPlugin({
      output: 'result.zip',
    }),
  ],
};

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

Он принимает только выходной параметр, который представляет имя сгенерированного почтового индекса.

Что касается основного содержимого конфигурации веб-пакета, я не буду повторяться здесь, если вы не уверены в основах, вы можете проверить эту статью.React-Webpack5-TypeScript для создания спроектированного многостраничного приложения.. Далее я познакомлю вас с плагином CompressAssetsPlugin.

Принципиальный анализ

В процессе объединения Webpack есть два основных объекта:

  • compiler

Компилятор создается, когда Webpack запускает упаковку, и сохраняет всю информацию о конфигурации инициализации для этой упаковки.

В каждом процессе упаковки будет создан объект компиляции для упаковки модуля.. О том, как понять каждый раз, скажем, мы находимся в режиме наблюдения (devServer), всякий раз, когда содержимое файла изменяется, будет сгенерирован объект компиляции для упаковки, и всегда будет только один объект компиляции, если только вы не завершите команду упаковки. и снова вызовите webpack.

  • compilation

На этот раз компиляция представляет собой процесс создания ресурсов.В объекте компиляции мы можем получить доступ/изменить модули, активы и фрагменты, сгенерированные этой упаковкой, через серию API.

Официальный сайт по адресуэта ссылкаВводятся эти два объекта.

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

Это JS-библиотека для создания zip-архивов, мы будем использовать эту библиотеку для создания zip-контента.

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

В процессе упаковки нам нужно получить ресурсы, которые будут сгенерированы этой упаковкой.Мы можем использовать методcompile.getAssets(), чтобы сначала получить содержимое файлов ресурсов, сгенерированных исходной упаковкой, и вывести zip, сгенерированный компиляцией. emitAssets() в результат упаковки.

руки вверх

Выше мы представили основные базовые принципы, а затем давайте вместе реализуем этот плагин.

первыйЛюбой плагин Webpack — это модуль, который экспортирует класс или функцию, которая должна иметь метод-прототип с именем apply.

При вызове метода webpack() для запуска упаковки объект компилятора передается методу применения каждого плагина, и они вызываются для регистрации соответствующего хука.

Давайте сначала реализуем основы, давайте./pluginsсоздать каталогCompressAssetsPlugin.jsдокумент:

const pluginName = 'CompressAssetsPlugin';

class CompressAssetsPlugin {
  // 在配置文件中传入的参数会保存在插件实例中
  constructor({ output }) {
    // 接受外部传入的 output 参数
    this.output = output;
  }

  apply(compiler) {
    // 注册函数 在webpack即将输出打包文件内容时执行
    compiler.hooks.emit.tapAsync(pluginName, (compilation,callback) => {
        // dosomething
    })
  }
}

module.exports = CompressAssetsPlugin;

Выше мы просто построили инфраструктуру плагина, передали ее в методе applycompiler.hooks.emit.tapAsyncЗарегистрирована функция события, которая будет вызываться каждый раз, когда сгенерированный файл собирается упаковываться.

Функция события, которую мы зарегистрировали с помощью tapAsync, принимает два параметра:

  • Первый параметр — это объект компиляции, который представляет связанный объект этой конструкции.

  • Параметр обратного вызова соответствует функции асинхронного события, которую мы зарегистрировали через tapAsync.Вызов callback() означает, что событие регистрации завершено.

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

const JSZip = require('jszip');
const { RawSource } = require('webpack-sources');
/* 
  将本次打包的资源都打包成为一个压缩包
  需求:获取所有打包后的资源
*/
 
const pluginName = 'CompressAssetsPlugin';

class CompressAssetsPlugin {
  constructor({ output }) {
    this.output = output;
  }

  apply(compiler) {
    // AsyncSeriesHook 将 assets 输出到 output 目录之前调用该钩子
    compiler.hooks.emit.tapAsync(pluginName, (compilation, callback) => {
      // 创建zip对象
      const zip = new JSZip();
      // 获取本次打包生成所有的assets资源
      const assets = compilation.getAssets();
      // 循环每一个资源
      assets.forEach(({ name, source }) => {
        // 调用source()方法获得对应的源代码 这是一个源代码的字符串
        const sourceCode = source.source();
        // 往 zip 对象中添加资源名称和源代码内容
        zip.file(name, sourceCode);
      });
      // 调用 zip.generateAsync 生成 zip 压缩包
      zip.generateAsync({ type: 'nodebuffer' }).then((result) => {
        // 通过 new RawSource 创建压缩包
        // 并且同时通过 compilation.emitAsset 方法将生成的 Zip 压缩包输出到 this.output
        compilation.emitAsset(this.output, new RawSource(result));
        // 调用 callback 表示本次事件函数结束
        callback();
      });
    });
  }
}

module.exports = CompressAssetsPlugin;

Я аннотировал каждую строку в приведенном выше коде.Основная идея состоит в том, чтобы контролировать результаты упаковки, используя Webpack Compilation Api с помощью Compiler Hook.

Есть два места, которые я намеренно хочу подчеркнуть:

  • Что касается параметров, возвращаемых компиляцией.getAssets(), мы передаемasset.source.source()метод для получения исходного кода модуля, который необходимо сгенерировать.

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

Но появление TypeScript изменило эту проблему: когда вам нужно временно найти объект или метод, вы можете передатьtypes.d.tsБыстро найдите соответствующие методы и свойства.

image.png

Мы можем ясно видеть, что в ресурсе Asset есть атрибут источника, и мы можем получить соответствующее содержимое модуля ресурса через source.source() , в котором хранится строка | буфер .

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

Эта библиотека является встроенной библиотекой веб-пакета, которая содержит ряд объектов подкласса на основе исходного кода, таких как Source.

Здесь мы создаем объект файла ресурсов (Source), который не требует сопоставления sourceMap с помощью new RawSource(), а затем передаемcompilation. emitAsset(name,source)Выведите соответствующий ресурс.

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

image.png

Вы можете видеть, что результат.zip выводится в каталог выходной конфигурации после упаковки.

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

ExternalWebpackPlugin

Далее я предложу вам разработать несколько более сложный плагин, для которого потребуются некоторые знания AST (абстрактного синтаксического дерева). Но это не главное, и не имеет значения, если вы мало знаете об этом.

Введение

В Webpack есть вариант конфигурации для внешних:

module.exports = {
  //...
  externals: {
    jquery: 'jQuery',
  },
};

Так называемые внешние элементы означают «исключить зависимости из выходного пакета».

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

Настройте вас более подробно о внешнихможно посмотреть здесь.

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

Например, используя приведенный выше файл конфигурации, в коде есть такие зависимости модулей:

import $ from 'jquery'

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

image.png

Это упакованный код в режиме разработки вышеуказанного оператора импорта. Мы видим, что WebPack обрабатывает его в модуль jQuery.module.exports = jQuery.

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

анализ спроса

На этом этапе давайте поговорим о требованиях, которые должен реализовать ExternalWebpackPlugin.

Метод исходной внешней конфигурации

Обычно в бизнес-коде, если нам нужно ввести некоторые внутренние зависимые модули в качестве CDN вместо того, чтобы упаковывать их в виде внешних, нам нужно пройти следующие два шага:

  • Внешняя конфигурация в конфигурации веб-пакета.

Например, если мы используем две библиотеки Vue и lodash в нашем коде, мы не хотим упаковывать эти две библиотеки в бизнес-код, а хотим представить их в сгенерированном html-файле в виде CDN, нам нужно сделать это:

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
  mode: 'development',
  entry: {
    main: path.resolve(__dirname, './src/entry1.js'),
  },
  devtool: false,
  output: {
    path: path.resolve(__dirname, './build'),
    filename: '[name].js',
  },
  externals: {
    vue: 'Vue',
    lodash: '_',
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: '../public/index.html',
    }),
  ],
}

Мы настроили опцию externals в webpack.config.js, чтобы сообщить webpack, что, если во время упаковки он обнаружит введение модулей vue или lodash, ему не нужно упаковывать содержимое этих двух модулей в окончательный выходной код.

При назначении переменных Vue в глобальной среде модулю vue назначьте _ модулю lodash.

На этом мы закончили настройку внешних компонентов, но этого недостаточно. Поскольку две глобальные переменные Vue и _ в настоящее время не существуют в упакованном и скомпилированном коде, нам нужно добавить ссылки CDN, соответствующие этим двум модулям, в окончательный сгенерированный html-файл.

  • Внешние ссылки конфигурации CDN в externals вставляются в сгенерированный html-файл.

В приведенной выше конфигурации мы использовали HtmlWebpackPlugin для указания шаблона сгенерированного HTML-файла.Давайте посмотрим на этот HTML-файл.public/index.html:


<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>Webpack App</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- 手动引入对应的模块CDN -->
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
</head>

<body>
</body>

</html>

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

существующие проблемы

На мой взгляд, на этом пути есть в основном два неразумных места:

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

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

  • Во-вторых, может возникнуть проблема с избыточной загрузкой CDN.

Если lodash используется в том же проекте, я хочу упаковать модуль lodash, используемый в проекте, как внешнюю зависимость.

В настоящее время я не могу использовать lodash, но нет гарантии, что другие разработчики в проекте использовали lodash.Когда я настраиваю lodash во внешних файлах, я должен указать CDN lodash в html-файле.

Но на самом деле возможно, что в финальном проекте lodash не используется, но мы все равно избыточно вносим его CDN в html. Конечно, этот шаг можно сделать в устной форме перед выходом в интернет или с участниками группы, но не лучше ли было бы, если бы он был более интеллектуальным?

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

Плагин дизайна

Во-первых, давайте посмотрим, как нам нужно писать плагины:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExternalsWebpackPlugin = require('./plugins/externals-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: {
    main: path.resolve(__dirname, './src/entry1.js'),
  },
  devtool: false,
  output: {
    path: path.resolve(__dirname, './build'),
    filename: '[name].js',
  },
  externals: {
    vue: 'Vue',
    lodash: '_',
  },
  plugins: [
    new HtmlWebpackPlugin(),
    new ExternalsWebpackPlugin({
      lodash: {
        // cdn链接
        src: 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js',
        // 替代模块变量名
        variableName: '_',
      },
      vue: {
        src: 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js',
        variableName: 'vue',
      },
    }),
  ],
};

Здесь мы выталкиваем процесс из результатов, и мы придумали плагин ExternalsWebpackPlugin для решения двух вышеупомянутых проблем.

Параметрический дизайн

Для начала поговорим о параметрах плагина:

{
      lodash: {
        // cdn链接
        src: 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js',
        // 替代模块变量名
        variableName: '_',
      },
      vue: {
        src: 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js',
        variableName: 'Vue',
      },
}

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

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

Также заменяет модуль lodash на свойство variableName., что похоже наexternals: {lodash: '_'}.

Наконец, это также поможет нам генерировать ссылки CDN, динамически вводя значение атрибута src в объект в сгенерированном html-файле.

Плагины нужны для решения проблем

  • Шаги настройки упрощены.

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

  • Избыточная загрузка CDN.

Для избыточности, которая может быть вызвана исходным методом CDN, мы проанализируем абстрактное синтаксическое дерево через AST в ExternalsWebpackPlugin.Сохраняйте внешние зависимые модули, которые используются только в коде, и вставляйте эти используемые модули только при создании ссылки CDN на html-файл..

Принципиальный анализ

Далее я расскажу вам немного о принципе веб-пакета, используемом ExternalWebpackPlugin.

  • NormalModuleFactory Hook

Модуль NormalModuleFactory — это модуль, тесно связанный с модулем генерации, и объект компилятора использует этот модуль для обработки запроса модуля компиляции.

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

import vue from 'vue'Короче говоря, такой оператор импорта является запросом модуля.

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

  • JavaScriptParser Hook

Как упоминалось выше, у NormalModuleFactory есть несколько хуков для обработки модулей, а также есть HookMap со свойствами парсера в объекте хука NormalModuleFactory.

Здесь я немного расскажу о свойстве парсера, объект компилятора использует модуль NormalModuleFactory для обработки модулей.

Поскольку это введение модуля, когда он идентифицирует введение нового модуля, который необходимо скомпилировать, webpack по существу выполняет процесс поиска в глубину для недавно введенного модуля, который является ролью Parser.

При компиляции и импорте модуля неизбежно потребуется преобразование абстрактного синтаксического дерева AST.Хук синтаксического анализатора в свойстве NormalModuleFactory — это список хуков, предоставляемый разработчику подключаемого модуля при компиляции каждого модуля для создания AST.

Здесь плагин будет использовать JavaScriptParser Hook для анализа оператора импорта зависимого модуля импортируемого модуля, принятия решений при создании AST и сохранения используемых внешних зависимых модулей.

Так называемое сохранение только используемых внешних модулей зависимостей означает, что, например, мы не используем lodash в своем коде и в параметрах плагина передается конфигурация CDN lodash, далее мы анализируем код через AST.import _ from 'lodash'/const _ = require('lodash')например, заявление о введении модуля, я не буду генерировать соответствующую ссылку CDN в html-файле.

Внутри webpack использует синтаксис acron для анализа и обработки абстрактных синтаксических деревьев.

Если вы не понимаете концепцию HookMap, вы можетенажмите здесь, чтобы посмотреть.

В то же время о правилах генерации AST можно также нажать здесьПроверьте онлайн-сайт.

  • HtmLWebpackPlugin Hook

При компиляции окончательного выходного html-файла нам нужно полагаться на HtmlWebapckPlugin .

HtmlWebpackPlugin черезHtmlWebpackPlugin.getHooks(compilation)Этот метод расширяет количество хуков, чтобы облегчить другим разработчикам плагинов вставку логики в сгенерированный html-файл.Для конкретных хуков и времени вы можете просмотреть это изображение:

flow.png

Изображение изHtmlWebapckPlugin адрес NPM.

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

NormalModuleFactory и JavaScriptParser

Некоторые друзья о NormalModuleFactory Hook и JavaScriptParser Hook могут быть не очень знакомы с ними в первый раз, но я все же хочу немного поговорить с вами здесь.

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

// index.js 入口文件
import module1 from './module1'
import module2 from './module2'

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

Только после входа в файл входа, когда файл зависимостей необходимо скомпилировать через хук NormalModuleFactory, он войдет в хуки JavascriptParser для анализа содержимого модуля через AST.

Если в начале хука NormalModuleFactory решено, что модуль не нуждается в компиляции, он, естественно, не войдет в фазу парсера зависимого модуля.

Возьмем приведенное выше в качестве примера:

  • При выполнении команды компиляции сначала анализируется запрос модуля файла index.js, и вызывается часть хука NormalModuleFactory Hook.

  • После этого будет скомпилирован файл index.js (он же модуль), и будет выполнен анализ AST.В этом роль объекта экземпляра Parser.Далее, когда модуль (index.js) будет проанализирован, будет запущена серия хуков JavascriptParser.

встречаimport module1 from './module1'Вышеупомянутые два шага также будут повторяться.

Функция NormalModuleFactory заключается в создании модуля, а AST, обрабатывающий текущий модуль, является важной частью при создании скомпилированных модулей, поэтому мы можем зарегистрировать функцию события обработки AST через NormalModuleFactory.hooks.parser.somehook, вы можете понять это просто.

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

руки вверх

Так много предзнаменований, давайте следовать идее шаг за шагом, чтобы достичь ее!

инициализация

Во-первых, давайте взглянем на фазу инициализации плагина:

const pluginName = 'ExternalsWebpackPlugin'


class ExternalsWebpackPlugin {
  constructor(options) {
    // 保存参数
    this.options = options
    // 保存参数传入的所有需要转化CDN外部externals的库名称
    this.transformLibrary = Object.keys(options)
    // 分析依赖引入 保存代码中使用到需要转化为外部CDN的库
    this.usedLibrary = new Set()
    
  }

  apply(compiler) {
    // do something
  }
}

module.exports = ExternalsWebpackPlugin;

В начале ExternalsWebpackPlugin мы инициализируем параметры, которые плагин должен использовать в функции сборки плагина:

  • this.options

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

  • this.transformLibrary

Сохраните, какие зависимые библиотеки в коде нужно преобразовать в имена зависимых библиотек в виде CDN, которые здесь преобразуются как['lodash','vue'].

  • this.usedLibrary

Это объект Set, в котором хранятся внешние зависимости, используемые в нашем коде. Например, если параметры нашего плагина передаются в lodash и vue, но код не использует только lodash и vue, то в объекте будет храниться только один vue.

Преобразование внешних зависимостей

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

Если this.transformLibrary содержит этот модуль, нам нужно пропустить компиляцию импортированного модуля и преобразовать его во внешнюю зависимость.

Давайте сначала посмотрим на код:

const { ExternalModule } = require('webpack');

const pluginName = 'ExternalsWebpackPlugin';

class ExternalsWebpackPlugin {
  constructor(options) {
    // 保存参数
    this.options = options;
    // 保存参数传入的所有需要转化CDN外部externals的库名称
    this.transformLibrary = Object.keys(options);
    // 分析依赖引入 保存代码中使用到需要转化为外部CDN的库
    this.usedLibrary = new Set();
  }

  apply(compiler) {
    // normalModuleFactory 创建后会触发该事件监听函数
    compiler.hooks.normalModuleFactory.tap(
      pluginName,
      (normalModuleFactory) => {
        // 在初始化解析模块之前调用
        normalModuleFactory.hooks.factorize.tapAsync(
          pluginName,
          (resolveData, callback) => {
            // 获取引入的模块名称
            const requireModuleName = resolveData.request;
            if (this.transformLibrary.includes(requireModuleName)) {
              // 如果当前模块需要被处理为外部依赖
              // 首先获得当前模块需要转位成为的变量名
              const externalModuleName =
                this.options[requireModuleName].variableName;
              callback(
                null,
                new ExternalModule(
                  externalModuleName,
                  'window',
                  externalModuleName
                )
              );
            } else {
              // 正常编译 不需要处理为外部依赖 什么都不做
              callback();
            }
          }
        );
      }
    );
  }
}

module.exports = ExternalsWebpackPlugin;

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

первыйcompiler.hooks.normalModuleFactory.tapСначала мы регистрируем функцию события после того, как компилятор создаст модуль normalModuleFactory.

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

normalModuleFactory.hooks.factorize, этот хук будет вызываться перед инициализацией NormalModuleFactory для разрешения, и его функция прослушивания событий примет в качестве параметра resolveData.

Проще говоря,Здесь мы регистрируем функцию выполнения через хук, предоставленный разработчику Webapck, эта функция будет вызываться после того, как объект компилятора создаст NormalModuleFactory и до того, как NormalModuleFactory инициализирует и анализирует каждый модуль..

Другими словами, в нашем коде есть такое предложение:

import Vue from 'vue'

В этот момент webpack встречает запрос модуля при разборе этого кода (vue), функция, которую мы зарегистрировали, будет вызываться перед первоначальным синтаксическим анализом.

Далее мы возвращаемся к его параметру resolveData:

image.png

Это определение типа resolveData в исходном коде. Заинтересованные партнеры могут распечатать его во время выполнения.

Здесь нам нужно использовать атрибут запроса в resolveData, который представляет имя модуля, который необходимо разрешить в данный момент.

Напримерimport Vue from 'vue', содержимое, полученное с помощью resolveData.request,'vue'.

Давайте посмотрим на этот код дальше:

const { ExternalModule } = require('webpack');
// ...

normalModuleFactory.hooks.factorize.tapAsync(
  pluginName,
  (resolveData, callback) => {
    // 获取引入的模块名称
    const requireModuleName = resolveData.request;
    if (this.transformLibrary.includes(requireModuleName)) {
      // 如果当前模块需要被处理为外部依赖
      // 首先获得当前模块需要转位成为的变量名
      const externalModuleName = this.options[requireModuleName].variableName;
      callback(
        null,
        new ExternalModule(externalModuleName, 'window', externalModuleName)
      );
    } else {
      // 正常编译 不需要处理为外部依赖 什么都不做
      callback();
    }
  }
);

// ...

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

  • Если модуль externals не нуждается в обработке, то есть модуля нет в this.transformLibrary.

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

  • Если его нужно обрабатывать как внешний модуль, то есть модуль существует в this.transformLibrary.

В этот момент мы сначала проходимthis.options[requireModuleName].variableNameПолучите имя переменной, переданное при настройке модуля.

После этого проходимcallback(null, new ExternalModule(externalModuleName, 'window', externalModuleName) )Вернулся, чтобы создать внешний модуль зависимости для возврата, сообщив веб-пакету, что этот модуль не нужно компилировать, я вернул вам экземпляр объекта ExternalModule, который напрямую обрабатывается как внешняя зависимость.

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

  • Прежде всего, в функции слушателя асинхронного метода регистрации tapAsync в tapable вызов callback() указывает на то, что функция асинхронного слушателя завершена.

Функция обратного вызова (ошибка, результат) принимает два параметра при вызове:

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

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

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

  • Второй оnew ExternalModule(externalModuleName, 'window', externalModuleName).

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

image.png

Первый запрос параметра представляет имя переменной, сгенерированное внешним модулем при создании внешнего зависимого модуля ExternalModule. Например, когда lodash используется в качестве внешнего модуля зависимостей, нам нужно начать с_принести его, в это время мы проходим в_Вот и все.

Второй тип параметра указывает, в каком объекте монтируется переменная, соответствующая первому параметру, при создании ExternalModule. Например, когда мы обычно вводим lodash через CDN, мыwindow._означает lodash , первый аргумент_Прикреплено кwindowПод этим объектом по умолчанию первая переменная будет браться напрямую из глобальной.

Третий параметр userRequest указывает, что webpack генерирует уникальное имя moduleId при объединении файлов, которое по умолчанию будет генерироваться автоматически. Что касается moduleId, вы можете просто понять его как идентификатор модуля после упаковки.

Если вам интересно узнать больше о процессе упаковки, вы можете прочитать мою статьюАнализ всего процесса основного принципа упаковки Webapck5.

Давайте посмотрим, как выглядит так называемый ExternalModule после упаковки с помощью webpack:

image.png

так называемый вернулсяnew ExternalModule(externalModuleName, 'window', externalModuleName), возьмите lodash в качестве примера, чтобы вернуть модуль, как показано на рисунке.

Когда в коде используется модуль lodash, webpack перейдет кwindow['_']Поднимитесь, чтобы найти соответствующее содержимое модуля.

Удалите неиспользуемые модули

Далее мы выполним еще одну функцию:При создании AST следует сохранять только используемые внешние зависимые модули., удаляет модули, переданные конфигурацией плагина, но не используемые в коде.

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

 // ...

 apply(compiler) {
    // normalModuleFactory 创建后会触发该事件监听函数
    compiler.hooks.normalModuleFactory.tap(
      pluginName,
      (normalModuleFactory) => {
        // 在初始化解析模块之前调用 将匹配的模块处理成为外部 externalModule
        normalModuleFactory.hooks.factorize.tapAsync(
          pluginName,
          (resolveData, callback) => {
            // 已经完成的逻辑...
          }
        );

        // 在编译模块时触发 将模块变成为AST阶段调用
        normalModuleFactory.hooks.parser
          .for('javascript/auto')
          .tap(pluginName,(parser) => {
            // 当遇到模块引入语句 import 时
            importHandler.call(this, parser);
            // 当遇到模块引入语句 require 时
            requireHandler.call(this, parser);
          });
      }
    );
  }
  
 // ...

В приведенном выше коде мы также отслеживаем хук анализатора объекта normalModuleFactory, разница в том, что свойство анализатора normalModuleFactory.hooks — это hookMap.

мы проходимhookMap.for('javascript/auto')метод, чтобы найти метод с именем'javascript/auto'Хук, про хук HookMap в парсере, можно найти вПроверьте это здесь.

о парсере в'javascript/auto' hook, короче говоря, этот хук будет выполнен, когда парсер объекта компилятора скомпилирует файл js.

Итак, выше мы зарегистрировали соответствующую функцию события через хук JavaScriptParser.Когда webpack преобразует файл js в AST, будет вызвана зарегистрированная функция слушателя.

Далее рассмотрим две функции importHandler/requireHander:


// ...

function importHandler(parser) {
  parser.hooks.import.tap(pluginName, (statement, source) => {
    // 解析当前模块中的import语句
    if (this.transformLibrary.includes(source)) {
      this.usedLibrary.add(source);
    }
  });
}

function requireHandler(parser) {
  // 解析当前模块中的require语句
  parser.hooks.call.for('require').tap(pluginName, (expression) => {
    const moduleName = expression.arguments[0].value;
    // 当require语句中使用到传入的模块时
    if (this.transformLibrary.includes(moduleName)) {
      this.usedLibrary.add(moduleName);
    }
  });
}

// ...

Часть знаний, связанных с AST, будет раскрыта здесь, но это очень просто.

Сначала давайте взглянем на функцию importHandler, которая принимает в качестве параметра объект paser, который мы передалиparser.hooks.import.tap, функция события, зарегистрированная этим хуком, будет находиться вВо время разбора кода (преобразование кода в AST)Вызывается для каждого обнаруженного оператора импорта.

Например, при обнаружении оператора запроса модуля внутри модуляimport _ from lodash, при разборе webpack преобразует этот код в такую ​​структуру:

image.png

При этом, после завершения конвертации зарегистрированныйparser.hooks.import.tapФункция зарегистрированного события, передать:

  • оператор, весь объект ImportDeclaration в операторе выше.

  • source , который представляет имя импортированного модуля, напримерimport _ from 'lodash', его значение равно lodash .

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

Функция requireHandler аналогична логике importHandler, но ее внутренняя обработка направлена ​​на обработку оператора require import, и эффект тот же.

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

После разрешения всех модулей this.usedLibrary сохраняет имя внешней библиотеки зависимостей, используемой в коде. Также не забывайте, что это объект Set, поэтому внутри нет дублирования.

Внедрить скрипты CDN

Логика, вставленная выше, когда мы анализировали серию модулей, завершена:

  • Преобразование соответствующих модулей во внешние зависимости externals .

  • Сохраняйте только имя входящей внешней библиотеки зависимостей this.usedLibrary, используемое в коде.

Далее, давайте завершим последний шаг.Согласно содержимому this.usedLibrary, вставьте внешнюю ссылку CDN, соответствующую используемому модулю, при создании окончательного html-файла.

Давайте сначала посмотрим на реализованный код:

const HtmlWebpackPlugin = require('html-webpack-plugin');


  // ....
  apply() {
    // ...
    compiler.hooks.compilation.tap(pluginName, (compilation) => {
      // 获取HTMLWebpackPlugin拓展的compilation Hooks
      HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tap(
        pluginName,
        (data) => {
          // 额外添加scripts
          const scriptTag = data.assetTags.scripts
          this.usedLibrary.forEach((library) => {
            scriptTag.unshift({
              tagName: 'script',
              voidTag: false,
              meta: { plugin: pluginName },
              attributes: {
                defer: true,
                type: undefined,
                src: this.options[library].src,
              },
            });
          });
        }
      );
    });
    // ...
  }

Здесь мы воспользуемся преимуществом дополнительного расширения хука alterAssetTags, предоставляемого HtmlWebpackPlugin для объекта компиляции.

Когда html-файл будет окончательно сгенерирован, зациклите this.usedLibrary, зациклите ссылки CDN внешних зависимостей и добавьте ссылки CDN в html-файл.

оdata.assetTags.scripts:

image.png

Это его результат печати, здесь сохранен только одинscriptСкрипт представляет собой файл js, упакованный и сгенерированный в соответствии с файлом входа в проект.

Здесь в приведенном выше коде мы добавляем соответствующий тег CDN к этому объекту.После того, как пакет будет завершен, html-файл будетassetTags.scriptsКонтент генерируется в соответствии с тегом script.

Написав тут ExternalsWebpackPlugin мы реализовали всю его логику, на самом деле это не сложно, правда?

близко

Далее я возьму вас, чтобы проверить наш ExternalsWebpackPlugin.

code.png

Мы используем такой webpack.config.js и передаем конфигурации двух библиотек vue и lodash в ExtendsPlugin.

Также в файле вводаsrc/entry1.js, просто импортируйте модуль lodash:

import _ from 'lodash';

На этом этапе давайте снова запустим команду упаковки, чтобы увидеть результаты:

code.png

Это упакованный выходной файл index.html, мы настроили два CDN в конфигурации плагина, но поскольку vue не используется внутри кода, поэтомуТолько ссылка CDN используемого lodash монтируется в окончательный html-файл.

code.png

Это содержимое файла js, который я перехватил и упаковал с помощью webpack.Вы можете видеть, что мы успешно добились желаемого эффекта для модуля lodash.Он не компилирует lodash в окончательный результат, а как внешний модуль зависимостей.window['_']Поднимитесь и найдите.

На данный момент ExternalsWebpackPlugin готов!

напиши в конце

Прежде всего, спасибо каждому однокласснику, который может видеть здесь, разработка плагинов Webapck незнакома большинству фронтенд-разработчиков.

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

Перед лицом огромной фронтенд-инжиниринга короткой статьи о Webpack далеко не достаточно.

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

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