С момента выпуска функции Angular Library (из Angular 7) разработка библиотеки Angular стала проще, чем когда-либо. Сама библиотека Angular оснащена управляемым сообществом пакетом ng-packagr
, который в значительной степени является ядром. В этой статье мы рассмотрим, как мы можем использовать ng-packagr
вторичные точки входа, чтобы еще больше разделить нашу библиотеку Angular!
Зачем нужны вторичные точки входа?
Одна из причин, по которой мы хотим иметь вторичные точки входа, - это возможность разделить наши зависимости. Давайте посмотрим на пример, где один модуль имеет peerDependencies
, а другой его нет.
Предположим, у нас есть следующий пример библиотеки Angular:
- Существует только одна библиотека с именем
my-awesome-lib
. - Внутри
my-awesome-lib/src
есть 2 модуля:awesome-plain
иawesome-text
.
Теперь посмотрим на содержимое компонента awesome-plain
:
import { Component } from '@angular/core'; @Component({ selector: 'awesome-plain', template: ` <div>Hey I'm just a plain text with no dependencies!</div> ` }) export class AwesomePlainComponent {}
и awesome-time
компонент:
import { Component } from '@angular/core'; import * as moment_ from 'moment'; const moment = moment_; @Component({ selector: 'awesome-time', template: ` <div>Hey, Awesome Time:</div> <div>{{ time }}</div> ` }) export class AwesomeTimeComponent { time: string; constructor() { this.time = moment().format(); } }
- Компонент
awesome-plain
НЕ имеет никаких зависимостей. - Компонент
awesome-time
зависит отmoment
.
И вот как moment
указывается в my-awesome-lib/package.json
:
{ "name": "my-awesome-lib", "version": "0.0.1", "peerDependencies": { "@angular/common": "^8.2.14", "@angular/core": "^8.2.14", "moment": "^2.26.0" } }
Обратите внимание, что эти
peerDependencies
помещаются в область библиотекиmy-awesome-lib
, не в отдельный модуль (файлы внутри библиотеки).
Наконец, вот как awesome-plain
и awesome-time
экспортируются в my-awesome-lib/src/public-api.ts
:
/* * Public API Surface of my-awesome-lib */ export * from './awesome-plain/awesome-plain.component'; export * from './awesome-plain/awesome-plain.module'; export * from './awesome-time/awesome-time.component'; export * from './awesome-time/awesome-time.module';
Из приведенных выше объяснений ясно, что внутри my-awesome-lib
есть 2 файла: awesome-plain
не имеет никаких зависимостей, а awesome-time
имеет peerDependencies
(то есть moment
). Но где же все может пойти не так?
Проблема: клиенту необходимо установить ВСЕ одноранговые зависимости из библиотеки.
Предположим, у нас есть приложение Angular, которое хочет использовать my-awesome-lib
. Первое, что необходимо сделать клиентскому приложению (угловому приложению), - это установить библиотеку:
npm i my-awesome-lib
После установки клиентское приложение переходит к импорту и использованию, например, только awesome-plain
компонента. Вот код в клиентском приложении может выглядеть одинаково:
// app.module.ts import { AwesomePlainModule } from 'my-awesome-lib'; @NgModule({ ..., imports: [ ..., AwesomePlainModule, ], bootstrap: [AppComponent] }) export class AppModule {} // app.component.html <awesome-plain></awesome-plain>
Когда мы запускаем ng serve
, в нашем терминале внезапно возникает эта ошибка:
ERROR in ./node_modules/my-awesome-lib/fesm2015/my-awesome-lib.js Module not found: Error: Can't resolve 'moment' in '/app-showcase-v8/node_modules/my-awesome-lib/fesm2015'
Он говорит, что не может найти moment
установленное в клиентском приложении. Что ж, вот что происходит. Хотя клиентское приложение импортирует и использует только awesome-plain
, компилятор Angular по-прежнему будет запрашивать установку всех peerDependencies
, определенных в my-awesome-lib
, которым в данном случае является moment
.
Текущее состояние может быть удовлетворительным, если клиентское приложение использует как awesome-plain
, так и awesome-time
. Однако представьте, что библиотека разрастается и в ней больше двух модулей, скажем, вместо 10 модулей. Давайте еще немного преувеличим; что, если 5 из 10 модулей имеют разные peerDependencies
? Если существует клиентское приложение, которое использует эту библиотеку и использует только 1 модуль, у которого нет ни одного peerDependencies
, клиентское приложение все равно должно установить все 5 peerDependencies
! Конечно, должен быть подход получше, не так ли?
Введите второстепенные точки входа!
К счастью, нынешний подход вполне можно оптимизировать. Пока что подход, используемый в библиотеке, использует только так называемые первичные точки входа. Это обозначается package.json
файлом, который существует только в my-awesome-lib/package.json
, где определяется all peerDependencies
для всей библиотеки.
С помощью вторичных точек входа мы можем дополнительно разделить peerDependencies
за пределы уровня библиотеки; это позволяет определять peerDependencies
в папках или модулях внутри библиотеки. Например, сделав awesome-time
во вторичных точках входа, мы можем иметь другой package.json
файл внутри подкаталога, который содержит peerDependencies
только для awesome-time
модуля. В результате мы больше не определяем peerDependencies
на уровне библиотеки; вместо этого мы определяем их в подкаталоге.
Кроме того, вторичные точки входа позволяют нам импортировать библиотеку следующим образом:
// Primary entry points import { AwesomePlainModule } from 'my-awesome-lib'; // Secondary entry points import { AwesomeTimeModule } from 'my-awesome-lib/awesome-time';
Таким образом, если клиентское приложение использует только AwesomePlainModule
, компилятор больше не будет запрашивать установку moment
!
Реализуйте вторичные точки входа
Надеюсь, что приведенные выше объяснения дадут вам общее представление о том, почему мы хотим использовать вторичные точки входа. Хорошая новость в том, что реализация вторичных точек входа довольно проста и понятна, потому что ng-packagr
сделает большую часть работы за кулисами!
Мы будем использовать my-awesome-lib
в качестве контекста для следующих руководств по реализации. В этом случае мы собираемся установить awesome-time
как вторичные точки входа, в то время как awesome-plain
останется как есть (по-прежнему первичные точки входа).
1. Поместите папки для вторичных точек входа непосредственно под папку библиотеки.
Согласно ng-packagr
документации, один из примеров компоновки папок для вторичных точек входа должен иметь следующий вид:
my_package
├── src
| ├── public_api.ts
| └── *.ts
├── ng-package.json
├── package.json
└── testing (secondary entry points)
├── src
| ├── public_api.ts
| └── *.ts
└── package.json
Как можно видеть, папка для вторичных точек входа помещается непосредственно в
/my_package
, что отличается от папок первичных точек входа, помещенных в/my_package/src
.
Интересно, что это аналогичная структура папок, используемая в пакете @angular/common
, в котором @angular/common
является первичными точками входа, а @angular/common/testing
- вторичными точками входа.
При этом текущий макет папки библиотеки должен быть следующим:
2. Создайте дополнительные файлы package.json и public-api.ts в папке вторичных точек входа.
Чтобы создать вторичные точки входа, нам нужно указать ng-packagr
, какую папку искать. Этого можно добиться, создав еще файлы package.json
и public-api.ts
в папке /my-awesome-lib/awesome-time
в дополнение к файлу для основных точек входа. При этом ng-packagr
динамически обнаруживает вторичные точки входа.
Содержимое /my-awesome-lib/awesome-time/package.json
может быть таким:
{ "ngPackage": { "lib": { "entryFile": "public-api.ts", "umdModuleIds": { "moment": "moment" } } }, "peerDependencies": { "moment": "^2.26.0" } }
Обратите внимание, что на данный момент мы поместили здесь
moment
какpeerDependencies
. Кроме того,"umdModuleIds"
предназначен для удаления предупреждения сng-packagr
при сборке библиотеки.
и содержимое /my-awesome-lib/awesome-time/public-api.ts
следующим образом:
/* * Public API Surface of my-awesome-lib/awesome-time */ export * from './awesome-time.component'; export * from './awesome-time.module';
К настоящему времени макет папки библиотеки должен быть следующим:
3. Удалите взаимные зависимости вторичных точек входа из основного package.json и экспортируемых файлов вторичных точек входа из основного public-api.ts.
Этот шаг также важен, поскольку мы хотели бы явно указать ng-packagr
полностью исключить вторичные точки входа из первичных точек входа. Для этого нам нужно удалить peerDependencies
, которые используются только для вторичных точек входа, из основного package.json
. В этом случае мы собираемся удалить moment
.
Главный my-awesome-lib/package.json
должен выглядеть так:
{ "name": "my-awesome-lib", "version": "0.0.1", "peerDependencies": { "@angular/common": "^8.2.14", "@angular/core": "^8.2.14" } }
Пакет
moment
был удален, поскольку он уже определен в/my-awesome-lib/awesome-time/package.json
.
Кроме того, мы собираемся удалить awesome-time
файлы, которые экспортируются в основном my-awesome-lib/src/public-api.ts
. Теперь файл должен экспортировать только awesome-plain
файл, который выглядит следующим образом:
/* * Public API Surface of my-awesome-lib */ export * from './awesome-plain/awesome-plain.component'; export * from './awesome-plain/awesome-plain.module';
4. Соберите библиотеку.
Теперь, когда все настроено, мы можем попробовать собрать библиотеку, выполнив команду ng build my-awesome-lib
. Если все сделано правильно, вы должны увидеть в терминале следующее:
Кроме того, если вы откроете папку сборки библиотеки dist/my-awesome-library
, внутри папок должны быть дополнительные файлы с именем my-awesome-lib-awesome-time.*.js
, например dist/fesm2015
и dist/bundles
. Если вы сравните его с без вторичных точек входа, папки сборки обычно содержат только my-awesome-lib.*.js
, который является сборкой только для самой библиотеки.
5. Установите и импортируйте библиотеку в клиентском приложении.
Последний шаг - наконец использовать его в приложении Angular. В пути импорта будут небольшие изменения, поскольку мы переместили awesome-time
из основных точек входа. Чтобы использовать новую структуру папок библиотеки в клиентском приложении, она должна быть такой:
// Primary entry points import { AwesomePlainModule } from 'my-awesome-lib'; // Secondary entry points import { AwesomeTimeModule } from 'my-awesome-lib/awesome-time';
Теперь, если клиентское приложение только использует AwesomePlainModule
, мы сможем запустить приложение без установки moment
(которое используется только в AwesomeTimeModule
) .
Имейте в виду, что реализация вторичных точек входа может вызвать критические изменения в вашей библиотеке Angular. Причина в том, что клиентское приложение, которое использует вашу библиотеку, должно будет обновить пути импорта. В противном случае их приложение сломается, поскольку файлы вторичных точек входа больше не импортируются из
'your-lib'
. Таким образом, это изменение не имеет обратной совместимости.
Должны ли вообще быть какие-то первичные точки входа? Можно ли иметь только второстепенные точки входа в библиотеку?
Вам может быть интересно, должны ли мы вообще использовать первичные точки входа? На мой взгляд, это нормально иметь только вторичные точки входа, главным образом потому, что @angular/material
использует только вторичные точки входа. С другой стороны, также обычно рекомендуется использовать первичные точки входа для функций или возможностей, которые логически схожи. В документации Angular Package Format написано следующее:
Общее правило в формате пакета Angular - создавать файл FESM для наименьшего набора логически связанного кода. Например, в пакете Angular есть один FESM для
@angular/core
. Когда разработчик использует символ компонента из@angular/core
, он, скорее всего, также будет использовать такие символы, какInjectable
,Directive
,NgModule
и т. Д. - прямо или косвенно. Следовательно, все эти части должны быть объединены в один ФЭСМ. Для большинства библиотечных случаев одна логическая группа должна быть сгруппирована в один модуль NgModule, и все эти файлы должны быть объединены в один файл FESM в пакете, который представляет собой единую точку входа в пакете npm.
Кроме того, в моем случае практическое правило состоит в том, чтобы сделать некоторые модули вторичными точками входа, если они имеют разные peerDependencies
. Это сделано для предотвращения принудительной установки клиентского приложения вручную всех зависимостей, хотя они не все из них используются.
Выводы
Подводя итог, вторичные точки входа - это отличная функция, которая позволяет нам еще больше разделить нашу библиотеку Angular, особенно при работе с peerDependencies
. Это также довольно легко реализовать, поскольку ng-packagr
будет динамически обнаруживать вторичные точки входа через package.json
подкаталогов.
Одним из преимуществ является то, что это уменьшит вероятность того, что клиентское приложение будет вынуждено установить все зависимости, даже если приложение не импортирует / не использует какие-либо функции библиотеки, которые зависят от установленных зависимостей.
P.S. Эта статья во многом вдохновлена замечательной статьей, написанной Кевином Кройцером, в которой рассказывается о том, как еще больше реализовать вторичные точки входа с помощью так называемого указателя. Рекомендую ознакомиться со статьей!
Репозиторий GitHub
Если объяснение, написанное в этой статье, неясно, вы можете проверить репозиторий библиотеки примеров на GitHub:
использованная литература
- Повысьте производительность SPA, разделив ваши библиотеки Angular на несколько частей. Автор Кевин Кройцер → https://medium.com/angular-in-depth/improve-spa-performance-by-splitting-your-angular-libraries-in-multiple- chunks-8c68103692d0
- Как сделать вторичные точки входа в
ng-packagr
→ https://github.com/ng-packagr/ng-packagr/blob/master/docs/secondary-entrypoints.md - Документ Angular Package Format (APF) v10.0 → https://docs.google.com/document/d/1CZC2rcpxffTDfRDs6p1cfbmKNLA6x5O-NtkJglDaBVs/preview