Angular Libs как монорепозиторий: начало, настройка, работа с assets

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Всем привет! Меня зовут Илья, и я тимлид команды фронтенда в Каруне. В этой серии статей я хочу как можно подробнее осветить отличный инструмент Angular Libs. В частности, то, как можно использовать его в качестве монорепозитория для сборки нескольких приложений с tree-shaking и переиспользуемостью различных модулей в разных библиотеках и частях системы. 

Идея написать это руководство возникла из-за моих собственных болей, когда наша команда впервые столкнулась с Angular-библиотеками и с их возможностями. Куча вопросов — от “как настраивать?” до “как тестировать и катить в прод?”, и самое интересное, что найти об этом информацию удавалось, но с максимально простыми примерами и очень ограниченную. Поэтому захотелось создать что-то, что поможет новичкам разобраться более детально и начать жить начать использовать Angular-библиотеки, а, возможно, этот гайд будет полезен и более опытным разработчикам.

Оглавление

  1. Что такое Angular Libs, и для чего они нужны?

  2. Первые шаги.

  3. Разделяй и властвуй.

  4. Человек в картинках.

Забегая вперёд, сразу скажу, что описать всё в одной статье не получилось, так как объём текста выходил слишком большим. Так что сейчас вы читаете первый пост из серии. Он включает в себя процесс создания проекта с тестовой библиотекой, а также информацию о том, как настроить и вынести переиспользуемые ассеты в отдельную библиотеку для использования в приложении и в других библиотеках. 

В следующих статьях постараюсь осветить: создание библиотек-доменов, быструю сборку клонов приложений с различающимся функционалом, тестирование библиотек, настройку пайплайна с нюансами использования 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 посмотрим, как научить их взаимодействовать между собой.

Первые шаги

Для начала поднимем проект и создадим первую библиотеку. Нестареющая классика:

ng new angular-libs-examples

Сразу соглашаемся со строгой проверкой типов и добавляем роутер. В качестве формата стилей я выбрал scss.

Отлично, а теперь давайте создадим нашу первую библиотеку. Для этого также используем cli:

ng generate library ui-lib-example

Далее нам нужно будет разрешить или запретить отправку анонимных данных в Гугл по использованию этого проекта — тут уж решайте сами. После чего создастся наша первая библиотека по пути projects/ui-lib-example

Я предлагаю сразу переименовать папку projects в libs для более очевидного контекста. Однако это потребует некоторых изменений в файле angular.json: в частности, меняем все пути к файлам с ‘project/ui-lib-example/...’ на ‘libs/ui-lib-example/...’, как в примере ниже:

angular.json
…
"newProjectRoot": "libs", // все созданные позже библиотеки будут попадать сразу в эту папку

…
"ui-lib-example": {
…
  "root": "libs/ui-lib-example",
  "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:ng-packagr",
          "options": {
            "tsConfig": "libs/ui-lib-example/tsconfig.lib.json",
            "project": "libs/ui-lib-example/ng-package.json"
          },
          "configurations": {
            "production": {
              "tsConfig": "libs/ui-lib-example/tsconfig.lib.prod.json"
            }
          }
        },
...
}

Давайте ещё заглянем в наш tsconfig.json, в разделе paths должна появиться следующая запись:

...
  "paths": {
      "ui-lib-example": [
        "dist/ui-lib-example/ui-lib-example",
        "dist/ui-lib-example"
      ]
    }
...

Если вдруг что-то пошло не так, и запись не появилась, то просто добавьте её руками.

А теперь самое время проверить, что все собирается и работает. Как мы видели, при создании библиотеки был сразу создан и тестовый компонент. Давайте его подключим в наше приложение. Для этого пойдём в app.module.ts и заимпортим UiLibExampleModule:

app.module.ts
import { UiLibExampleModule } from 'ui-lib-example';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,

    UiLibExampleModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Теперь можно добавить в шаблон app.component.html тестовый компонент из библиотеки. Для большей очевидности я бы предложил удалить всё из шаблона и добавить только наш компонент:

<div>
  <lib-ui-lib-example></lib-ui-lib-example>
</div>

Прежде чем запускать проект и смотреть в браузер, нам нужно собрать нашу библиотеку. Ведь как мы видели в tsconfig.json, наш проект будет смотреть в папку dist для поиска заимпорченных модулей. 

yarn build ui-lib-example

Важно отметить, что если вы ведёте разработку в текущий момент прямо в библиотеке, то запускать сборку нужно с флагом --watch. Тогда не придётся пересобирать её после каждого изменения.

Далее выполняем стандартный yarn start и смотрим в браузер.

Результат в браузере
Результат в браузере

Всё работает, вы восхитительны!

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

Разделяй и властвуй

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

У себя в одном из проектов Каруны мы сделали отдельную библиотеку для ассетов (общие стили, переводы, иконки), core библиотеку для модулей, которые понадобятся для любого приложения (например, api сервис, translate сервис и т.д.) и библиотеку-домен — она отвечает за функционал, предположим, управления клиентской базой. Назовем её clients-library. В последнюю входят не отдельно взятые сервисы, а целые модули системы, которые встраиваются в приложение, состоят из компонентов, локальных api сервисов, и стейта.

Кажется, что звучит как что-то сложное и не совсем полезное, однако у такого подхода есть несколько плюсов: 

  • при настроенном lazy-loading и tree-shaking не грузятся все модули системы, значительно облегчая начальную загрузку приложения;

  • такой подход позволяет создать слабо связанную систему;

  • можно быстро собрать любое количество приложений.

К примеру, помимо сервиса управления клиентской базой, нам требуется ещё сделать отдельное приложение для управления телефонией. Это другое приложение, другой функционал, совершенно другие люди будут его использовать. Но нам нужно иметь внутри возможность, например, просматривать список клиентов с телефонами.  Этот функционал уже есть в нашей clients-library, и мы отдельно создаём такую же библиотеку-домен для телефонии — phone-library. Всё! Теперь у нас есть возможность собрать два приложения: одно для клиентской базы, второе для телефонии с импортом части возможностей из библиотеки clients-library.

Поэтому далее мы рассмотрим, как настроить взаимодействие библиотек между собой и с приложением.

Стили

Начнём с библиотеки ассетов и положим туда картинки, svg и стили. Поехали:

ng generate library assets-library

Теперь можно спокойно очистить папку libs/assets-library/src/lib и временно заменить содержимое файла libs/assets-library/src/public-api.ts на строку export const a = 1;. Дело в том, что из этого файла обязательно должно что-то экспортиться, иначе библиотека не соберётся.

Начнём, пожалуй, со стилей. Создадим папку styles и добавим в неё несколько базовых стилей, которые будем использовать в любом нашем проекте. Для примера я добавил нормализацию стилей, типографику, оффсеты и цвета, а также _core.scss, который будет являться точкой входа и содержать все импорты. Так как файлов получилось довольно много, оставляю вам ссылку на коммит.

Важно понимать, что Angular libs сами по себе не умеют экспортировать стили. Так что нам нужно написать небольшой скрипт в наш корневой package.json, чтобы иметь возможность забирать их в свои проекты. Для этого нам понадобится две бутылки водки такой инструмент как cpx.

yarn add cpx --dev

А теперь напишем наш скрипт. Фактически нам нужно взять наши стили и положить их в директорию собранной библиотеки.

"cp-styles": "cpx \"./libs/assets-library/src/lib/styles/**/*\" \"./dist/assets-library/styles\" -v"

И чтобы не париться дальше с запуском нескольких скриптов, добавим сразу и скрипт сборки нашей библиотеки ассетов.

"build:assets": "yarn build assets-library && yarn cp-styles",

Самое время его запустить и проверить, что получилось. Результат в консоли:

И результат в директории dist:

Как видим, всё собралось и сложилось в нужные нам места. Давайте теперь проверим, подключаются ли стили и правильно ли они применяются. Для этого в файл src/styles.scss нужно добавить строку:

@import "~dist/assets-library/styles/core.scss";

И немного доработаем наш app.component.html и app.component.scss.

app.component.html
<div>
  <lib-ui-lib-example></lib-ui-lib-example>

  <div class="offset-left-md-m">
    <div class="offset-top-sm-l">Это нежирный текст</div>
    <div class="text-bold offset-top-sm-s">Это жирный текст</div>

    <div class="text-h2 offset-top-sm-s change-color">Тут заголовок h2</div>
    <div class="text-h6 offset-top-sm-s">Тут заголовок h6</div>
  </div>
</div>
app.component.scss
.change-color {
  color: rgb(var(--color-green));
}

Теперь запускаем наше приложение и получаем вот такой результат:

Результат в браузере
Результат в браузере

То, что нужно. Далее проверим, сработает ли это для компонентов внутри библиотеки. Давайте изменим немного темплейт нашего ui-lib-example.component.ts:

<p class="text-bold text-h6 offset-top-md-m offset-left-md-m">
  ui-lib-example works!
</p>

Пересоберём ui-lib-example, затем перезапустим приложение и посмотрим в браузер.

Результат в браузере
Результат в браузере

Как видим, стили применяются к строке “ui-lib-example works!”, у неё появились отступы и изменилось начертание. Всё работает, и вы восхитительны!

Ссылка на коммит

SVG

Теперь самое время добавить svg в assets-library. И тут я покажу два варианта, как их можно будет использовать: первый подразумевает сборку общего чанка для всех иконок, второй — разделение иконок на отдельные файлы и их загрузку по необходимости.

В первом варианте нам не обязательно делать такой же перенос иконок, как мы делали со стилями. Достаточно будет воспользоваться инструментом svg-to-ts.

Для начала добавим наши svg в библиотеку. Можете для примера забрать их у меня из коммита.

Теперь настроим генерацию из svg в ts, чтобы можно было подключить их в модуль и использовать в приложении и библиотеках, не импортируя сами файлы. Установим пакет:

yarn add svg-to-ts --dev

И напишем скрипт генерации в libs/assets-library/package.json. Почему именно тут? Как мне кажется, совершенно правильным будет написать такой скрипт в той библиотеке, которая отвечает за хранение и распространение наших svg.

libs/assets-library/package.json.
{
  "name": "assets-library",
  "version": "0.0.1",
  "scripts": {
    "generate-icons": "svg-to-ts"
  },
  "peerDependencies": {...},
  "dependencies": {...},
  "svg-to-ts": {
    "conversionType": "object",
    "srcFiles": [
      "./src/lib/imgs/icons/*.svg"
    ],
    "outputDirectory": "./src/lib/imgs/generated-svg",
    "fileName": "svg-icons",
    "svgoConfig": {
      "plugins": [
        {
          "removeDimensions": true,
          "cleanupAttrs": true
        }
      ]
    }
  }
}

И для удобства вызова скрипта добавим его вызов из корневого package.json

...
"scripts": {
    ...
    "generate-icons": "yarn --cwd libs/assets-library generate-icons",
    ...
  },
...

Теперь при запуске команды 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.

yarn add @ngneat/svg-icon --dev

Затем нам нужно добавить этот пакет в наш libs/assets-library/package.json, в раздел peerDependencies, чтобы библиотека ассетов знала об этом модуле, и мы смогли бы его использовать. Можно просто скопировать из корневого package.json.

{
  "name": "assets-library",
  "version": "0.0.1",
  "peerDependencies": {
    "@angular/common": "^11.2.14",
    "@angular/core": "^11.2.14",
    "@ngneat/svg-icon": "^3.2.0"
  },
...
}

Давайте теперь создадим IconLibraryModule, который будем импортировать в необходимых местах. Положим его по пути libs/assets-library/src/lib/modules/icons/icons-library.module.ts.

icons-library.module.ts
import { SvgIconsModule } from '@ngneat/svg-icon';

import { iconsToArray } from './utils';

import icons from '../../imgs/generated-svg/svg-icons';

@NgModule({
  imports: [
    SvgIconsModule.forRoot({
      icons: iconsToArray(icons),
    }),
  ],
  exports: [
    SvgIconsModule,
  ],
})
export class IconsLibraryModule { }

Здесь мы скармливаем наш сгенерированный файл иконок в @ngneat/svg-icon. Обратите внимание на метод iconsToArray. Дело в том, SvgIconsModule ожидает на вход массив объектов в виде [{ name: имя иконки, data: сама свг }], а svg-to-ts не умеет генерить такие массивы. Так что нам придётся немного помочь. Ниже приведу код своей утилиты для преобразования, положил её туда же, к модулю — libs/assets-library/src/lib/modules/icons/utils.ts. И не забываем добавить SvgIconsModule в массив экспортов, иначе к нему не будет доступа из приложения.

Утилита
export const objConverting = ([name, data]: [string, string]) => ({ name, data });

export const iconsToArray = (obj: { [key: string]: string }): { name: string, data: string }[] =>
  Object.entries(obj).map(objConverting);

Теперь экспортируем наш модуль через 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.

app.component.html
<div>
  ...
  <div class="icons offset-top-sm-l">
    <svg-icon key="work" color="red"></svg-icon>
    <svg-icon key="settings" color="green"></svg-icon>
    <svg-icon key="settings" color="yellow"></svg-icon>
  </div>
</div>
app.module.ts
...
import { IconsLibraryModule } from 'assets-library';
...

@NgModule({
  ...
  imports: [
    ...
    IconsLibraryModule,
  ],
  ...
})
export class AppModule { }
app.component.scss
.icons {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 200px;
  height: 80px;
  margin-left: var(--offset-md-m);
  background-color: rgb(var(--color-grey-40));

  & svg-icon {
    width: 40px;
    height: 40px;
  }
}

Также нам нужно изменить скрипт сборки библиотеки ассетов. Нужно учесть, что в первую очередь должна происходить генерация иконок и лишь затем сборка и перенос стилей:

"build:assets": "yarn generate-icons && yarn build assets-library && yarn cp-styles"

Думаю, теперь можно пересобрать нашу библиотеку с приложением и посмотреть, что получилось — yarn build:assets && yarn start.

Результат в браузере
Результат в браузере

Ну что ж, давайте теперь протестим, будет ли это работать внутри нашей ui-lib-example. Предлагаю просто скопировать изменения в темплейте, модуле и стилях и перенести их в ui-lib-example-шаблон, модуль и стили соответственно. Останется только пересобрать библиотеку и ещё раз перезапустить проект.

Результат в браузере
Результат в браузере

Отлично! Всё работает, и мы получили удобный инструмент для использования svg. Однако нужно понимать, что при таком подходе пользователь будет каждый раз грузить вообще все иконки, даже если на странице используется только одна, и tree-shaking на них не сработает. Но это может быть вполне актуально, если у вас в приложении действительно много иконок, и проще загрузить их разом, а потом менять динамически.

Ссылка на коммит

При втором подходе всё будет работать несколько иначе, и нам придётся написать немного больше кода. Однако это позволит нам отказаться от одной внешней зависимости, а иконки будут подгружаться “лениво” и вполне себе тришейкаться.

За идею спасибо Олегу Климакову  — и за то, что задал направление, куда копать.

В первую очередь нам потребуется внести изменения в конфигурацию svg-to-ts в файле libs/assets-library/package.json:

libs/assets-library/package.json
"svg-to-ts": {
    "conversionType": "files",
    "srcFiles": [
      "./src/lib/imgs/icons/*.svg"
    ],
    "outputDirectory": "./src/lib/modules/icons/generated-icons",
    "fileName": "svg-icons",
    "interfaceName": "SvgIcon",
    "typeName": "svgIcon",
    "prefix": "svgIcon",
    "optimizeForLazyLoading": true,
    "modelFileName": "svg-icon.model",
    "additionalModelFile": "./src/lib",
    "compileSources": true,
    "svgoConfig": {
      "plugins": [
        {
          "removeDimensions": true,
          "cleanupAttrs": true
        }
      ]
    }
  }

Сначала меняем тип генерируемых файлов, вернее, делаем из одного объекта несколько файлов. В итоге каждой иконке у нас будет соответствовать один js файл и один файл типов d.ts. Ещё будут сгенерированы модели (или интерфейсы) для иконок, и всё это аккуратно сложится в указанное нами место. Подробно останавливаться на каждом поле не буду, всю информацию вы сможете найти в официальной документации к пакету. 

Если запустим скрипт генерации иконок yarn generate-icons, то результатом его выполнения будет создание директории generated-icons в нашем модуле icons. Я положил её туда для наглядности, но в целом вы можете сложить ваши сгенерированные файлы в любое место вашей библиотеки ассетов.

Теперь, чтобы обеспечить доступ к нашим иконкам, нам нужно каким-то образом добавить файлы в так называемый реестр. Для этого нужно будет создать свой собственный сервис для регистрации используемых иконок, а также компонент, который сможет их использовать, что-то вроде <svg-icon>. При таком подходе у нас будет возможность регистрировать иконки непосредственно в модуле, в котором они используются, а DI ангуляра провернёт свою магию с внедрением зависимостей, и мы получим лениво загружаемый модули на каждую иконку.

Но пока это всё слова, давайте уже что-то сделаем. Начнём с создания сервиса регистрации. Я добавил его в тот же модуль icons.

libs/assets-library/src/lib/modules/icons/icons.service.ts
@Injectable({ providedIn: 'root' })
export class IconsRegistryService {
  private registry = new Map<string, string>();

  registerIcons(icons: SvgIcon[]): void {
    icons.forEach((icon: SvgIcon) => this.registry.set(icon.name, icon.data));
  }

  getIcon(iconName: string): string | undefined {
    if (!this.registry.has(iconName)) {
      // eslint-disable-next-line no-console
      console.warn(
        `We could not find the icon with name ${ iconName }, did you add it to the icon registry?`,
      );
    }

    return this.registry.get(iconName);
  }
}

Всё довольно тривиально. Наш реестр иконок — это стандартный Map, который в качестве ключа содержит имя иконки, а в качестве контента — саму svg. Кроме того, сервис предоставляет два публичных метода: один для регистрации иконок, второй для получения svg по имени иконки. Обратите внимание, что сервис регистрируется именно в корне приложения через { providedIn: 'root' }.

Теперь, чтобы упростить использование иконок, создадим свой собственный компонент по типу svg-icon но с блэкджеком и куртизанками. Положил его туда же, к icons-library.module

libs/assets-library/src/lib/modules/icons/icons.component.ts
@Component({
  selector: 'lib-svg-icons',
  template: '<ng-content></ng-content>',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class IconsComponent {
  private svgIcon: SVGElement | undefined;

  @Input() set name(iconName: svgIcon) {
    const svgData = this.svgIconRegistry.getIcon(iconName);

    if (this.svgIcon) {
      this.el.nativeElement.removeChild(this.svgIcon);
    }

    if (svgData) {
      this.svgIcon = this.svgElementFromString(svgData);
      this.el.nativeElement.appendChild(this.svgIcon);
    }
  }

  constructor(
    private el: ElementRef,
    private svgIconRegistry: IconsRegistryService,
    @Optional() @Inject(DOCUMENT) private document: any,
  ) { }

  private svgElementFromString(svgContent: string): SVGElement {
    const div = this.document.createElement('DIV');
    div.innerHTML = svgContent;

    return div.querySelector('svg') || this.document.createElementNS('http://www.w3.org/200/svg', 'path');
  }
}

В общих чертах компонент работает следующим образом: в сеттер name передаётся iconName для получения ссылки на svg. Затем мы передаём его в наш, IconsRegistryService, который предоставляет нам svg-иконку (если она есть в реестре). Далее мы передаём в приватный метод svgElementFromString svg и получаем SVGElement-иконку, которую добавляем внутрь nativeElement текущего ElementRef.

Также нам нужно задекларировать наш компонент в модуле и добавить его в массив exports. И не забудьте экспортировать новые файлы на глобальном уровне в public-api.ts, чтобы они были доступны за пределами библиотеки.

icons-library.module.ts
@NgModule({
  declarations: [
    IconsComponent,
  ],
  imports: [
    SvgIconsModule.forRoot({
      icons: iconsToArray(icons),
    }),
  ],
  exports: [
    SvgIconsModule,
    IconsComponent,
  ],
})
export class IconsLibraryModule { }

Оставляю в проекте два варианта осознанно, чтобы вам было проще попробовать каждый из них.

Теперь, чтобы сделать наши сгенерированные иконки доступными из библиотеки, нам необходимо снова воспользоваться cpx и написать скрипт их переноса в готовый билд библиотеки ассетов. Ну, и заодно изменим скрипт самой сборки библиотеки.

package.json
"scripts": {
...
    "build:assets": "yarn generate-icons && yarn build assets-library && yarn cp-styles && yarn cp-images && yarn cp-icons",
...
    "cp-icons": "cpx \"./libs/assets-library/src/lib/modules/icons/generated-icons/**/*.{d.ts,js}\" \"./dist/assets-library/icons\" -v",
  },

Прекрасно! Теперь мы готовы воспользоваться новым инструментом и посмотреть, как же он будет работать в нашем приложении. 

Для этого нужно немного изменить app.module.ts и app.component.html

app.module.ts
import { IconsLibraryModule, IconsRegistryService } from 'assets-library';
import { svgIconWork } from 'assets-library/icons';

...
export class AppModule {
  constructor(private svgIconRegistry: IconsRegistryService) {
    svgIconRegistry.registerIcons([svgIconWork]);
  }
}
app.component.html
...
  <div class="icons offset-top-sm-l">
    <lib-svg-icons name="work"></lib-svg-icons>
    <svg-icon key="settings" color="green"></svg-icon>
    <svg-icon key="settings" color="yellow"></svg-icon>
  </div>

Еще нужно дописать стили для lib-svg-icons. Так как мы не указали размеры иконки при генерации в компоненте, то рискуем её практически не увидеть, поэтому стоит указать размеры в стилях. Помимо этого, мы можем управлять цветом иконки через свойство fill там же в scss. Для простоты вы можете добавить в компонент IconsComponent строку “styles: [‘:host::ng-deep svg{ width: 50px; height: 50px }’]”, но мне кажется более логичным такое управление выносить туда, где иконка используется.

lib-svg-icons {
  width: 40px;
  height: 40px;
  fill: #941894;
}

Теперь запустим скрипт сборки библиотеки и перезапустим наше приложение. В результате в браузере мы увидим следующее: иконка в нижнем ряду всё так же подгружается, но теперь отдельным модулем и с другим цветом.

Результат в браузере
Результат в браузере

А это значит, что в нашем арсенале есть два способа вынесения иконок в библиотеку и оба не только работают корректно, но и весьма удобны в использовании. И знаете что ещё? Вы восхитительны!

Ссылка на коммит

Человек в картинках

Кроме svg и стилей, вы также можете сложить в библиотеку ассетов и картинки. Хотя это довольно тонкий момент. В конце концов, такие ассеты как картинки, как правило, для каждого приложения свои, и нам редко нужно их переиспользовать. Но возможна ситуация, когда мы будем собирать несколько админок, например, как говорил в начале этого раздела — управление клиентской базой и телефонией. При этом админки будут весьма похожи визуально. Так что мешает нам сделать для них похожие сервисные страницы 404 и 403? С одними и теми же картинками, но разными функциональными возможностями.

Собственно, раз у нас есть возможность выносить картинки во внешнюю библиотеку, то почему бы не поговорить о том, как это сделать? Может, кому-то будет полезно. Давайте для начала положим какую-нибудь картину к нам в библиотеку, для примера можете забрать из коммита (да, я не слишком заморачивался с подбором).

Как и со стилями, Angular Libs не умеет обрабатывать картинки так, чтобы они были доступны извне. Для этого нужно провернуть фокус с cpx. Пойдём в корневой package.json, напишем скрипт переноса и обновим скрипт сборки библиотеки ассетов:

"build:assets": "yarn generate-icons && yarn build assets-library && yarn cp-styles && yarn cp-images",
...
"cp-images": "cpx \"./libs/assets-library/src/lib/imgs/img/**/*\" \"./dist/assets-library/img\" -v",

Результатом выполнения скрипта будет добавление папки img в директорию билда assets-library с переносом туда картинок.

Тут важно отметить, что при таком подходе импорт будет доступен только средствами css.

Давайте изменим app.component.html и app.component.scss:

<div>
  ...
  <div class="img offset-top-sm-l"></div>
</div>
...
.img {
  width: 174px;
  height: 210px;
  margin-left: var(--offset-md-m);
  background: no-repeat url("~dist/assets-library/img/example.png") 0 0;
  background-size: contain;
}

Теперь, если собрать библиотеку ассетов и приложение, то в браузере мы увидим следующее:

Результат в браузере
Результат в браузере

А вот использовать такие картинки в библиотеках не так просто. Дело в том, что при сборке приложения внутренний webpack ангуляра способен правильно зарезолвить все пути. Но он понятия не имеет о путях в библиотеке, и подобная ссылка ~dist/assets-library/img/example.png  по итогу будет вести на несуществующий файл. Сборка самой библиотеки осуществляется благодаря ng-packagr, и настроить в нём  резолв таких путей на данный момент нет возможности. Однако выход есть: нужно лишь как-то дать понять вебпаку проекта о путях импортов заранее. Для этого мы можем использовать ng-content с указанием select=”.image” внутри компонента библиотеки, а стили класса при этом будут находиться в приложении. Давайте посмотрим на изменения:

libs/ui-lib-example/src/lib/ui-lib-example.component.ts
@Component({
  selector: 'lib-ui-lib-example',
  template: `
    <div class="offset-left-md-m">
      <div class="offset-top-md-m">
        <ng-content select=".image"></ng-content>
      </div>

      <p class="text-bold text-h6 offset-top-md-m">
        ui-lib-example works!
      </p>

      <div class="icons offset-top-sm-l">
        <svg-icon key="work" color="red"></svg-icon>
        <svg-icon key="settings" color="green"></svg-icon>
        <svg-icon key="settings" color="yellow"></svg-icon>
      </div>
    </div>
  `,
  styleUrls: ['./ui-lib-example.component.scss'],
})
export class UiLibExampleComponent { }
angular-libs-examples/src/app/app.component.html
<div>
  <lib-ui-lib-example>
    <div class="image"></div>
  </lib-ui-lib-example>
  ...
</div>
angular-libs-examples/src/app/app.component.scss
.img, .image {
  width: 174px;
  height: 210px;
  margin-left: var(--offset-md-m);
  background: no-repeat url("~dist/assets-library/img/example.png") 0 0;
  background-size: contain;
}

Теперь мы можем пересобрать нашу библиотеку ui-lib-component и перезапустить проект. В итоге в браузере мы увидим следующую картину:

Результат в браузере
Результат в браузере

Как видите, при таком подходе нужно обязательно учитывать работу паддингов, так как они начинают плюсоваться к отступам внутреннего элемента. Но вариант полностью рабочий, а вы… Ну вы поняли)

Ссылка на коммит

Заключение

Таким образом, на примере создания библиотеки ассетов мы разобрали основной механизм создания библиотек с помощью встроенного инструмента Angular libs. Ещё научились настраивать их использование не только в приложении, но и друг с другом. В следующей же статье подробнее расскажу, как правильно настроить tree-shaking. Мы вместе создадим библиотеку для core-модулей и научимся собирать несколько приложений из одного набора библиотек, похожих по форме, но разных по содержанию.

Ссылки на библиотеки и документацию:

  1. Angular libs - https://angular.io/guide/libraries 

  2. Angular CLI - https://angular.io/cli 

  3. Node.js - https://nodejs.org/en/ 

  4. cpx - https://www.npmjs.com/package/cpx 

  5. svg-to-ts - https://www.npmjs.com/package/svg-to-ts 

  6. @ngneat/svg-icon - https://www.npmjs.com/package/@ngneat/svg-icon 

  7. ng-packagr - https://www.npmjs.com/package/ng-packagr 

Отдельное спасибо @klimakov_me  за помощь с tree-shaking’ом иконок.

Источник: https://habr.com/ru/company/karuna/blog/588448/


Интересные статьи

Интересные статьи

Invisible Internet Project (I2P, проект невидимого интернета) – лидер среди технологий приватной передачи информации. Полная децентрализация и независимость делают сеть I...
Моя предыдущая статья Разборщик (Parsing Complex) JSON (JavaScript Object Notation) во Flutter получила много хороших отзывов от людей, которые начинают работать во Flutt...
Герой нового выпуска «Где работать в ИТ» — платформа автоматизации маркетинга Майндбокс. На наши каверзные вопросы ответили технический директор Никита Прудников и директ...
В этой статье будут рассмотрены 3 практических вопроса, с которыми я столкнулся при разработке приложения. Как работать с Video и Audio с максимальной точностью. Как оптимально обраба...
Предлагаю ознакомиться с расшифровкой доклада начала 2020 года Андрея Бородина "Odyssey: архитектура, настройка, мониторинг" Совсем недавно мы выпустили версию 1.0 нашего пулера соедин...