Angular: создание и публикация библиотеки +12


Начнем с начала


Если мне не изменяет память, то с версии 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




К сожалению, не доступен сервер mySQL