Всем привет! Меня зовут Илья, и я тимлид команды фронтенда в Каруне. В этой серии статей я хочу как можно подробнее осветить отличный инструмент Angular Libs. В частности, то, как можно использовать его в качестве монорепозитория для сборки нескольких приложений с tree-shaking и переиспользуемостью различных модулей в разных библиотеках и частях системы.  Идея написать это руководство возникла из-за моих собственных болей, когда наша команда впервые столкнулась с Angular-библиотеками и с их возможностями. Куча вопросов — от “как настраивать?” до “как тестировать и катить в прод?”, и самое интересное, что найти об этом информацию удавалось, но с максимально простыми примерами и очень ограниченную. Поэтому захотелось создать что-то, что поможет новичкам разобраться более детально и начать жить начать использовать Angular-библиотеки, а, возможно, этот гайд будет полезен и более опытным разработчикам. Что такое Angular Libs, и для чего они нужны? Первые шаги. Разделяй и властвуй. Человек в картинках. Забегая вперёд, сразу скажу, что описать всё в одной статье не получилось, так как объём текста выходил слишком большим. Так что сейчас вы читаете первый пост из серии. Он включает в себя процесс создания проекта с тестовой библиотекой, а также информацию о том, как настроить и вынести переиспользуемые ассеты в отдельную библиотеку для использования в приложении и в других библиотеках.  В следующих статьях постараюсь осветить: создание библиотек-доменов, быструю сборку клонов приложений с различающимся функционалом, тестирование библиотек, настройку пайплайна с нюансами использования angular-libs, подключение стейт менеджера. А ещё поделюсь опытом, как всё это применять, не создавая сильной связности. Что такое Angular Libs, и для чего они нужны? По сути это — привычный нам angular-проект. Он отличается от приложения только тем, что не может работать самостоятельно. Такую библиотеку нужно импортировать и использовать непосредственно в приложении.  Наиболее часто angular-libs используется для выноса во внешние зависимости переиспользуемого функционала. Например, вы разрабатываете некий модуль с динамическими формами или ui-компонентами, которые можно применять в нескольких проектах: вполне логично вынести такую функциональность в переиспользуемую библиотеку, а не копировать её из проекта в проект. Чаще всего такие библиотеки версионируются и публикуются в npm. Но что если нам нужно собирать несколько клонов приложения с отличающимся набором функциональности, и при этом мы не хотим публиковать куда-то наши библиотеки, учитывая, что их может быть много? Ведь в итоге можно неслабо заблудиться в версионировании самих библиотек и приложений. Обычно для таких целей используют монорепозиторий с настройкой какого-нибудь сборщика (тут можете подставить свой любимый). Однако в случае с Angular это потребует немалой сноровки и танцев с бубном, чтобы правильно переопределить конфиг webpack’а и не ловить подозрительные предупреждения и ошибки. И вот тут на сцену выходят Angular Libs! Благодаря возможностям самого Angular мы можем совершенно спокойно превратить проект в монорепозиторий и при этом практически не трогать настройки сборщика. Прежде чем мы начнём, хочу сделать небольшую ремарку для тех, кто не работал с Angular. Чтобы его установить, вам потребуются: active LTS или maintenance LTS версия Node.js, менеджер пакетов npm или yarn (ниже будет использоваться yarn),  Angular CLI (в проекте будет использоваться та версия Angular, какая была установлена и для cli; примеры в статье используют cli версии 11).  Более подробно ознакомиться с этими шагами вы можете в документации к Angular на официальном сайте. В ходе статьи на примере одного проекта мы с вами:  разберём, как создать проект с библиотекой; создадим библиотеку ассетов и подключим в неё стили, svg-иконки и картинки; разберём, зачем разделять логику на библиотеки; на примере библиотек assets и ui-lib-example посмотрим, как научить их взаимодействовать между собой. Для начала поднимем проект и создадим первую библиотеку. Нестареющая классика: Сразу соглашаемся со строгой проверкой типов и добавляем роутер. В качестве формата стилей я выбрал scss. Отлично, а теперь давайте создадим нашу первую библиотеку. Для этого также используем cli: Далее нам нужно будет разрешить или запретить отправку анонимных данных в Гугл по использованию этого проекта — тут уж решайте сами. После чего создастся наша первая библиотека по пути projects/ui-lib-example Я предлагаю сразу переименовать папку projects в libs для более очевидного контекста. Однако это потребует некоторых изменений в файле angular.json: в частности, меняем все пути к файлам с ‘project/ui-lib-example/...’ на ‘libs/ui-lib-example/...’, как в примере ниже:  Давайте ещё заглянем в наш tsconfig.json, в разделе paths должна появиться следующая запись: Если вдруг что-то пошло не так, и запись не появилась, то просто добавьте её руками. А теперь самое время проверить, что все собирается и работает. Как мы видели, при создании библиотеки был сразу создан и тестовый компонент. Давайте его подключим в наше приложение. Для этого пойдём в app.module.ts и заимпортим UiLibExampleModule:  Теперь можно добавить в шаблон app.component.html тестовый компонент из библиотеки. Для большей очевидности я бы предложил удалить всё из шаблона и добавить только наш компонент: Прежде чем запускать проект и смотреть в браузер, нам нужно собрать нашу библиотеку. Ведь как мы видели в tsconfig.json, наш проект будет смотреть в папку dist для поиска заимпорченных модулей.  Важно отметить, что если вы ведёте разработку в текущий момент прямо в библиотеке, то запускать сборку нужно с флагом --watch. Тогда не придётся пересобирать её после каждого изменения. Далее выполняем стандартный yarn start и смотрим в браузер. Всё работает, вы восхитительны! Ссылка на проект Отлично! Мы сделали заготовку для нашего проекта с возможностью сборки в несколько отдельных приложений. Предлагаю теперь подумать, что бы мы хотели вынести в библиотеки и зачем.  У себя в одном из проектов Каруны мы сделали отдельную библиотеку для ассетов (общие стили, переводы, иконки), core библиотеку для модулей, которые понадобятся для любого приложения (например, api сервис, translate сервис и т.д.) и библиотеку-домен — она отвечает за функционал, предположим, управления клиентской базой. Назовем её clients-library. В последнюю входят не отдельно взятые сервисы, а целые модули системы, которые встраиваются в приложение, состоят из компонентов, локальных api сервисов, и стейта. Кажется, что звучит как что-то сложное и не совсем полезное, однако у такого подхода есть несколько плюсов:  при настроенном lazy-loading и tree-shaking не грузятся все модули системы, значительно облегчая начальную загрузку приложения; такой подход позволяет создать слабо связанную систему; можно быстро собрать любое количество приложений. К примеру, помимо сервиса управления клиентской базой, нам требуется ещё сделать отдельное приложение для управления телефонией. Это другое приложение, другой функционал, совершенно другие люди будут его использовать. Но нам нужно иметь внутри возможность, например, просматривать список клиентов с телефонами.  Этот функционал уже есть в нашей clients-library, и мы отдельно создаём такую же библиотеку-домен для телефонии — phone-library. Всё! Теперь у нас есть возможность собрать два приложения: одно для клиентской базы, второе для телефонии с импортом части возможностей из библиотеки clients-library. Поэтому далее мы рассмотрим, как настроить взаимодействие библиотек между собой и с приложением. Начнём с библиотеки ассетов и положим туда картинки, svg и стили. Поехали: Теперь можно спокойно очистить папку libs/assets-library/src/lib и временно заменить содержимое файла libs/assets-library/src/public-api.ts на строку export const a = 1;. Дело в том, что из этого файла обязательно должно что-то экспортиться, иначе библиотека не соберётся. Начнём, пожалуй, со стилей. Создадим папку styles и добавим в неё несколько базовых стилей, которые будем использовать в любом нашем проекте. Для примера я добавил нормализацию стилей, типографику, оффсеты и цвета, а также _core.scss, который будет являться точкой входа и содержать все импорты. Так как файлов получилось довольно много, оставляю вам ссылку на коммит. Важно понимать, что Angular libs сами по себе не умеют экспортировать стили. Так что нам нужно написать небольшой скрипт в наш корневой package.json, чтобы иметь возможность забирать их в свои проекты. Для этого нам понадобится две бутылки водки такой инструмент как cpx. А теперь напишем наш скрипт. Фактически нам нужно взять наши стили и положить их в директорию собранной библиотеки. И чтобы не париться дальше с запуском нескольких скриптов, добавим сразу и скрипт сборки нашей библиотеки ассетов. Самое время его запустить и проверить, что получилось. Результат в консоли: И результат в директории dist: Как видим, всё собралось и сложилось в нужные нам места. Давайте теперь проверим, подключаются ли стили и правильно ли они применяются. Для этого в файл src/styles.scss нужно добавить строку: И немного доработаем наш app.component.html и app.component.scss.   Теперь запускаем наше приложение и получаем вот такой результат: То, что нужно. Далее проверим, сработает ли это для компонентов внутри библиотеки. Давайте изменим немного темплейт нашего ui-lib-example.component.ts: Пересоберём ui-lib-example, затем перезапустим приложение и посмотрим в браузер. Как видим, стили применяются к строке “ui-lib-example works!”, у неё появились отступы и изменилось начертание. Всё работает, и вы восхитительны!  Ссылка на коммит Теперь самое время добавить svg в assets-library. И тут я покажу два варианта, как их можно будет использовать: первый подразумевает сборку общего чанка для всех иконок, второй — разделение иконок на отдельные файлы и их загрузку по необходимости. В первом варианте нам не обязательно делать такой же перенос иконок, как мы делали со стилями. Достаточно будет воспользоваться инструментом svg-to-ts. Для начала добавим наши svg в библиотеку. Можете для примера забрать их у меня из коммита.  Теперь настроим генерацию из svg в ts, чтобы можно было подключить их в модуль и использовать в приложении и библиотеках, не импортируя сами файлы. Установим пакет: И напишем скрипт генерации в libs/assets-library/package.json. Почему именно тут? Как мне кажется, совершенно правильным будет написать такой скрипт в той библиотеке, которая отвечает за хранение и распространение наших svg.  И для удобства вызова скрипта добавим его вызов из корневого package.json Теперь при запуске команды yarn generate-icons у нас по указанному в конфиге пути появится файл libs/assets-library/src/lib/imgs/generated-svg/svg-icons.ts, в котором будет лежать объект с ключами по названию svg-файлов и содержащими сами svg. Отлично, теперь нам нужно каким-то образом использовать это в своих библиотеках и приложениях. Для упрощения работы воспользуемся пакетом @ngneat/svg-icon. Он позволит создать единый angular-модуль для работы с svg, который добавит новый html-тег `<svg-icon>` c инпутом `key` для указания имени иконки из файла svg-icons.ts и прекрасно впишется в существующий DI. Затем нам нужно добавить этот пакет в наш libs/assets-library/package.json, в раздел peerDependencies, чтобы библиотека ассетов знала об этом модуле, и мы смогли бы его использовать. Можно просто скопировать из корневого package.json. Давайте теперь создадим IconLibraryModule, который будем импортировать в необходимых местах. Положим его по пути libs/assets-library/src/lib/modules/icons/icons-library.module.ts.   Здесь мы скармливаем наш сгенерированный файл иконок в @ngneat/svg-icon. Обратите внимание на метод iconsToArray. Дело в том, SvgIconsModule ожидает на вход массив объектов в виде , а svg-to-ts не умеет генерить такие массивы. Так что нам придётся немного помочь. Ниже приведу код своей утилиты для преобразования, положил её туда же, к модулю — libs/assets-library/src/lib/modules/icons/utils.ts. И не забываем добавить SvgIconsModule в массив экспортов, иначе к нему не будет доступа из приложения.  Теперь экспортируем наш модуль через libs/assets-library/src/lib/modules/icons/index.ts, а также экспортируем его в libs/assets-library/src/public-api.ts. Если этого не сделать, то ваш модуль не будет доступен за пределами библиотеки. Следующий шаг — немного подправить наши app.component.html, app.module.ts и app.component.scss.    Также нам нужно изменить скрипт сборки библиотеки ассетов. Нужно учесть, что в первую очередь должна происходить генерация иконок и лишь затем сборка и перенос стилей: Думаю, теперь можно пересобрать нашу библиотеку с приложением и посмотреть, что получилось — yarn build:assets && yarn start.  Ну что ж, давайте теперь протестим, будет ли это работать внутри нашей ui-lib-example. Предлагаю просто скопировать изменения в темплейте, модуле и стилях и перенести их в ui-lib-example-шаблон, модуль и стили соответственно. Останется только пересобрать библиотеку и ещё раз перезапустить проект. Отлично! Всё работает, и мы получили удобный инструмент для использования svg. Однако нужно понимать, что при таком подходе пользователь будет каждый раз грузить вообще все иконки, даже если на странице используется только одна, и tree-shaking на них не сработает. Но это может быть вполне актуально, если у вас в приложении действительно много иконок, и проще загрузить их разом, а потом менять динамически.  Ссылка на коммит При втором подходе всё будет работать несколько иначе, и нам придётся написать немного больше кода. Однако это позволит нам отказаться от одной внешней зависимости, а иконки будут подгружаться “лениво” и вполне себе тришейкаться. За идею спасибо Олегу Климакову  — и за то, что задал направление, куда копать. В первую очередь нам потребуется внести изменения в конфигурацию svg-to-ts в файле libs/assets-library/package.json:  Сначала меняем тип генерируемых файлов, вернее, делаем из одного объекта несколько файлов. В итоге каждой иконке у нас будет соответствовать один js файл и один файл типов d.ts. Ещё будут сгенерированы модели (или интерфейсы) для иконок, и всё это аккуратно сложится в указанное нами место. Подробно останавливаться на каждом поле не буду, всю информацию вы сможете найти в официальной документации к пакету.  Если запустим скрипт генерации иконок yarn generate-icons, то результатом его выполнения будет создание директории generated-icons в нашем модуле icons. Я положил её туда для наглядности, но в целом вы можете сложить ваши сгенерированные файлы в любое место вашей библиотеки ассетов. Теперь, чтобы обеспечить доступ к нашим иконкам, нам нужно каким-то образом добавить файлы в так называемый реестр. Для этого нужно будет создать свой собственный сервис для регистрации используемых иконок, а также компонент, который сможет их использовать, что-то вроде <svg-icon>. При таком подходе у нас будет возможность регистрировать иконки непосредственно в модуле, в котором они используются, а DI ангуляра провернёт свою магию с внедрением зависимостей, и мы получим лениво загружаемый модули на каждую иконку. Но пока это всё слова, давайте уже что-то сделаем. Начнём с создания сервиса регистрации. Я добавил его в тот же модуль icons.  Всё довольно тривиально. Наш реестр иконок — это стандартный Map, который в качестве ключа содержит имя иконки, а в качестве контента — саму svg. Кроме того, сервис предоставляет два публичных метода: один для регистрации иконок, второй для получения svg по имени иконки. Обратите внимание, что сервис регистрируется именно в корне приложения через { providedIn: 'root' }. Теперь, чтобы упростить использование иконок, создадим свой собственный компонент по типу svg-icon но с блэкджеком и куртизанками. Положил его туда же, к icons-library.module  В общих чертах компонент работает следующим образом: в сеттер name передаётся iconName для получения ссылки на svg. Затем мы передаём его в наш, IconsRegistryService, который предоставляет нам svg-иконку (если она есть в реестре). Далее мы передаём в приватный метод svgElementFromString svg и получаем SVGElement-иконку, которую добавляем внутрь nativeElement текущего ElementRef. Также нам нужно задекларировать наш компонент в модуле и добавить его в массив exports. И не забудьте экспортировать новые файлы на глобальном уровне в public-api.ts, чтобы они были доступны за пределами библиотеки.  Оставляю в проекте два варианта осознанно, чтобы вам было проще попробовать каждый из них. Теперь, чтобы сделать наши сгенерированные иконки доступными из библиотеки, нам необходимо снова воспользоваться cpx и написать скрипт их переноса в готовый билд библиотеки ассетов. Ну, и заодно изменим скрипт самой сборки библиотеки.  Прекрасно! Теперь мы готовы воспользоваться новым инструментом и посмотреть, как же он будет работать в нашем приложении.  Для этого нужно немного изменить app.module.ts и app.component.html   Еще нужно дописать стили для lib-svg-icons. Так как мы не указали размеры иконки при генерации в компоненте, то рискуем её практически не увидеть, поэтому стоит указать размеры в стилях. Помимо этого, мы можем управлять цветом иконки через свойство fill там же в scss. Для простоты вы можете добавить в компонент IconsComponent строку “styles: [‘:host::ng-deep svg{ width: 50px; height: 50px }’]”, но мне кажется более логичным такое управление выносить туда, где иконка используется. Теперь запустим скрипт сборки библиотеки и перезапустим наше приложение. В результате в браузере мы увидим следующее: иконка в нижнем ряду всё так же подгружается, но теперь отдельным модулем и с другим цветом. А это значит, что в нашем арсенале есть два способа вынесения иконок в библиотеку и оба не только работают корректно, но и весьма удобны в использовании. И знаете что ещё? Вы восхитительны!  Ссылка на коммит Кроме svg и стилей, вы также можете сложить в библиотеку ассетов и картинки. Хотя это довольно тонкий момент. В конце концов, такие ассеты как картинки, как правило, для каждого приложения свои, и нам редко нужно их переиспользовать. Но возможна ситуация, когда мы будем собирать несколько админок, например, как говорил в начале этого раздела — управление клиентской базой и телефонией. При этом админки будут весьма похожи визуально. Так что мешает нам сделать для них похожие сервисные страницы 404 и 403? С одними и теми же картинками, но разными функциональными возможностями. Собственно, раз у нас есть возможность выносить картинки во внешнюю библиотеку, то почему бы не поговорить о том, как это сделать? Может, кому-то будет полезно. Давайте для начала положим какую-нибудь картину к нам в библиотеку, для примера можете забрать из коммита (да, я не слишком заморачивался с подбором). Как и со стилями, Angular Libs не умеет обрабатывать картинки так, чтобы они были доступны извне. Для этого нужно провернуть фокус с cpx. Пойдём в корневой package.json, напишем скрипт переноса и обновим скрипт сборки библиотеки ассетов: Результатом выполнения скрипта будет добавление папки img в директорию билда assets-library с переносом туда картинок. Тут важно отметить, что при таком подходе импорт будет доступен только средствами css. Давайте изменим app.component.html и app.component.scss: Теперь, если собрать библиотеку ассетов и приложение, то в браузере мы увидим следующее: А вот использовать такие картинки в библиотеках не так просто. Дело в том, что при сборке приложения внутренний webpack ангуляра способен правильно зарезолвить все пути. Но он понятия не имеет о путях в библиотеке, и подобная ссылка ~dist/assets-library/img/example.png  по итогу будет вести на несуществующий файл. Сборка самой библиотеки осуществляется благодаря ng-packagr, и настроить в нём  резолв таких путей на данный момент нет возможности. Однако выход есть: нужно лишь как-то дать понять вебпаку проекта о путях импортов заранее. Для этого мы можем использовать ng-content с указанием select=”.image” внутри компонента библиотеки, а стили класса при этом будут находиться в приложении. Давайте посмотрим на изменения:    Теперь мы можем пересобрать нашу библиотеку ui-lib-component и перезапустить проект. В итоге в браузере мы увидим следующую картину: Как видите, при таком подходе нужно обязательно учитывать работу паддингов, так как они начинают плюсоваться к отступам внутреннего элемента. Но вариант полностью рабочий, а вы… Ну вы поняли) Ссылка на коммит Таким образом, на примере создания библиотеки ассетов мы разобрали основной механизм создания библиотек с помощью встроенного инструмента Angular libs. Ещё научились настраивать их использование не только в приложении, но и друг с другом. В следующей же статье подробнее расскажу, как правильно настроить tree-shaking. Мы вместе создадим библиотеку для core-модулей и научимся собирать несколько приложений из одного набора библиотек, похожих по форме, но разных по содержанию. Ссылки на библиотеки и документацию: Angular libs - https://angular.io/guide/libraries  Angular CLI - https://angular.io/cli  Node.js - https://nodejs.org/en/  cpx - https://www.npmjs.com/package/cpx  svg-to-ts - https://www.npmjs.com/package/svg-to-ts  @ngneat/svg-icon - https://www.npmjs.com/package/@ngneat/svg-icon  ng-packagr - https://www.npmjs.com/package/ng-packagr  Отдельное спасибо @klimakov_me  за помощь с tree-shaking’ом иконок.  Тимлид, frontend-разработчик