Crear una librería Angular con angular-cli y ng-packagr

14
13032

Índice de contenidos


1. Entorno

Este tutorial está escrito usando el siguiente entorno:

  • Hardware: Slimbook Pro 13.3″ (Intel Core i7, 32GB RAM)
  • Sistema Operativo: LUbuntu 16.04
  • Visual Studio Code 1.16.1
  • @angular/cli 1.6.0
  • @angular 5.0.1


2. Introducción

Seguro que a estas alturas ya estáis cansados de escucharme decir que una de las grandezas de Angular es que es altamente reutilizable a través de sus módulos y que podemos encapsularlos en librerías para poder ser utilizadas en otros proyectos de Angular.

Mi consejo es que esta librería tenga el mínimo código de HTML y CSS posible y que se centre en la lógica que quiera resolver y una interfaz «fea» de pruebas.

De esta forma podemos implementar una librería con toda la lógica perfectamente testeada y que podremos reutilizar en proyectos de naturaleza Angular como Ionic y con ciertas restricciones también en NativeScript.

Con esto marcamos la separación entre la lógica y la forma de presentar la solución haciendo que cada rol (desarrollador/arquitecto vs diseñador/ux) invierta el tiempo en lo que realmente es productivo.

Ya os digo yo que cuando tengo que tocar CSS y HTML para que la aplicación quede «bonita» me vuelvo altamente improductivo.


3. Vamos al lío

Existen generadores de Yeoman que facilitan la creación de librerías en Angular: uno de ellos es «generator-angular-library» de mattlewis92 y el otro más conocido es «generator-angular2-library» de jvandemo. Ambos pueden ser útiles pero para mí la forma más cómoda es seguir utilizando la misma herramienta que utilizamos para la implementación de aplicaciones con Angular CLI.

El caso es que a día de hoy no existe un soporte oficial del CLI para la creación de librerías; así que nos tenemos que apoyar en un proyecto llamado «ng-packagr» y seguir los siguientes pasos desde cero.

Para la creación de la librería simplemente creamos un nuevo proyecto con el comando ng.

$> ng new my-lib-poc

Una vez creado el proyecto podemos abrirlo con el editor de texto y crear un módulo secundario como ya sabemos con la lógica que queramos compartir, en este caso, lo vamos a simplificar al máximo, porque el objetivo es crear una librería no importa tanto el contenido de la misma.

$> npm run ng -- g module header

Vamos a crear un componente asociado al módulo:

$> npm run ng -- g component header/header

Y un servicio también asociado al módulo:

$> npm run ng -- g service header/header

Nota: Como ves no perdemos las ventajas de trabajar con Angular CLI pudiendo hacer uso de los comandos de generación.

Ahora editamos el fichero header.service.ts para añadir un método que devuelva el texto del header.

export class HeaderService {

  constructor() { }

  getHeader(): string {
    return 'Header service';
  }

}

Este servicio lo instanciamos en el componente, para ello editamos el fichero header.component.ts y establecemos el valor en una propiedad del componente.

import { HeaderService } from './header.service';
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-header',
  template: `
      <h1>
        {{header}}
      </h1>
  `,
  styles: ['h1 { font-weight: normal; }']
})
export class HeaderComponent implements OnInit {

  header: string;

  constructor(private service: HeaderService) { }

  ngOnInit() {
    this.header = this.service.getHeader();
  }

}

Nota importante: si estás usando una versión de Angular inferior a la 6 para tu librería es imprescindible que tanto el HTML como el CSS están definidos de manera «inline» como ves en el ejemplo de arriba. Si no verás que a la hora de cargar la librería en otra aplicación aparecerá un mensaje por consola indicando con un 404 que no encuentra el HTML o el CSS asociados al componente.

Y editamos el fichero header.module.ts para permitir a los usuarios de nuestra librería poder hacer uso del componente HeaderComponent, declarándolo en el array de «exports» y establecemos en el array de «providers» el servicio para poder hacer uso de él de forma interna. En caso de permitir hacer uso del servicio de forma externa tenemos que declarar la función forRoot(), como vemos en el siguiente código:

import { HeaderService } from './header.service';
import { ModuleWithProviders, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HeaderComponent } from './header.component';

@NgModule({
  imports: [
    CommonModule
  ],
  declarations: [HeaderComponent],
  exports: [HeaderComponent],
  providers: [HeaderService]
})
export class HeaderModule {
  public static forRoot(): ModuleWithProviders {
    return {
      ngModule: HeaderModule,
      providers: [
        HeaderService
      ]
    };
  }
}

Nuestra librería tiene implementada toda la funcionalidad que queremos compartir, podemos hacer uso del componente en el fichero app.component.html, importarla en el módulo principal y arrancar la aplicación para verificarlo.

Llega el momento de querer compartir nuestra librería con el mundo,
o por lo menos con los desarrolladores de nuestra empresa. Para ello necesitamos incluir en nuestro proyecto la siguiente dependencia de desarrollo:

$> npm install --save-dev ng-packagr

Esta librería necesita que creemos dos ficheros en la raíz del proyecto: el primero llamado «ng-package.json» donde indicamos el esquema que tiene que utilizar la librería y donde se encuentra el segundo de los ficheros:

{
  "$schema": "./node_modules/ng-packagr/ng-package.schema.json",
  "lib": {
    "entryFile": "public_api.ts"
  }
}

El segundo fichero «public_api.ts» define todos los exports de nuestra librería y se utiliza para generar los .d.ts adecuados.

export * from './src/app/header/header.module';
export * from './src/app/header/header.service';
export * from './src/app/header/header.component';

Ahora editamos el fichero package.json para poner la propiedad private a false, ya que se quiere publicar en algún repositorio de npm ya sea público o privado, y añadimos en la sección de scrips un «task» de npm para ejecutar la herramienta de empaquetado. Además todas las dependencias del proyecto las ponemos como peerDependencies para que sean tenidas en cuenta por el proyecto que vaya a utilizar nuestra librería:

...
"scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
    "packagr": "ng-packagr -p ng-package.json"
  },
  "private": false,
  "peerDependencies": {
    "@angular/animations": "^5.0.0",
    "@angular/common": "^5.0.0",
    "@angular/compiler": "^5.0.0",
    "@angular/core": "^5.0.0",
    "@angular/forms": "^5.0.0",
    "@angular/http": "^5.0.0",
    "@angular/platform-browser": "^5.0.0",
    "@angular/platform-browser-dynamic": "^5.0.0",
    "@angular/router": "^5.0.0",
    "core-js": "^2.4.1",
    "rxjs": "^5.5.2",
    "zone.js": "^0.8.14"
  },
  ...

Ahora simplemente ejecutamos:

$> npm run packagr

Y si el proceso es correcto nos generará una carpeta dist con el contenido de nuestra librería listo para publicar y preparado para soportar AOT; además genera los ficheros d.ts del API, los bundles UMD para su ejecución con SystemJS y los ficheros en es5 necesarios para su ejecución en el navegador.

Ahora solo tenemos que entrar dentro de la carpeta dist y ejecutar «npm publish» con los permisos necesarios en el repositorio corporativo o público que tengamos configurado y la librería ya puede ser consumida por nuestros usuarios, simplemente ejecutando en sus proyectos:

$> npm install --save my-lib-poc

Una vez que la librería está instalada ya podemos hacer uso de ella en nuestro proyecto, simplemente declarando el módulo creado en la librería en la sección imports de cualquier @NgModule de la aplicación que vaya a hacer uso de las funcionalidades de la librería.

import {HeaderModule} from ‘my-lib-poc’;

@NgModule({
  ..
  imports: [HeaderModule],
  ..
})


4. Conclusiones

Hasta que exista una solución más oficial por parte de Angular el uso de ng-packagr nos puede ayudar a crear librerías corporativas para reutilizar en nuestros desarrollos con este framework, de una manera muy limpia y cómoda.

Recordad que esta técnica y otras muchas más las encontraréis en la guía práctica de Angular y también ofrecemos cursos in-house y online.

Cualquier duda o sugerencia en la zona de comentarios.

Saludos.

14 COMENTARIOS

  1. Hola buenas tardes, tengo una duda, luego de crear la libreria la quiero tener de manera local en mi maquina sin subirla a ningún repositorio, como puedo incluir la libreria dentro de mis proyectos locales?

    Saludos

    • Hola Kendall,

      En teoría podrías ejecutar dentro del proyecto que vaya a hacer uso de la librería:


      $> npm install --save /path/to/library

      Donde el path apunta a la carpeta que contenga el package.json de la librería (no del proyecto con el que has generado la librería). Digo en teoría porque yo me he encontrado con situaciones en las que haciéndolo de esta forma he tenido problemas de ejecución en el proyecto que hace uso de la librería, que haciéndolo a través de Nexus o a través del registro público de NPM no he tenido.

      Prueba y a ver si tienes suerte 🙂

      Saludos

  2. Hola,

    Gracias por el post, es muy completo.
    Tras haber hecho una librería me encuentro con que el interceptor de la aplicación que la utiliza no captura las peticiones correctamente.
    ¿Conocerías alguna solución al respecto? ¿Tendría que hacer un interceptor específico para la librería?

    Gracias de antemano.

    • Hola Elena,

      Sin conocer el contenido de la librería es complicado darte una respuesta clara… asumo que la librería contiene servicios de proxy (los que utilizan el servicio HttpClient) para recuperar información de un servidor.

      Para poder hacer uso de estos servicios desde la aplicación antes tienes que exponerlos implementando una función forRoot con la propiedad providers de este modo:


      @NgModule({
      imports: [
      CommonModule
      ],
      declarations: [HeaderComponent],
      exports: [HeaderComponent],
      providers: [HeaderService]
      })
      export class HeaderModule {
      public static forRoot(): ModuleWithProviders {
      return {
      ngModule: HeaderModule,
      providers: [
      HeaderService
      ]
      };
      }
      }

      Y dentro del módulo principal de la aplicación o donde vayas a hacer uso de la librería, en la propiedad imports poner HeaderModule.forRoot() de forma que esos providers se añaden al injector principal de tu aplicación. Nota: en caso de que estés utilizando la versión 6 de Angular, con el providerIn ya vienen declarados por defecto y esto no sería necesario.

      En el caso de librería que planteo (no se si es exactamente tu caso) lo normal suele ser tener el interceptor a nivel de librería y declarado de forma interna (sin estar en el forRoot()) de forma que la aplicación ni siquiera tenga que declarar el HttpClientModule; este podría ser un ejemplo:


      @NgModule({
      imports: [
      CommonModule,
      HttpClientModule
      ],
      declarations: [],
      providers: [
      TntLibProxyService,
      TntLibProxyServiceFake,
      {provide: HTTP_INTERCEPTORS, useClass: TntLibAuthInterceptor, multi: true},
      {provide: HTTP_INTERCEPTORS, useClass: TntLibErrorInterceptor, multi: true},
      ]
      })
      export class TntLibModule {
      public static forRoot(config: ConfigLib): ModuleWithProviders {
      return {
      ngModule: TntLibModule,
      providers: [
      {provide: 'config', useValue: config}
      ]
      };
      }
      }

      Espero que con esto te haya orientado en la solución.

      Saludos

  3. Hola, muy bueno tu post!!

    Soy nueva en Angular y me encuentro desarrollando una aplicación que será común para otras aplicaciones y por eso he llegado a este punto. Al grano

    Luego de ejecutar ‘npm install –save my-lib-poc’ como puedo consumirlo? Sorry si la pregunta es muy basica!

    • Hola Rebe,

      Una vez que la librería está instalada en el proyecto donde la vas a usar, lo que tienes que hacer es importar el módulo que has creado en la librería en algún @NgModule de tu proyecto dentro de la propiedad imports.

      Voy a añadir esta parte al tutorial con la esperanza de que quede más claro.

      Muchas gracias por el feedback!

  4. Saludos buen día, muy buena publicación.
    Mi consulta tengo un proyecto demasiado grande en angular. Ahora lo que necesito es transformar un módulo del ese proyecto en una librería, ese modulo tiene sub módulos, componentes, modelados, servicios etc., Para poderlo transformar en una librería con ng-packagr
    1 tendría que pasar a un nuevo proyecto el modulo?
    2 tengo que respetar alguna estructura de datos (como con Yeoman) o suficiente con que el proyecto sea funcional?
    3 Como comparto los enviroments estilos, etc entre una librería y el proyecto que lo consume?.

    Mi problema se centra en probar la librería, que haga lo que tenga que hacer, para solo asi publicarla. Entonces quiero tener claro que herramienta usar (yeoman, ng-packagr u otra)
    Gracias por tu ayuda.

    • Hola Jaime,
      En tu caso, dado que tienes un proyecto muy grande que quieres modularizar, lo que te recomendaría es echarle un vistazo a la tecnología NX, aquí tienes una pequeña introducción (https://www.adictosaltrabajo.com/2018/05/23/primeros-pasos-con-nx/). Esta tecnología está más orientada a la modularización de las aplicaciones pero desde un único repositorio y te permite hacer lo que comentas de compartir los estilos y los environments de una forma más sencilla.

      Espero que te sea de ayuda.

      Saludos

  5. Buen día,
    Cuando se crea el *.d.ts no me funciona me dice que no encuentra en angular Core y no me deja pasar de ahí

    import { OnInit } from ‘@angular/core’;
    export declare class LoginComponent implements OnInit {
    constructor();
    ngOnInit(): void;
    }

    • Hola Cris Portillo,

      Entiendo que el error se está produciendo cuando ejecutas el comando «npm run packagr», asegúrate de haber cambiado la propiedad dependencies a pearDependencies en el fichero package.json de la raíz del proyecto.

      En caso de que estés utilizando Angular por encima de la versión 6, te recomiendo que no hagas las librerías de esta forma, ya que a partir de la versión 6 el Angular-CLI tiene soporte para la creación de librerías.

      En caso de querer modularizar un único proyecto, te recomiendo también que le eches un vistazo a la tecnología NX (https://www.adictosaltrabajo.com/2018/05/23/primeros-pasos-con-nx/)

      Espero que te sea de ayuda.

      Saludos

  6. Buenos días, siguiendo tus indicaciones he creado una librería para uso corporativo.
    Me surge un problema, tras la primera publicación, he realizado distintas modificaciones y tras volver a empaquetar con un nuevo nombre de version 0.0.1 y publicar, ha dejado de fucoinar en el proyecto en el que estaba usándola.

    Al parecer, por lo que he visto, me he encontrado que la estructura de directorios es distinta entre la primera version 0.0.0 y la posterior/posteriores 0.0.1.

    ¿Hay que lanzar algún comando distinto para publicar versiones posteriores?

    Muchas gracias.

    • Hola Sergi,

      El caso que planteas me suena raro porque no hay que hacer nada especial o distinto para el resto de publicaciones. El error puede estar en la forma de compartir la libreŕía, lo más seguro es hacerlo a través del registro público de NPM o si es privado a través de un registro corporativo implementado con Nexus (por ejemplo), si que me he encontrado algún mal funcionamiento si no se hace por estos medios y se hace de forma manual estableciendo el path en el package.json del proyecto que vaya a utilizar la librería.

      Dinos si esto resuelve tu problema o manda algo más de información del problema.

      Gracias por seguirnos y saludos!

  7. Hola Rubén, he creado mi propia librería y la he subido a un repositorio privado nexus. Desde ahí la descargo a mi proyecto sin problemas. el problema viene a la hora de añadir imágenes a mi librería. Conoces alguna forma de meter imágenes e iconos en la librería? Gracias.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad