blexin

Sviluppo
Consulenza e
Formazione IT


Blog

Evolvi la tua azienda

Vediamo come possiamo riutilizzare i nostri componenti in più progetti con le librerie di Angular

Creare librerie di componenti Angular

Mercoledì 8 Gennaio 2020

Capita spesso di dover risolvere un problema già risolto in un progetto precedente con un classico copia e incolla del codice. Tale approccio, ovviamente, fornisce una soluzione rapida al problema, ma può innescare una serie di effetti collaterali, tra cui il peggiore di tutti è sicuramente scoprire la presenza di un bug e doverlo correggere in tutti i progetti in cui abbiamo copiato il codice. E se poi volessi aggiungere una nuova funzionalità? Dovrò propagarla ovunque?

Nei progetti Angular su cui ho lavorato, è sempre stato necessario creare un componente che fosse in grado di prendere dei dati e mostrarli in forma tabulare. Perché non rendere questo componente riutilizzabile in più progetti? Angular offre questa possibilità con le Angular Library. Nell’articolo precedente , ho mostrato come creare un componente che visualizzi dei dati in forma tabellare, e permetta il drag and drop delle colonne: rendiamo questo componente riutilizzabile in tutti i nostri progetti creando una libreria!

Dalla CLI di Angular lanciamo i seguenti comandi:

ng new BlexinLibrary

In merito alla creazione di una libreria, la documentazione ufficiale di Angular ci dice di impostare il flag --create-application a false per indicare alla CLI che non deve creare un’applicazione iniziale ma semplicemente un workspace vuoto. Non è però la soluzione che ho scelto e che preferisco, perché diventa poi difficile fare il debug della libreria.
Quello che invece vi consiglio, è creare una classica applicazione Angular che vi faccia da host e al suo interno sviluppare la libreria: in questo modo potrete testarla prima di effettuarne il rilascio. Nella cartella radice del progetto, lanciamo il comando per creare la libreria:

ng generate library DataTable

Notiamo che l’Angular CLI ha creato una cartella projects con, al suo interno, una cartella col nome della libreria (DataTable). Nella sottocartella data-table/src, troviamo una cartella lib che conterrà i file che compongono il progetto (componenti, servizi, moduli, etc), più un file public-api.ts che dichiarerà cosa vogliamo rendere disponibile agli utilizzatori della nostra libreria.

/*
* Public API Surface of data-table
*/
 
export * from './lib/data-table.service';
export * from './lib/data-table.component';
export * from './lib/data-table.module';

Scopriamo così che nella libreria è già stato creato un component, un service e un modulo. Il file di configurazione dell’intero workspace, angular.json, viene aggiornato con un nuovo progetto di tipo library che ha il nome scelto durante la creazione della libreria.

{
 "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
 "version": 1,
 "newProjectRoot": "projects",
 "projects": {
   "BlexinLibrary": {
     "root": "",
     "sourceRoot": "src",
     "projectType": "application",
     "prefix": "app",
     ...
   },
   "BlexinLibrary-e2e": {
     "root": "e2e/",
     "projectType": "application",
     "prefix": "",
     ...
   },
   "DataTable": {
     "root": "projects/data-table",
     "sourceRoot": "projects/data-table/src",
     "projectType": "library",
     "prefix": "lib",
     "architect": {
       "build": {
         "builder": "@angular-devkit/build-ng-packagr:build",
         "options": {
           "tsConfig": "projects/data-table/tsconfig.lib.json",
           "project": "projects/data-table/ng-package.json"
         }
       },

A questo punto, procediamo scrivendo codice come in un progetto Angular standard. Prendendo spunto dal mio articolo precedente , eliminiamo i file che ci ha creato la CLI lasciando data-table.component.ts e data-table.module.ts, e passiamo alla costruzione del component.

Nell’articolo precedente, la tabella non era un componente a sé ma faceva parte dell’app.component: quindi, un passaggio obbligatorio è quello di estrarre il component data-table dall’app.component.
Osservando il template, ci accorgiamo che data-table opera fondamentalmente su due strutture dati: columns e rows. Queste due strutture dati devono esserci fornite da chi utilizza la libreria, diventando quindi i nostri Input. Il file .ts del nostro component sarà il seguente:

import { Component, Input } from '@angular/core';
 
@Component({
 selector: 'blx-data-table',
 templateUrl: './data-table.component.html',
 styleUrls: ['./data-table.component.css']
})
export class DataTableComponent {
 @Input() columns: string[];
 @Input() rows: any[];
 
 constructor() { }
}

Modifichiamo il selettore da lib-DataTable a blx-data-table, andando anche a modificare il prefix nel file tslint.json:

{
 "extends": "../../tslint.json",
 "rules": {
   "directive-selector": [
     true,
     "attribute",
     "blx",
     "camelCase"
   ],
   "component-selector": [
     true,
     "element",
     "blx",
     "kebab-case"
   ]
 }
}

Di seguito potete vedere anche il template del nostro componente:

<div class="p-5 m-5">
 <div class="p-5 m-5">
 <div class="table-responsive table-container">
   <table class="table">
     <thead class="thead-dark">
       <tr dragula="table_columns" [(dragulaModel)]="columns">
         <th *ngFor="let column of columns">
           {{column}}
         </th>
       </tr>
     </thead>
     <tbody>
       <tr *ngFor="let row of rows">
         <td *ngFor="let column of columns">
           {{row[column]}}
         </td>
       </tr>
     </tbody>
   </table>
 </div>
</div>

La libreria esterna che stiamo utilizzando per il drag&drop necessita di alcune classi CSS, per poter mostrare l’effetto desiderato. Nel progetto precedente, avevo messe queste classi nello style.css. Ora, però, non voglio che chi importa la libreria debba preoccuparsi di aggiungere tali classi al suo progetto: devono già essere presenti nella libreria stessa.
Le includiamo quindi nel file data-table.component.css.

Come già detto, il file public_api.ts permette di definire ciò che i nostri utilizzatori vedranno della libreria. Andiamo quindi a modificarlo, per tener conto della cancellazione del service:

export * from './lib/data-table.component';
export * from './lib/data-table.module';

Il componente data-table necessita di alcune dipendenze esterne: ng2-dragula (gestione drag&drop) e bootstrap (classi CSS). È necessario, quindi, comunicare a chi installa la libreria che, per il corretto funzionamento, ha bisogno di aggiungere al proprio progetto tali dipendenze. Modifichiamo quindi il file package.json della cartella data-table, aggiungendole alla sezione peerDependencies.

{
 "name": "data-table",
 "version": "0.0.1",
 "peerDependencies": {
   "@angular/common": "^7.0.0",
   "@angular/core": "^7.0.0",
   "bootstrap": "^4.3.1",
   "ng2-dragula": "^2.1.1"
 }
}

Prima della pubblicazione, dobbiamo assicurarci che il tutto funzioni correttamente. La scelta iniziale di creare un progetto Angular, e non un workspace vuoto, ora ci dà un enorme vantaggio: per poter testare la libreria è sufficiente importarne il modulo nel nostro app.module e utilizzare il component mediante il selettore. Ecco come diventa il nostro app.component.html:

<div style="text-align:center">
 <h1>
   Welcome to {{ title }}!
 </h1>
</div>
<blx-data-table [columns]="columns" [rows]="rows"></blx-data-table>

dove column e row vengono definite in app.component.ts e poi passate in input al data-table. Prima di poter lanciare la nostra applicazione, occorre installare le due dipendenze della nostra libreria (con le istruzioni npm install ng2-dragula e npm install bootstrap), aggiungere "node_modules/bootstrap/dist/css/bootstrap.css" tra gli style del nostro angular.json ed infine aggiungere (window as any).global = window; nel file polyfills.ts come suggerito nella documentazione di Dragula.

Lanciamo quindi l’applicazione con il classico ng serve

Verificato che tutto funziona, possiamo distribuirla. Compiliamo la libreria, aggiungendo al solito comando di build il nome della libreria:

ng build DataTable

A fine compilazione, la libreria sarà disponibile nella cartella dist.

Come utilizzare la libreria appena creata

Abbiamo due possibilità: importarla in un nuovo progetto prelevandola dal suo folder dist oppure renderla disponibile assieme ai suoi aggiornamenti utilizzando lo stesso ecosistema scelto da Angular: npm.
Creiamo un nuovo progetto Angular:

ng new demo-with-lib

All’interno della struttura del progetto appena creato, andiamo a creare una nuova cartella lib dove copiamo la cartella contenente il risultato della build della libreria. Posso quindi installarla con il comando:

npm i ./libs/data-table

Aggiungiamo anche le dipendenze necessarie al suo corretto funzionamento ed importiamo il modulo DataTableModule nel app.module.ts per poter utilizzare il component esposto.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { DataTableModule } from 'data-table';
import { AppComponent } from './app.component';
 
@NgModule({
 declarations: [
   AppComponent
 ],
 imports: [
   BrowserModule,
   DataTableModule
 ],
 providers: [],
 bootstrap: [AppComponent]
})
export class AppModule { }

Posizioniamo il componente con l’opportuno tag blx-data-table nel app.component.ts, fornendo gli Input attesi:

<div class="text-center">
 <h1>
   Welcome to {{ title }}!
 </h1>
</div>
<blx-data-table [columns]="columns" [rows]="rows"> </blx-data-table>

Lanciamo l’applicazione e verifichiamo il corretto funzionamento.

Abbiamo fatto dei progressi, nel senso che possiamo riutilizzare il componente ovunque vogliamo. In realtà, abbiamo di nuovo fatto ricorso a un copia e incolla: non più del codice sorgente ma del folder di distribuzione della libreria. E se volessimo permettere ad altri elementi del team di utilizzare la libreria, fornendo uno strumento semplice per seguire i suoi aggiornamenti? E se volessi renderla pubblica, ossia permettere a chiunque nel mondo di installarla tramite npm?

All’indirizzo https://docs.npmjs.com/creating-and-publishing-private-packages , è disponibile il tutorial ufficiale per pubblicare un package sul registy pubblico npm. Seguiamo invece una strada diversa, offerta da qualche mese a questa parte da Github.

GitHub offre la possibilità, mediante GitHub Packages, di creare un proprio repository, dove pubblicare e distribuire la propria libreria in maniera privata con il proprio team oppure pubblica.

Pubblicare su GitHub Packages

Per poter pubblicare, installare ed eventualmente eliminare un package da GitHub Packages è necessario disporre di un access token. Ci sono due strade per ottenerne uno: utilizzando l’Access Token personale con la propria username per autenticarsi su GitHub, oppure utilizzando un GITHUB_TOKEN per autenticarsi su GitHub Actions.

Ho scelto la strada dell’access token personale. Occorre quindi modificare il file ~/.npmrc ed includere la seguente riga:

//npm.pkg.github.com/:_authToken=TOKEN

sostituendo “TOKEN” con il proprio token.

A questo punto, occorre creare il file .npmrc nella stessa cartella dove è presente il package.json, nella quale inserire la seguente riga:

registry=https://npm.pkg.github.com/OWNER

dove OWNER è il nome dell’account Git del repo. Aggiungiamo lo stesso file anche sul repo e controlliamo che nel package.json sia specificato il nome del pacchetto, la versione e il repo dove risiede il codice sorgente.

{	
	 "name": "@accountgit/packagename",
	 "version": "2.0.1",
	 "peerDependencies": {
	   "@angular/common": "^8.2.14",
	   "@angular/core": "^8.2.14",
	   "bootstrap": "^4.3.1",
	   "ng2-dragula": "^2.1.1",
	   ...
	 },
	 "repository": {
	   "type": "git",
	   "url": "git+https://github.com/accountgit/reponame.git"
	 }
	}

Possiamo pubblicarlo con il comando:

$ npm publish

A seguito della pubblicazione, nel nostro repository troviamo popolata la sezione package che ci porta a una sezione più dettagliata su quanto è stato pubblicato.

Come modificare la libreria

Come possiamo gestire il normale ciclo di vita della nostra libreria? La correzione dei bug, l’aggiunta di una nuova feature, una modifica sostanziale che la renda incompatibile con le versioni precedenti? Tutto ciò, consentendo a tutti quelli che hanno installato la libreria di aggiornare il pacchetto nei propri progetti in maniera sicura.

È il momento di gestire il versionamento della mia libreria. Nel package.json troviamo il campo version, che definisce la versione attuale della libreria. Utilizzando Semantic Version, possiamo rendere chiaro a chi utilizza la nostra libreria quali versioni del nostro pacchetto può installare in maniera sicura, ovvero senza la necessità di dover apportare modifiche al proprio codice per integrare la nostra libreria.
In questo modo, il campo version conterrà una stringa formattata nel modo seguente: “major.minor.patch”, in cui ogni blocco corrisponde ad un numero intero non negativo. Vi rimando all’ articolo di Antonio per una spiegazione dettagliata

Tornando alla nostra modifica: per la risoluzione dei bug occorrerà quindi modificare la patch del nostro numero di versione, incrementando di uno. Allo stesso modo, dopo aver aggiunto una nuova funzionalità che sia compatibile con il codice attuale, occorrerà aggiornare minor incrementando di uno. Major sarà incrementato quando verranno apportate delle modifiche che impattano sull’API, come ad esempio, nel nostro caso, la modifica di un nome di una proprietà di INPUT. Terminate le modifiche, possiamo pubblicare il pacchetto aggiornato con lo stesso comando utilizzato in precedenza $ npm publish

Per i client che utilizzano la nostra libreria, a seguito del comando npm install, l’aggiornamento della stessa (ottenuto lanciando il comando npm update)dipende dalla configurazione della dipendenza nel package.json.

Ci sono due possibili configurazioni

  • “nome_del_pacchetto”: “~major.minor.patch”
  • “nome_del_pacchetto”: “^major.minor.patch”

Nel primo caso, verrà scaricato il pacchetto che ha lo stesso major e minor ma con l’ultima versione di patch disponibile. Nel secondo caso, invece, verrà scaricato il pacchetto che ha lo stesso major ma con l’ultima versione di minor e patch disponibile.

Con l’utilizzo della tilde(~) stiamo quindi indicando di mantenere il nostro pacchetto a meno di bug fix, mentre utilizzando il caret(^) indichiamo la volontà di aggiornare il pacchetto anche in caso di aggiunta di nuove funzionalità compatibili con la versione attualmente installata.

Notate che, se non è stato ancora installato il pacchetto, npm install avrà lo stesso effetto di npm update, ovvero quello di andare a trovare il pacchetto più aggiornato in base alle direttive date nel package.json. Il discorso cambia, se già sono stati installati pacchetti in locale: in quel caso non è sufficiente npm install per aggiornare il pacchetto, perché quello trovato in locale rispetta già le direttive date, va quindi utilizzato npm update.

È possibile, infine, specificare che si vuole installare l’ultima versione disponibile indipendentemente da quella installata con il comando $ npm install packagename@latest –save
In questo modo, verrà aggiornato anche il package.json con il numero di versione del pacchetto scaricato.

L’intero codice è su github al seguente indirizzo: https://github.com/AARNOLD87/AngularLibrary

Al prossimo articolo!

Autore

Servizi

Evolvi la tua azienda