С момента выпуска функции 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:



использованная литература