Кастомизация сборки Angular-проекта
При сборке Angular-приложения со временем возникает задача, которую не решить с помощью того, что доступно из коробки. Узнаём, что делать в таком случае.
5К открытий5К показов
Рано или поздно может наступить ситуация, когда при сборке вашего Angular-приложения возникнет задача, выходящая за рамки того, что предлагает вам сборка Angular из коробки. Старший Angular-разработчик Noveo Мария рассказывает, как в такой ситуации быть.
Мария
старший Angular-разработчик Noveo
Как вы уже поняли, такое случилось и со мной. В один прекрасный день я захотела оптимизировать наше приложение, чтобы повысить его оценки в Lighthouse, и тут же столкнулась с классический задачей, универсального решения которой в Angular почему-то до сих пор не изобрели. Для всех кастомных шрифтов, которые мы используем, требовалось добавить для браузера информацию, необходимую для их предварительной загрузки через . Это позволило бы начать загрузку шрифтов, не дожидаясь загрузки содержимого CSS, и повысило бы производительность. Задачка, прямо скажем, не сильно критичная, но, сталкиваясь с ней из проекта в проект, я захотела решить её раз и навсегда.
Итак, что же требовалось сделать? Получить информацию о шрифтах проекта и добавить эту информацию в index.html, сгенерированный Angular. Делов-то! Но, как водится, для того, чтобы написать кастомизацию сборки, потребовалось пройти через все стадии принятия неизбежного (и если вам не хочется проходить через них вместе со мной, просто отправляйтесь к секции с готовым решением).
Отрицание. Решение задачи без каких-либо кастомизаций
В зависимости от того, как прописаны пути до шрифтов, вебпак под капотом Angular либо генерирует им новые имена с уникальным хешем и кладет в корень вашей сборки, либо не трогает их вовсе, и тогда в настройках можно включить простое копирование в сборку папки с исходными файлами.
Уникальные имена нам не требовались: в ближайшие много лет мы вряд ли планировали менять шрифты на нашем проекте, и никаких проблем с кешированием возникнуть не могло. Сделав абсолютные пути до ассетов, можно было прописать папку со шрифтами в настройку assets в angular.json и захардкодить нужные нам ссылки прямо в html. Но, как вы уже поняли, этой статьи не было бы, если бы всё было так просто ?
Поскольку мы используем встроенную в Angular 9 локализацию, мы генерируем проект в несколько папок под разные языки. Попытка указать абсолютные пути до шрифтов приводила к поломанным путям, ведь ассеты лежали в папках с именами локалей, языковой префикс не добавлялся к путям до ассетов, а глобальный <base href="/fr">
не работал с абсолютными путями нигде, кроме Chrome (что, согласитесь, странно, ведь и там он работать не должен))). Таким образом, попытка обойтись старым добрым хардкодом провалилась.
Гнев и npm script-ы
Раз уж ссылки не получилось захардкодить, возникла мысль постфактум прочитать имена ассетов, сгенерировать недостающий кусок html-кода и добавить его в предусмотрительно созданный билдом Angluar index.html. C помощью post-команд можно выполнять npm-скрипт после каждой сборки автоматически, и задача будет решена.
Подводные камни на этом пути оказались не очень подводными и лежали на поверхности. Во-первых, скрипт надо было написать так, чтобы его могли выполнить коллеги на разных операционных системах. Во-вторых, мы опять упирались в локализацию: под каждый язык сборщик Angular создавал отдельную папку, каждая папка содержала свой index.html и свои шрифты, а ходить по всем папкам и менять каждый html-файл — это было как-то уж слишком. Моя природная лень не была готова к этому испытанию, и я отправилась на поиски следующего решения.
Торг. Готовые пакеты на гитхабе
Где-то здесь стало понятно, что без кастомизации сборки не обойтись. Быстро выяснилось, что в Angular CLI, начиная с версии 8, появилось CLI Builder API, позволяющее создавать свои компоновщики. У него довольно обширная документация, и даже на русском есть! Но камон, ребята, кто же хочет читать длинную скучную документацию, когда кто-нибудь уже наверняка написал за вас быстрое, готовое и не очень оптимальное решение (которое к тому же вытащит к вам в проект десяток-другой ненужных зависимостей) и выложил его на гитхаб?
И действительно, быстрый поиск по гитхабу тут же предложил мне npx-build-plus, у которого на момент написания этой статьи 915 звездочек и последний коммит был 5 месяцев назад, и @angular-builders/custom-webpack, который обновлялся буквально пару дней назад, но вот звездочек у него было только 736.
Звездочки перевесили, и я решила дать шанс ngx-build-plus, но быстро выяснилось, что генерация index.html в Angular не входит в стандартную сборку вебпака, а значит, никаких Preload Webpack Plugin уже не подключить, ведь тогда придется генерировать свой собственный index.html с base href-ами и языками, а это даже звучит слишком сложно.
Со вторым плагином все получилось: он позволял указать и расширение для стандартного вебпак-конфига, и трансформацию для index.html. И в том, и в другом случае надо было добавить в настройки пути до файлов с нужными трансформациями. Беда заключалась в том, что в одном файле нам надо было получить список ассетов из вебпака, а в другом — добавить информацию о них в html, но для этого решено было временно воспользоваться объектом global
.
Для того, чтобы вытащить имена нужных нам файлов из сборки вебпака, был написан следующий микро-плагин:
А файл с расширением вебпак-конфига выглядел следующим образом:
Для трансформации html я воспользовалась регулярными выражениями и просто добавляла полученный html либо перед первой ссылкой на стили, либо перед закрывающим тегом </head>
:
Тут сразу видны все минусы: для каждой настройки нам требуется отдельный файл, использовать global
не очень-то хорошая идея, а значит, потребуется оркестратор. Получаем три файла плюс сторонний пакет, с поддержкой которого могут возникнуть проблемы в будущем. Конечно, плагин универсален и позволяет разными способами расширять конфигурацию, но нам эта универсальность не нужна: всё, что мы хотим, — повторить стандартную сборку, добавив информацию о шрифтах в html.
Тут мы плавно подошли к необходимости написать свой велосипед.
Депрессия и неизбежное чтение документации
За выполнение задач-компоновщиков (builder-ов) в Angular CLI отвечает специальный инструмент под названием Architect, который можно найти в пакете @angular-devkit/architect. Задачи-компоновщики – это специальные функции, которые выполняют задачи по сборке и обслуживанию нашего кода. Именно они скрываются за такими командами, как ng build
, ng lint
, ng test
.
За связь между командами в консоли и компоновщиками отвечает специальная секция конфигурации angular.json под названием architect. Здесь настраивается связь между командами и конкретными компоновщиками, выполняющимися по ним.
Для каждой команды доступно три параметра настройки. По ключу builder
указывается, какой компоновщик будет использован, в options
задаются параметры по умолчанию, а опциональный configurations
позволяет переопределить дефолтные параметры для различных конфигураций. При этом имя компоновщика в builder
состоит из двух частей: имени npm-пакета и непосредственно имени компоновщика внутри этого пакета.
Например, интересующая нас больше прочих команда build
(как мы знаем, именно билд запускает вебпак и генерирует index.html) использует компоновщик @angular-devkit/build-angular:browser.
Каким же образом architect находит в таком пакете нужную задачу и как понимает, какие параметры ему требуются? В package.json любого проекта с компоновщиками должно быть специальное свойство builders, в котором прописывается путь до JSON-файла (обычно, но не обязательно builders.json), а уже в этом файле указана информация обо всех задачах-компоновщиках в текущем проекте по этой схеме.
Да-да, в сундуке (angular.json) заяц (architect), в зайце (architect.build.builder) — утка (@angular-devkit/build-angular:browser), в утке (@angular-devkit/build-angular) — яйцо (builders.json, вы находитесь здесь), в яйце игла (код сборщика). То есть остался последний шаг к тому, чтобы наконец-то найти код нужного нам сборщика. Если вы еще не до конца запутались, едем дальше ?
В нашем builders.json обязательно должен присутствовать ключ builders. Его значением будет являться объект, ключи которого — имена отдельных задач компоновщиков (в частности, для @angular-devkit/build-angular в таком объекте мы обязательно найдем ключ browser), а значения — информация о том, где брать детали реализации.
Детали реализации состоят из трех частей: пути до кода (implementation), пути до файла с описанием схемы параметров, требуемых компоновщику (schema), и, наконец, просто описания.
Осталось потерпеть еще чуть-чуть, и ловкими движениями пальцев по клавиатуре мы расширим ангуляровский сборщик, обещаю ?
Итак, пара слов о реализации.
Для создания любого компоновщика требуется метод createBuilder()
, предоставляемый @angular-devkit/architect. Этот метод принимает асинхронную функцию, выполняющую всю логику нашего компоновщика. Для сборщика browser из @angular-devkit/build-angular это выглядит так:
При этом buildWebpackBrowser
позднее экспортируется наружу под именем executeBrowserBuilder (а это значит — его можно переиспользовать!).
Давайте наконец глянем на то, что представляет собой функция buildWebpackBrowser:
Не вдаваясь в подробности реализации, нетрудно заметить, что данный метод принимает опциональные параметры webpackConfiguration
и indexHtml
, где
Что это значит? Стандартный сборщик Angular на самом деле позволяет добавить асинхронную трансформацию вебпак-конфига и html-контента и изобретать велосипед целиком не нужно! И вот оно: вооружившись полученными знаниями, вы наконец готовы увидеть, как я создала компоновщик и передала ему необходимую логику для предзагрузки ассетов.
Принятие. Пишем свой сборщик для ангуляра
Первый вопрос, требующий решения: куда поместить кастомный компоновщик (сборщик)? В официальной документации и большинстве примеров для этого рекомендуется создавать отдельный проект и паблишить его в npm. Но на самом деле – та-дам! – это не обязательно, и если вам не хочется маяться с поддержкой отдельного пакета, достаточно вспомнить, что у вас уже есть как минимум один репозиторий с package.json, готовый указать на требуемого компоновщика: собственно, ваш проект.
Поэтому прямо в package.json моего проекта я указала
И убедилась, что в моих зависимостях присутствуют @angular-devkit/architect и @angular-devkit/build-angular.
Так как я расширяла стандартный сборщик ангуляра, свой компоновщик я назвала так же: browser. Моему сборщику не требовалось каких-то новых параметров, отличных от стандартного сборщика, поэтому схему я тоже решила не создавать, а вместо этого нагло добавила ссылку на схему сборщика browser из @angular-devkit/build-angular и какое-никакое описание:
Для того, чтобы ng build
брал кастомный сборщик вместо стандартного, в angular.json достаточно было заменить стандартный сборщик на новый (а так как конфигурация компоновщика находилась в текущем проекте, вместо имени пакета требовалось указать относительный путь до директории с package.json):
Так как все параметры остались прежними, ничего больше менять не требовалось!
Отлично, теперь нужно было только написать реализацию самого сборщика. Для простоты я отказалась от TypeScript и использовала чистый JavaScript.
Кастомный сборщик, который не делал бы ничего нового, выглядел так:
Здесь мы можем воспользоваться вебпак-плагином и методом добавления недостающей части html, написанными выше при использовании @angular-builders/custom-webpack, а использование global
заменить на какую-нибудь локальную переменную (например, sharedSpace
):
Вот и все, наш сборщик готов и отлично справляется со своей новой задачей! Итого имеем: одну JSON-конфигурацию, один js-файлик с реализацией и пару изменений в package.json и angular.json. И выглядит так, как будто бы удалить будет не сложно, ежели что ?
А вы, если прочитали этот текст до конца, да еще и поняли что-нибудь, – большой молодец.
Пользуясь случаем, хочу поблагодарить этих классных ребят за их статьи о компоновщиках и посоветовать вам для дальнейшего изучения и статьи, и официальную доку (уж теперь-то вы её точно осилите):
5К открытий5К показов