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

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

Для меня большая часть путаницы вокруг webpack возникла из-за того, что я никогда не настраивал его сам и не понимал, что происходит под капотом. Я думаю, что это так для многих, потому что обычно вы не настраиваете веб-пакет с нуля. Например, когда вы запускаете новый проект Vue, Vue CLI предварительно настроит для вас веб-пакет. Я думаю, что то же самое и с React. Это удобно, потому что вы не хотите тратить полчаса на настройку webpack каждый раз, когда начинаете новый проект. Но если вы хотя бы один раз ничего не настраивали, эту «магию» трудно понять, если вам придется ее настраивать. Так что давайте заглянем под капот и сами настроим webpack. Однако мы не будем объединять кучу существующих плагинов. Будем строить их с нуля 💪.

Проблема, которую пытается решить webpack

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

Несколько лет назад в этом не было необходимости, потому что приложения JavaScript и так обычно были не такими уж большими. Вы можете просто включить пару тегов<script> в свой HTML. Но в настоящее время приложения стали настолько большими, что у вас взорвется голова, если вы попытаетесь включить все свои файлы JavaScript вручную и в правильном порядке. Но webpack идет еще дальше и позволяет объединять изображения, таблицы стилей и даже ваш HTML. Все это он делает с помощью загрузчиков и плагинов, что делает веб-пакет чрезвычайно настраиваемым, а иногда и заставляет вашу голову взорваться.

Настройка проекта

Мы создадим простое внешнее приложение, включая некоторые HTML, CSS и JavaScript, и объединим все это с веб-пакетом. Это будет крошечный проект, но его должно хватить, чтобы проиллюстрировать некоторые концепции, лежащие в основе webpack. Создадим папку проекта и установим webpack и webpack-cli.

mkdir webpack-from-scratch
cd webpack-from-scratch
mkdir src
echo "console.log('Hello world');" > src/index.js
yarn add -D webpack webpack-cli

Теперь мы можем запустить yarn webpack bundle из терминала, и webpack объединит наш index.js и выведет его в dist/main.js. Как webpack узнает, какой файл нам собрать? Условно. Конфигурация веб-пакета по умолчанию предполагает, что существует файл src/index.js, и использует его как точку входа в пакет. Точка входа - это исходный файл, из которого webpack начинает строить дерево зависимостей. Вы можете изменить точку входа и даже добавить несколько точек входа, но в нашем примере это не обязательно. Давайте проверим содержимое нашего связанного приложения:

Конечно, webpack пока не добавляет ценности нашему проекту. С таким же успехом мы могли бы скопировать index.js файл поверх себя. Но мы еще до этого дойдем. Во-первых, давайте добавим HTML-файл для загрузки нашего кода JavaScript. Мы рассмотрим, как объединить его автоматически позже, а пока давайте сохраним файл в папке dist/.

Откройте файл в браузере, и вы должны увидеть «Hello world» в консоли разработчика. Затем давайте заставим наш JavaScript что-нибудь делать. Как насчет отображения текущего времени на нашем сайте? Чтобы код оставался модульным, создайте новый файл src/dateFormat.js, который экспортирует одну функцию:

Мы импортируем функцию в index.js для отображения текущего дня недели и времени на веб-странице и обновляем их каждую секунду.

Запустите yarn webpack bundle, чтобы восстановить приложение, затем снова откройте index.html. Вы должны увидеть часы, которые обновляются каждую секунду. Когда вы снова откроете связанный файл, вы увидите, что в webpack включен код из src/dateFormat.js. Он также уменьшил код.

Теперь мы хотели бы добавить немного стиля к нашей странице. Раньше мы бы сами создавали таблицу стилей и добавляли ее в HTML. Но если позволить webpack обрабатывать все наши активы, есть несколько преимуществ: каждый модуль может явно импортировать активы, от которых он зависит. Это упрощает разработчикам отслеживание зависимостей и позволяет webpack включать в пакет только то, что действительно необходимо. Например, если на вашем веб-сайте пять страниц, использующих разные JavaScript и таблицы стилей, вы можете создать пять пакетов, каждый со своим собственным деревом зависимостей. Это может показаться неважным, если ваша кодовая база небольшая, но в конечном итоге ваше приложение будет расти. Исключение ненужного кода может изменить время загрузки страницы на несколько миллисекунд, что в Интернете может длиться вечность.

Загрузка таблиц стилей с помощью webpack

Мы сохраним стили в src/style.css. Придадим телу яркий цвет фона, отцентрируем текст и изменим семейство и размер шрифта:

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

Запуск yarn webpack bundle выдаст нам следующую ошибку:

You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders

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

Создание собственного загрузчика таблиц стилей

Когда webpack встречает оператор импорта, он загружает содержимое импортированного файла в строку UTF-8. Загрузчик веб-пакетов - это просто функция. Он принимает строку в качестве аргумента, при необходимости применяет к ней преобразования и возвращает результат. Это действительно так просто. Вы можете создать загрузчик всего несколькими строками JavaScript:

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

Мы хотим, чтобы наш загрузчик скопировал таблицу стилей в нашу директорию сборки. Затем он должен экспортировать путь к сгенерированному файлу вместо того, чтобы возвращать содержимое таблицы стилей. Мы можем использовать функцию emitFile из контекста загрузчика веб-пакета, чтобы передать таблицу стилей в каталог сборки. Давайте создадим загрузчик таблиц стилей в webpack/CssLoader.js:

Это выведет .css файл со случайным именем файла и вернет модуль, который экспортирует путь к файлу. Мы используем случайное имя файла, чтобы избежать конфликта имен, когда мы импортируем несколько таблиц стилей с одним и тем же базовым именем. Очень важно, чтобы вы определяли загрузчик как обычную функцию, а не как стрелочную функцию, потому что последняя не будет иметь доступа к контексту загрузчика через this.

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

В приведенном выше примере возврат простого пути к файлу приведет к следующему:

Это недопустимый код JavaScript. Во-первых, нет экспорта, поэтому оцененный код не возвращает никакого значения. Во-вторых, путь к файлу не заключен в кавычки, что приводит к синтаксической ошибке. Конечно, это очень упрощенное (и, вероятно, несколько неверное) объяснение сложных вещей, происходящих под капотом, но я надеюсь, что это поможет понять, как должен выглядеть результат работы загрузчика.
Кстати, легко изменить загрузчик для работы с другими типами файлов, например с изображениями. Взгляните на документацию webpack о необработанных загрузчиках, чтобы узнать, как загружать ресурсы, не конвертируя их в строки UTF-8.

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

touch webpack.config.js

И настройте правило загрузчика для .css файлов:

Теперь мы можем запустить yarn webpack bundle без ошибок. Но мы не добавили таблицу стилей в наш HTML, поэтому она ничего не сделает. Давайте воспользуемся JavaScript, чтобы добавить таблицу стилей в DOM:

Снова свяжите приложение, и вы увидите красивый яркий фон 🎉.

К сожалению, у этого есть один недостаток: тег <link> добавляется в DOM нашим кодом JavaScript во время выполнения, что означает, что он загружается последним, после отображения веб-страницы и после выполнения кода JavaScript. В идеале мы хотели бы добавить тег <link> к нашему HTML во время компиляции. Как мы это делаем? С нашим собственным плагином!

Создание плагина для генерации HTML во время компиляции

Изначально мы создали файл index.html и вручную скопировали его в папку dist/. Конечно, мы также могли бы добавить тег <link> для таблицы стилей в этот HTML-файл. Но что, если у нас более одной таблицы стилей? Это быстро становится неуправляемым. Было бы здорово, если бы webpack делал это за нас автоматически? Можем ли мы сделать то же самое с загрузчиком?
У загрузчиков нет информации о других ресурсах, от которых зависит модуль. Мы могли бы создать загрузчик HTML, но он не мог бы знать, какие таблицы стилей нужно добавить в шаблон. Вместо этого нам нужно создать плагин для веб-пакетов. Давайте посмотрим, как работают плагины.

Когда мы запускаем команду webpack bundle, webpack выполняет серию шагов для создания окончательного пакета. Он разрешает зависимости, оптимизирует код, выдает активы - список довольно длинный. Плагин позволяет нам подключиться к каждому из этих шагов для выполнения дополнительной работы.
К сожалению, хотя сам написание плагина не очень сложно, мне было трудно понять, как работают все доступные хуки и какой из них я пришлось подключиться. Документация webpack включает список доступных хуков, но информация о каждом отдельном хуке очень ограничена. Больше всего мне помогло копание в исходном коде некоторых плагинов webpack. Хорошая новость в том, что вам редко приходится писать плагин самостоятельно. Существует множество существующих плагинов для веб-пакетов, и большая часть того, что вы обычно хотите сделать, уже кем-то придумана. Например, плагин, похожий на то, что мы собираемся создать (на самом деле, он намного более мощный), это html-webpack-plugin. Но мы здесь, чтобы разобрать картонную коробку, так что заточите резак.

Плагин - это класс JavaScript с методом apply. Метод получает экземпляр компилятора в качестве аргумента, который предоставляет список ловушек, к которым мы можем подключиться. Нам нужно подключиться к компиляции после того, как webpack выпустил все ресурсы, потому что это позволяет нам получить список таблиц стилей, которые нам нужно добавить в шаблон HTML.
Мы подключаемся к хуку thisCompilation, который, в свою очередь, дает нам доступ к compilationContext. Это, опять же, открывает несколько ловушек, к которым мы можем подключиться, одна из которых - ловушка processAssets. Хук processAssets имеет несколько этапов, и мы подключаемся к этапу, который выводит дополнительные ресурсы. Я упоминал, что это довольно сложный материал? Надеюсь, код прояснит это. Создадим плагин в webpack/HtmlPlugin.js:

Тот факт, что есть вложенные хуки, не сделал это менее запутанным. Как я узнал, какие хуки использовать? Я просто посмотрел исходный код html-webpack-plugin, чтобы увидеть, что они сделали. Документация webpack о processAssets крючке на самом деле довольно хороша, но я не думаю, что сам когда-либо нашел бы этот крючок. Я также выполнил много тестовых компиляций, где я просто console.logged аргументы ловушек, чтобы увидеть, каковы их интерфейсы.

Хук использует шаблон HTML, который мы еще не создали. Давайте сделаем это дальше:

И, наконец, давайте добавим плагин в конфигурацию нашего веб-пакета:

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

Когда вы снова запустите yarn webpack bundle, он должен отправить три файла в выходной каталог:

dist/
|- 1612259960631-0.19359674520973624.css
|- index.html
|- main.js

index.html должен по-прежнему иметь жирный текст и чудесный цвет фона, но теперь стили загружаются раньше, чем JavaScript.

Заключение

Оглядываясь назад, мы начали с одного файла JavaScript, который мы связали с webpack. Затем мы добавили таблицу стилей и создали собственный загрузчик веб-пакетов для ее импорта. Наконец, мы разработали плагин, который генерирует HTML-файл и заполняет его тегами <link> для всех таблиц стилей пакета.

Конечно, обычно вы не создадите это с нуля. Есть более совершенные загрузчики веб-пакетов и плагины, которые могут работать намного лучше, чем мы сделали всего несколькими строками кода. Но я надеюсь, что на этот раз вам понравилось упражнение по созданию вещей для себя - по крайней мере, мне. Я также не боюсь нырять в глубины webpack, хотя мы пока лишь поверхностно. Вы можете написать целую книгу о том, на что способен webpack. Говоря о книгах, я думаю, что книга веб-пакетов на survivaljs.com - отличный ресурс, чтобы узнать больше о веб-пакетах. Используйте его как справочник, если продолжаете экспериментировать. Например, вы можете расширить плагин, чтобы он автоматически добавлял тег <script>, который ссылается на пакет JavaScript. Это полезно, если вы решили изменить имя пакета или если вы сконфигурируете webpack для вывода более одного пакета.

Спасибо за чтение.

Первоначально опубликовано на https://stricker.digital 13 февраля 2021 г.