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

Я создал надуманное приложение, чтобы использовать его в качестве средства для понимания того, как Webpack обрабатывает импорт статических модулей ES. Упомянутые пакеты были созданы с исходным кодом, найденным в этом репозитории, который использует Webpack 4.44.2. Следуйте инструкциям в разделе README, если вы хотите создать такой же пакет.

Теперь покопаемся.

Приложение

Webpack поддерживает модули ES, CommonJS и несколько других методов импорта модулей, которые можно найти здесь, в том числе динамические выражения импорта, которые немного отличаются друг от друга. В этом случае я буду использовать синтаксис модуля ES, import { fn } from './module;', так как он наиболее актуален и поддерживается современными браузерами/Node.js через расширение .mjs.

Конфигурация веб-пакета

Файл webpack.config.js, используемый для этого проекта, довольно минимален.

Webpack 4 предоставляет конфигурацию по умолчанию, которая помогает проектам быстро стартовать. В данном случае я добавил только конфигурацию mode, а также конфигурацию html-webpack-plugin.

Предоставление mode из 'development' даст легко отлаживаемый файл main.js, который включает в себя некоторые полезные комментарии от команды Webpack.

html-webpack-plugin генерирует файл index.html, который уже ссылается на сгенерированный пакет Webpack. Есть много вариантов конфигурации, если вам нужен более надежный HTML-документ, но для этого проекта ни один из них не понадобился.

Источник

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

L1 (строка 1) и L2 содержат операторы импорт. Функции listItemTemplate и complete будут доступны после того, как эти операторы будут проанализированы. Поскольку модули ES недоступны в большинстве браузеров, Webpack необходимо проделать некоторую работу за кулисами, чтобы открыть эти экспорты коду, который их импортирует.

Комплект

npm run webpack создает каталог dist, содержащий весь код, представленный в src/index.js, а также HTML-код, сгенерированный html-webpack-plugin. Файл dist/main.js содержит исполняемый код Webpack вместе с исходным кодом.

Источник

Прежде чем погрузиться в загрузку Webpack, я думаю, стоит посмотреть, что случилось с исходным кодом. Прокрутка до конца файла main.js показывает, что объект передается в вызов IIFE (выражение немедленно вызываемой функции) — ключи объекта — это пути к исходным файлам, а значения — это функции, которым поручено инкапсулировать исходный код, который имеет некоторые очевидные дополнения и изменения.

Исходный код был преобразован в строку, а зависимости Webpack были разбросаны повсюду. При вызове eval будет выполняться строковый код JavaScript, а также будут вызываться эти функции Webpack, открывая код модуля инкапсулирующему замыканию. Детали этого раскрытия будут изложены по мере того, как мы будем анализировать функцию require Webpack.

Начальная загрузка веб-пакета

В верхней части файла находится упомянутый выше экземпляр IIFE, который был определен Webpack в комментарии как webpackBootstrap. По сути, это объявления всех функций и переменных, необходимых Webpack для загрузки связанного кода, а также для выполнения этого кода.

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

Кэширование и загрузка модулей

installedModules объявляется и устанавливается в качестве объекта в первой строке тела функции. Он используется как хеш-таблица для кэширования загруженных модулей. Каждый раз, когда загружается модуль, его экспорт запоминается для последующего быстрого поиска с помощью функции __webpack_require__. Это оптимизация производительности для ситуаций, когда модуль импортируется в несколько файлов, минуя перезагрузку, а также попытка согласования со спецификацией ECMAScript. Модули, по сути, являются синглтонами, то есть ссылаются только на один экземпляр модуля после его создания или, в данном случае, загрузки.

Предположим, что entry.js импортирует foo.js в L1 и bar.js в L2. При анализе foo.js все экспорты из utils.js будут загружены и кэшированы в installedModules. После загрузки bar.js Webpack снова попытается загрузить utils.js. Webpack проверит кэш installedModules, чтобы увидеть, существует ли модуль, что он и сделает, а затем вернет кэшированные экспорты, а не загрузит модуль снова.

Загрузка статических модулей

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

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

Требуемая функция Webpack

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

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

Кэш installedModules проверяется для модуля, и если он существует, выполнение останавливается и возвращается свойство экспорта этого модуля. Если он не существует, создается новый модуль (L9) со строкой moduleId, установленной в свойство i, которое по сути является идентификатором. Свойству l, или загруженному, присваивается значение false, и создается объект экспорта. Вот как module выглядит в этот момент исполнения:

{
  i: './src/index.js',
  l: false,
  exports: {}
}

а вот installedModules:

{
  './src/index.js': {
    i: './src/index.js',
    l: false,
    exports: {}
  }
}

Мы видим, что этот конкретный модуль все еще не загружен, потому что свойство l равно false.

Функция в modules[moduleId] вызывается на L17 с объектом module.exports в качестве контекста, который в настоящее время является пустым объектом, созданным на L14, и с аргументами module, ссылкой на installedModules[moduleId], также известным как объект module, написанный выше, и самим __webpack_require__. Эти аргументы отражают параметры, показанные ранее в объекте аргументов modules.

Оценка функции модуля

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

__webpack_require__.r — это функция, определенная в среде выполнения Webpack, которая получает объект в качестве аргумента и наряжает этот объект так, чтобы он выглядел как модуль CommonJS, а также как модуль ES, если используется оператор import.

Затем объявляется renderUtils__WEBPACK_IMPORTED_MODULE_0__ и устанавливается в значение, возвращаемое другим вызовом __webpack_require__, на этот раз с аргументом './src/renderUtils.js'. Это добавит еще одну запись в installedModules и также запустит поток требований с этим модулем.

Несколькими строками позже объявляется _todo__WEBPACK_IMPORTED_MODULE_1__ и устанавливается возвращаемое значение __webpack_require__('./src/todo.js');, добавляя еще одну запись в installedModules.

На данный момент installedModules содержит 3 ключа — './src/index.js', './src/renderUtils.js' и './src/todo.js'. Посмотрим, как это выглядит.

Атрибут l элемента '.src/index.js' по-прежнему помечен как false, потому что он все еще загружается. Его зависимости — renderUtils и todo — были загружены, и их экспорты доступны для использования, что видно из дочерних свойств экспортов на изображении выше.

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

Вызов конструктора объекта

Выполнение может выглядеть странно — функции доступны, передаются в Object вызовы, а затем вызываются, а не просто вызываются как методы.

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

var foo =
  _renderUtils__WEBPACK_IMPORTED_MODULE_0__['listItemTemplate'];
Object(foo)() === foo; // true

Согласно спецификации, импортированные модули ES по умолчанию находятся в строгом режиме. Функции в режиме strict будут блокировать доступ к объекту window по ключевому слову this. Поскольку Webpack делает все возможное, чтобы придерживаться спецификации каждого из различных методов импорта, модули ES, связанные с Webpack, должны вести себя одинаково — this при обращении к объекту window в режиме strict должно возвращаться undefined.

Когда Webpack экспортирует модуль, он предоставляет объект exports, а не то, что изначально было бы чем-то вроде статической привязки. Функции, которые ранее не были привязаны, теперь будут привязаны к контексту объекта экспорта. Передача этих строгих функций через вызовы конструктора Object удаляет эту привязку.

Остальная часть файла оценивается так, как она была написана, что приводит к выполнению кода.

Возврат к требованию

Вернувшись в исходную функцию require, L18 видит, что свойство модуля l (загружено) установлено на true, а затем возвращается свойство exports. На данный момент объект module был обновлен и теперь выглядит следующим образом:

{
  i: './src/index.js',
  l: true,
  exports: {
    Symbol(Symbol.toStringTag): 'Module',
    __esModule: true
  }
}

Модуль exports был обновлен для представления модулей CommonJS и ES. Если бы у него были экспорты, как у renderUtils, он бы включил эти экспорты в объект экспорта как геттеры.

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

Вывод

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

Пожалуйста, не стесняйтесь обращаться ко мне с любыми вопросами, проблемами или разъяснениями.

Ресурсы

Webpack 4 Analysisрепозиторий
https://github.com/mskalandunas/webpack-4-analysis/tree/master/static-imports

Документация Webpack
https://webpack.js.org/

Репозиторий Webpack
https://github.com/webpack/webpack

Спецификация ECMAScript TC39 2021
https://tc39.es/ecma262/

MDN
https://developer.mozilla.org/en-US/

Stack Overflow
https://stackoverflow.com/questions/64186011/why-does-webpack-pass-functions-imported-as-es-modules-into-an-object-call-in-we