Webpack - это своего рода черный ящик для большинства разработчиков. Такие инструменты, как create-react-app »абстрагируют большую часть функциональности сборщика. Я провел небольшое исследование и начал создавать свой собственный легкий веб-сборщик, чтобы лучше понять, что он влечет за собой.

Это часть моей серии о секретах:

Полное видео прохождение этого поста можно найти здесь. Часть моей серии видео Под капотом.

Эта статья будет состоять из 3 частей:

  1. Что такое "веб-сборщик"
  2. Создание компилятора для «веб-сборщика»
  3. Использование вывода с приложением

1. Что такое "веб-сборщик"

Прежде всего, мы должны задать вопрос: «Это 2020 год, зачем вообще нужны бандлы?». На этот вопрос есть много ответов:

  • Производительность: сторонний код стоит дорого, мы можем использовать статический анализ кода для его оптимизации (например, сбор вишен и встряхивание деревьев). Мы также можем упростить то, что поставляется, превратив 100 файлов в 1, ограничив расходы на данные и ресурсы для пользователя.
  • Поддержка: в Интернете так много разных сред, и вы хотите, чтобы ваш код выполнялся в максимально возможном количестве, при этом записывая его только один раз (например, добавляя полифиллы, где это необходимо)
  • Взаимодействие с пользователем: используйте кеширование браузера с отдельными пакетами (например, поставщик всех ваших библиотек и приложение для самого приложения)
  • Отдельные проблемы: управляйте тем, как вы обслуживаете шрифты, CSS, изображения, а также JS.

Базовая архитектура веб-сборщика:

В основном мы пропускаем модули через компилятор для создания активов.

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

Эти концепции:

  • IIFE
  • Пройти по исх.
  • Графики зависимостей (когда мы просматриваем файлы нашего приложения)
  • Определение пользовательской системы импорта / экспорта (которая может работать в любой среде)
  • Рекурсивные функции
  • Разбор и генерация AST (превращение исходного кода в токенизированную форму)
  • Хеширование
  • Собственный ESM (ESM хорошо управляет циклическими зависимостями благодаря проверкам во время компиляции)

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

2. Создание компилятора для «веб-сборщика».

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

Обзор компилятора приведен ниже, мы разберем каждую фазу.

Наше приложение:

Наше приложение состоит из 4 файлов. Его задача - получить дату и время, а затем передать его в logDate, задача которого - добавить текст к дате и отправить его в регистратор. Все очень просто.

Наше дерево приложений выглядит следующим образом:

ФАЗА 1

Используя сторонний инструмент для синтаксического анализа AST, мы (см. Код ниже):

  • Определить полный путь к файлам (очень важно, поэтому ясно, если мы снова имеем дело с тем же файлом)
  • Захватить содержимое файлов
  • Разобрать в AST
  • Сохраните как содержимое, так и AST в объекте «модуль».
  • Обработайте зависимости внутри содержимого (используя значение AST «ImportDeclaration»), рекурсивно вызывая эту функцию со значением
  • Наконец, добавьте эту функцию в depsArray, чтобы мы могли построить наше дерево так, чтобы первый файл появлялся последним (это важно).

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

ФАЗА 2

Задача компилятора - «Выполнить код, который создаст исполняемый код». Это означает, что у нас будет 2 уровня кода, поэтому мы будем проверять их по одному. Сначала мы рассмотрим, что создает компилятор, а затем рассмотрим построенный / выводимый код (запускаемый браузером).

Сначала построенный код

Шаблоны:

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

Мы передаем ему код модуля и индекс (Webpack также делает это с индексом).

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

В NodeJS все модули ES внутренне обернуты функцией, присоединяющей детали времени выполнения (например, экспорт), здесь мы используем то же самое. И снова Webpack делает это.

Шаблон времени выполнения: его задача - загрузить наши модули и дать идентификатор начального модуля.

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

Пользовательский импорт / экспорт:

С помощью нашего оператора импорта мы заменим экземпляр «импорта» нашим собственным. Он будет выглядеть как средний комментарий.

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

Стоит отметить, что ранее Webpack хранит идентификаторы зависимостей в модуле. У него есть собственный «шаблон зависимости», который заменяет использование импорта и экспорта пользовательскими переменными. Мой меняет местами только сам импорт (у них меняет местами вся строка и все ее использования). Одна из МНОГИХ вещей, которые не совсем такие же, как настоящий Webpack.

Преобразовать

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

Теперь код, выводимый компилятором:

Левая часть - это наша среда выполнения, правая часть показывает все загруженные «модули». Вы можете видеть, что это те модули, с которых мы начали в самом начале.

Что происходит?

Шаблон времени выполнения IIFE запускается немедленно, передавая массив модулей в качестве аргумента. Мы определяем кеш (installedModules) и нашу функцию импорта (_our_require_). Его задача - выполнить среду выполнения модуля и вернуть экспорт для данного идентификатора модуля (идентификатор коррелирует с его положением в массиве модулей). Экспорт устанавливается в родительском модуле с использованием передачи по ссылке, а затем модуль сохраняется в кеше для более легкого повторного использования. Наконец, мы выполняем функцию импорта для нашей точки входа, которая запустит приложение, поскольку оно не требуют вызова самого экспорта. Теперь весь импорт внутри наших модулей будет использовать наш собственный метод.

3. Использование вывода с приложением

Теперь у нас есть обновленная строка vendorString, которую мы хотим использовать (приведенный выше код). Итак, мы:

  1. Создайте хэш содержимого, которое будет использоваться в имени файла пакета и сохранено в манифесте.
  2. Напишите vendorString в наш новый пакет

Наконец, мы запускаем небольшое экспресс-серверное приложение, которое извлекает имя пакета из манифеста и предоставляет встроенный код (/ build) по статическому маршруту.

Если мы сейчас запустим:

›npm run compile

›Запуск npm

Наше приложение запустится, и мы сможем увидеть наш пакет и его содержимое на вкладке «сеть».

Наконец, мы можем подтвердить, что это сработало, проверив «консоль». Хорошая работа 👍

Не распространяется

Вы можете спросить: «А что еще делает Webpack, а наш нет?»

  • Обрабатывает ресурсы, отличные от js (css / images / fonts)
  • Dev и HMR: это встроено в Webpack
  • Чанки: Webpack может помещать разные модули в разные блоки, и каждый из них может иметь немного другое время выполнения и полифиллы, если это необходимо. т.е. поставщик, динамический импорт
  • Множественный экспорт: наш может сделать это, но требует защитной проверки типа модуля, так что это не стоит того для этого беспорядка.
  • Дальнейшая оптимизация (например, минификация / разделение кода / сбор вишен / встряхивание деревьев / полифиллы)
  • Исходные карты: Webpack использует комбинацию препроцессоров, каждый из которых генерирует свои собственные карты. Webpack удается объединить их все вместе.
  • Делаем его расширяемым или настраиваемым (например, загрузчики, плагины или жизненный цикл). Webpack на 80% состоит из плагинов, даже внутри, то есть компилятор запускает перехватчики для событий жизненного цикла (например, файл предварительной обработки), а загрузчики прослушивают это событие и запускаются, когда это необходимо. Кроме того, мы могли бы расширить наш компилятор для поддержки событий жизненного цикла, возможно, используя эмиттер событий NodeJS, но, опять же, этого не стоит из-за этого беспорядка.

Вот и все

Я надеюсь, что это было полезно для вас, потому что я многому научился за время, проведенное на нем. Для всех желающих есть репозиторий, который можно найти по адресу https://github.com/craigtaub/our-own-webpack.

Спасибо, Крейг 😃