Начнем с начала
Если мне не изменяет память, то с версии 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/*"
]
}
теперь, если запустить приложение,
ng serve
то мы увидим стандартный работающий шаблон приложения angularСоздание функционала библиотеки
Давайте создадим библиотеку с сервисом, директивой и компонентом. Сервис и директиву разместим в разных модулях. Переместимся в директорию 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