Начнем с начала
Если мне не изменяет память, то с версии 6 в angular появилась возможность создавать в одном workspace проекты разных типов: application и library.
До этого момента люди, которые хотели создать библиотеку компонент, скорее всего, пользовались отличным и полезным пакетом ng-packagr, который помогал создавать пакет в принятом для angular формате. Собственно, предыдущую библиотеку я создавал при помощи этого инструмента. Теперь команда angular включила ng-packagr в angular-cli и добавила schematics для создания и сборки библиотек, расширила формат angular.json и добавила еще несколько приятностей. Давайте теперь пройдем путь от ng new до npm install — от создания пустой библиотеки до ее публикации и импорта в сторонний проект.
Workspace создается как обычно
ng new test-app
Будет создан workspace и проект приложения, заглянем в angular.json
{ ... "projects": { "test-app": { ... "sourceRoot": "src", "projectType": "application", "prefix": "app" ... } ... } ... }
Теперь добавим проект библиотеки
ng generate library test-lib --prefix=tl
ключ --prefix добавим, чтобы указать, что у компонент и директив будет использоваться префикс tl, то есть тэги компонент будут иметь вид
<tl-component-name></tl-component-name>
Посмотрим теперь в angular.json, у нас добавился новый проект
{ ... "projects": { "test-app": { "root": "", "sourceRoot": "src", "projectType": "application", "prefix": "app" ... }, ... "test-lib": { "root": "projects/test-lib", "sourceRoot": "projects/test-lib/src", "projectType": "library", "prefix": "tl" } ... } ... }
В директории проекта появилась следующая структура
- projects - test-lib ng-package.json package.json - src public-api.ts - lib test-lib.component.ts test-lib.module.ts test-lib.service.ts
Также, в tsconfig.json есть добавление в секции paths
"paths": { "test-lib": [ "dist/test-lib" ], "test-lib/*": [ "dist/test-lib/*" ] }
теперь, если запустить приложение,
то мы увидим стандартный работающий шаблон приложения angularng serve
Создание функционала библиотеки
Давайте создадим библиотеку с сервисом, директивой и компонентом. Сервис и директиву разместим в разных модулях. Переместимся в директорию projects/test-lib/src/lib и удалим test-lib.*.ts, также удалим содержимое projects/test-lib/src/public-api.ts.
Переместимся в projects/test-lib/src/lib и создадим модули, директиву, сервис и компонент
ng g module list ng g module border ng g service list /*переходим в list*/ ng g component list /*переходим в border*/ ng g directive border
Наполним компонент, сервис и директиву логикой. Компонент будет отображать поданный на вход список строк. Директива — добавлять красную рамку, сервис будет каждую секунду добавлять в Observable текущий timestamp.
Сервис
/*list.service.ts*/ import {Injectable} from '@angular/core'; import {Observable, Subject} from 'rxjs'; @Injectable({ providedIn: 'root' }) export class ListService { timer: any; private list$: Subject<string> = new Subject<string>(); list: Observable<string> = this.list$.asObservable(); constructor() { this.timer = setInterval(this.nextItem.bind(this), 1000); } nextItem() { const now = new Date(); const currentTime = now.getTime().toString(); this.list$.next(currentTime); } }
Компонент список и модуль
/*list.module.ts*/ import {NgModule} from '@angular/core'; import {CommonModule} from '@angular/common'; import {ListComponent} from './list/list.component'; @NgModule({ declarations: [ ListComponent ], exports: [ ListComponent ], imports: [ CommonModule ] }) export class ListModule { } /*list.component.ts*/ @Component({ selector: 'tl-list', template: ` <ul> <li *ngFor="let item of list">{{item}}</li> </ul>`, styleUrls: ['./list.component.css'], changeDetection: ChangeDetectionStrategy.OnPush }) export class ListComponent implements OnInit { @Input() list: string[]; constructor() { } ngOnInit() { } }
Рамка
/*border.module.ts*/ import {NgModule} from '@angular/core'; import {CommonModule} from '@angular/common'; import {BorderDirective} from './border.directive'; @NgModule({ declarations: [ BorderDirective ], exports: [ BorderDirective ], imports: [ CommonModule ] }) export class BorderModule { } /*border.directive.ts*/ import {Directive, ElementRef, OnInit} from '@angular/core'; @Directive({ selector: '[tlBorder]' }) export class BorderDirective implements OnInit { private element$: HTMLElement; constructor(private elementRef$: ElementRef) { this.element$ = elementRef$.nativeElement; } ngOnInit() { this.element$.style.border = 'solid 1px red'; } }
! Важно. При генерации компонент и библиотек cli не создает export, так что в модулях обязательно добавьте в секции exports те компоненты и директивы, которые должны быть доступны.
Далее, чтобы в будущем классы из библиотеки были доступны, добавим немного кода в public-api.ts
export * from './lib/list.service'; export * from './lib/border/border.module'; export * from './lib/border/border.directive'; export * from './lib/list/list.module'; export * from './lib/list/list/list.component';
Подключение библиотеки в тестовом приложении
Соберем проект библиотеки
ng build test-lib --watch
Далее в app.module заимпортим модули с компонентом и директивой и добавим логику
/*app.module.ts*/ import {BrowserModule} from '@angular/platform-browser'; import {NgModule} from '@angular/core'; import {AppRoutingModule} from './app-routing.module'; import {AppComponent} from './app.component'; import {ListModule} from 'test-lib'; import {BorderModule} from 'test-lib'; /*!!!Обратите внимание, импортим по названию пакета, а не по пути файла*/ @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AppRoutingModule, ListModule, BorderModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
И используем штуки из нашей библиотеки в приложении
import {Component, OnInit} from '@angular/core'; import {ListService} from 'test-lib'; @Component({ selector: 'app-root', template: ` <tl-list [list]="list"></tl-list> <div tlBorder>I am bordered now</div>`, styleUrls: ['./app.component.styl'] }) export class AppComponent implements OnInit { list: string[] = []; constructor(private svc$: ListService) { } ngOnInit() { this.svc$.list.subscribe((value => this.list = [...this.list, value])); } }
Запустим и проверим приложение, все работает:

Сборка и публикация
Осталось собрать и опубликовать пакет. Для сборки и публикации удобно добавить команды в scripts в package.json приложения
{ "name": "test-app", "version": "0.0.1", "scripts": { ... "lib:build": "ng build test-lib", "lib:watch": "ng build test-lib --watch", "lib:publish": "npm run lib:build && cd dist/test-lib && npm pack && npm publish", ... } }
Библиотека собрана, опубликована, теперь после установки в любом ином angular проекте
npm install test-lib
можно использовать компоненты и директивы.
Небольшое примечание
У нас в компании есть целое семейство npm пакетов, поэтому в нашем случае пакет должен публиковаться с namespace как company/test-lib. Для этого сделаем всего несколько правок.
В package.json библиотеки переименуем пакет
/* projects/test-lib/package.json */ { "name": "@company/test-lib", "version": "0.0.1", "peerDependencies": { "@angular/common": "^7.2.0", "@angular/core": "^7.2.0" } }
И чтобы в тестовом приложении библиотека была доступна по названию с namespace немного поправим tsconfig
/* test-app/tsconfig.json */ *** "paths": { "@company/test-lib": [ "dist/test-lib" ], "@company/test-lib/*": [ "dist/test-lib/*" ] } ***
И в тестовом приложении заменить импорты, например
import {ListModule} from 'test-lib';
Заменим на
import {ListModule} from '@company/test-lib';
На этом закончим.
P.S.: При изучении данной темы я в свое время читал следующие статьи
The Angular Library Series
How to built npm ready component library with Angular
