Разбиение библиотеки на точки входа

Ключевые слова: angular, secondary entry-points, component library.

В прошлой заметке мы разбивали одну библиотеку на несколько, дабы пользователи не тянули крупные куски функциональности, которые им не нужны. Но что, если библиотека продолжает расти? Можно продолжать разбивать на небольшие библиотеки, но управляться с ними в ручном режиме уже будет неудобно (версионирование, публикация). Есть и другое решение — второстепенные точки входа.

Что вообще такое «точка входа»? Это файл, из которого экспортируется публичный API вашей библиотеки. Классически это public-api.ts, ссылка на который находится в файле ng-package.json. В этой основной точке входа мы пишем следующее (продолжаем работать с уже выделенной библиотекой @lib/core):

export * from './lib/cmp1/cmp1.component';
export * from './lib/cmp2/cmp2.component';

Это и позволяет пользователю импортировать компоненты по единому пути, это точка входа:

import { Cmp1, Cmp2 } from '@lib/core';

Но кроме основной точки входа, у библиотеки может быть множество второстепенных. Начнём со структуры: разделим нашу библиотеку на кусочки, которые хотим поставлять пользователям независимо друг от друга, и создадим в корне библиотеки (у нас это projects/core) папки с соответствующими именами, они и будут точками входа. Далее, в каждой такой папке кроме используемых файлов должен находиться ng-package.json или package.json с соответствующей секцией (воспользуемся вторым вариантом), а также файл, который выступит этой точкой входа (пусть будет index.ts). В итоге получается следующее:

projects
└╴core (@lib/core)
  └╴cmp1
    ├╴cmp1.component.ts
    ├╴cmp1.module.ts
    ├╴index.ts
    └╴package.json
{
  "ngPackage": {
    "lib": {
      "entryFile": "index.ts"
    }
  }
}

Теперь для импорта условного компонента Cmp1 мы будем писать уже import { Cmp1 } from '@lib/core/cmp1';

Но что это даёт? А давайте вернёмся к ситуации с единой точкой входа в библиотеку, где есть один package.json, в котором указываются зависимости библиотеки. И не важно, какую часть библиотеки использует пользователь, он устанавливает все зависимости. Например, компонент Cmp2 работает с экселем, использует тяжёлую зависимость xlsx, и все пользователи будут её устанавливать, даже если они пользуются только компонентом Cmp1. Возвращаемся к второстепенным точкам входа. В этом случае из основного package.json подобные зависимости можно убрать и перенести в соответствующую точку входа, под секцию ngPackage! В итоге пользователь, который использует Cmp1 не будет тянуть к себе ни исходный код Cmp2, ни его зависимости.

Минусом здесь является то, что зависимости, которые мы указываем во второстепенных package.json, не будут установлены автоматически, и пользователю придётся самому уже устанавливать условный xlsx, когда он воспользуется компонентом Cmp2 и увидит ошибку о недостающей зависимости.

Стоит отметить, что точки входа могут ссылаться друг на друга, в этом случае пользователь подтянет все необходимые файлы. Например, A ссылается на B, B ссылается на C и D, а E ссылается на A. В данном случае, если нам нужен A, то затянем код из A, B, C и D. Важно, что зависимости всегда должны идти только в одну сторону, не могут две точки входа ссылаться друг на друга, а также любые более сложные циркулярные зависимости не позволят вам просто собрать библиотеку. В итоге, подобная система не только позволяет минимизировать размер для конечных пользователей, но и помогает следить за архитектурой и контролировать зацепления, когда есть понятные кирпичики и связи между ними (сравните с базовым подходом, когда все компоненты, модули, сервисы находятся либо по соответствующим папкам, или по модулям, но всё в едином пространстве, что не позволяет выделять чётко связи, и это приводит к беспорядочной связанности).

Кстати, основная точка входа уже не нужна, но просто удалить её не получится, поэтому просто оставим там одну строчку export default {};.

{{ message }}

{{ 'Comments are closed.' | trans }}