1 Commento

Divide (in zone) et impera

Angular e zone.js

di

thumbnail

05

lug

2017

Inclusione di librerie esterne, zone.js per il change detection e compilazione dinamica di moduli. Un esempio da programmatori.

Nel libro Sviluppare applicazioni con Angular abbiamo discusso di come Angular effettui il change detection, ma senza fare un esempio pratico. Tratteremo anche un altro argomento che più volte mi è stato sollecitato, ovvero la possibilità di includere dinamicamente uno script esterno.

Zone.js è una libreria sviluppata dal team Angular ispirata da Dart, il linguaggio che implementa la gestione delle zone. Immaginiamo le zone come contenitori in cui verranno effettuate chiamate asincrone.

Angular fa uso di questa libreria per sapere cosa effettivamente sia stato eseguito in maniera asincrona ed è in grado di rilevare gli eventuali cambiamenti. Il sistema di base è piuttosto semplice quanto ingegnoso. Ogni chiamata che genera una routine asincrona viene grabbata, ingabbiata dalla libreria, affinché al suo termine generi un change detection. Rispetto alla versione precedente, la propagazione del detection avviene in maniera unidirezionale dalla radice verso i figli.

Questo sistema garantisce una propagazione uniforme, cosa che non accadeva con AngularJS, dove non era certo chi avrebbe eseguito il detection per primo. Chi ha lavorato con AngularJS ricorderà l’uso smodato di cicli digest causati da questo problema. Angular è più rigido nel binding ed in generale nello sharing soprattutto per questo motivo, onde evitare che la pioggia di two-way data binding generi il caos che ha collassato la vecchia versione del nostro amato framework. Procediamo con il nostro esempio.

app.service.ts

import {Injectable} from '@angular/core';
import {Observable} from 'rxjs/Observable';

declare var document: any;

@Injectable()
export class LoadMapService {
  constructor() {}
  load(): Observable {
    return new Observable(observer => {
      const script = document.createElement('script');
      script.type = 'text/javascript';
      document.getElementsByTagName('head')[0].appendChild(script);
      script.src = 'https://maps.googleapis.com/maps/api/js?key=KEY';
      script.onload = function(){ observer.next(true); observer.complete(); };
      script.onerror = function(){ observer.next(false); observer.complete(); };
    });
  }
}

 

Questa tecnica è abbastanza usata ma, a oggi, è preferibile sicuramente gestire le dipendenze tramite module loader come webpack o systemjs, che permettono la gestione lazy (on demand) dei moduli. In questo contesto, la gestione della libreria via iniezione dello script nello head risulta ugualmente accettabile, in quanto lo script in questione sarà il fulcro della nostra applicazione e non un aspetto secondario.

Il metodo load della classe LoadMap crea un elemento script di tipo text/javascript. AppendChild appende script nello head e imposta la sua proprietà src con il link corrispondente allo script da importare.

Terminata questa routine, il browser troverà questo codice all’interno del DOM e potrà solo elaborarlo.


Gli eventi onload e onerror vengono evocati al termine del caricamento dello script. Nel primo caso l’import da parte del browser avrà esito positivo; diversamente, verrà evocato onerror.

L’observer emetterà un valore corrispondente all’elaborazione dello script (true, false) e concluderà lo stream. Per venire elaborato, un Observable vuole all’altro capo dello stream un subscribe. Nel nostro caso il subscribe notificherà che l’oggetto google è disponibile. Dovevamo lavorare così per avere la certezza che al momento dell’evocazione l’oggetto google si trovasse già all’interno del DOM.

Implementare Google Maps nel codice che stiamo per analizzare era assolutamente propedeutico per affrontare il nostro esempio. Procediamo.

app.component.ts

import {
  Component,
  ViewContainerRef,
  AfterContentInit,
  ComponentFactoryResolver,
  AfterViewInit,
  NgZone
 } from '@angular/core';
 import { LoadMapService } from './app.service';

declare var document, google, window;

 @Component({
   selector: 'app-injecthtml',
   template: '
TEST: {{test|json}}
' }) export class InjectHTMLComponent { test: any; } @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements AfterViewInit { componentRef: any; map: any; constructor( private zone: NgZone, private viewContainer: ViewContainerRef, private componentResolver: ComponentFactoryResolver, private loadMap: LoadMapService ) { const componentFactory = this.componentResolver.resolveComponentFactory(InjectHTMLComponent); this.componentRef = this.viewContainer.createComponent(componentFactory); } ngAfterViewInit() { this.loadMap.load().subscribe((bool) => { if (!bool) { return; } this.map = new google.maps.Map(document.getElementById('map'), { center: {lat: -34.397, lng: 150.644}, zoom: 8 }); const marker = new google.maps.Marker({ position: {lat: -34.397, lng: 150.644}, map: this.map }); marker.addListener('click', () => { (this.componentRef.instance).test = {test: 'Google Maps'}; }); document.getElementById('btn').addEventListener('click', ( event ) => { (this.componentRef.instance).test = {test: 'Button'}; }, false); }); } onClick() { (this.componentRef.instance).test = {test: 'Button 2'}; } }

 

Sviluppare applicazioni con Angular

Pronto per qualsiasi sfida di uso di Angular.

 

In questo esempio implementiamo un concetto che in AngularJS era richiamato tramite il servizio $compile. Lo scopo è fare elaborare un template Angular dopo che il browser abbia terminato la compilazione.

Immaginiamo di creare un elemento DOM e al suo interno inserire un’interpolazione, un semplice {{Hello}}. Il browser, una volta avviata l’applicazione, avrà già terminato la compilazione e di conseguenza agirà, da quel momento in poi, fuori dall’ecosistema Angular. L’interpolazione non verrà elaborata da Angular poiché non è stata compilata in Angular. Scenari di questo tipo sono piuttosto comuni, soprattutto quando si scrivono direttive che agiscono dinamicamente sul DOM.

La classe ComponentFactoryResolver viene in nostro aiuto, nel predisporre un’istanza dinamica del componente esterno InjectHTMLComponent, che porta con sé il suo template di riferimento. Questa operazione viene finalizzata istanziando il componente InjectHTMLComponent tramite il metodo createComponent. In questo modo il template del componente iniettato verrà ancorato alla view corrente. Il riferimento alla view è dato dalla classe ViewContainerRef.

app.component.html

<div id="map"></div>

<button id="btn"></button>
<button (click)="onClick()"></button>

 

Avviando l’applicazione (ricordiamoci di inserire i servizi ed i componenti in app.modules) importante ricordarsi di specificare il componente che utilizzeremo dinamicamente all’interno dell’array entryComponents in @NgModule, oltre che in declarations.

@NgModule({
  declarations: [
    AppComponent,
    InjectHTMLComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  entryComponents: [
    InjectHTMLComponent
  ],
  providers: [LoadMapService],
  bootstrap: [AppComponent]
})
export class AppModule {
}

 

Google Maps implementato in Angular

Lavoro sulle mappe di Google attraverso Angular e zone.js.

 

La stringa contenuta in TEST: farà riferimento alla proprietà test di InjectHTMLComponent. Valorizziamo la proprietà nel componente iniettato tramite la sua istanza cui facciamo riferimento con this.componentRef.instance. Superiamo il type safe di TypeScript effettuando un cast con questo codice:

(<any>this.componentRef.instance).test = {test: 'Button'};

 

In questo modo abbiamo istanziato un componente esterno all’interno del nostro componente e, di conseguenza, effettuato quello che era comune fare tramite $compile in AngularJS. Se non avessimo operato in questo modo, il template sarebbe stato elaborato da Angular come semplice testo. Consiglio di effettuare una la prova giocando con questo esempio ed inserendo il template all’interno di una infowindow associata al marker, a verificare che l’interpolazione non verrà elaborata.

In questo esempio, il clic sul marker non elaborerà alcun cambiamento sulla view. Perché? La risposta è zone.js. Dopo ottomila caratteri, possiamo concludere in meno di venti secondi.

Zone.js, ripetiamolo nuovamente, grabba entrambi gli eventi legati ai due clic, ma non quello sul marker. Quella è una callback slegata dalle chiamate asincrone preventivate dalla libreria e zone.js non può sapere che quel clic genererà qualcosa di asincrono.

Ecco spiegato il perché sulla nostra istanza non si riflette il cambiamento di test. Se avete l’occhio allenato vi siete già resi conto che ho incluso NgZone nel nostro componente; ma finora non era necessario.

Modifichiamo il sorgente per racchiudere l’addListener del marker all’interno di una zona.

marker.addListener('click', () => {
   this.zone.run(() => {
     (this.componentRef.instance).test = {test: 'Google Maps'};
   });
});

 

Al termine dell’esecuzione verrà effettuato il change detection che magicamente propagherà alla view il cambio di stato di test.

Desidero salutare Alberto che mi ha dato lo spunto per questo post.

Buono studio!




Vincenzo Giacchina è uno sviluppatore e un amministratore di sistema con una grande passione per la scrittura. A 16 anni ha fondato una ezine chiamata NoFlyZone e negli anni ha collaborato con le principali realtà del mondo underground e di settore dedicate alla divulgazione su temi tecnici e informatici. Lavora con Angular da oltre tre anni sia in ambito desktop che mobile e segue costantemente l'evoluzione dello sviluppo web. È autore di Sviluppare applicazioni con Angular.

Letto 1.718 volte | Tag: , , , ,

Un commento

  1. antonino

    veramente interessante!!!

Lascia il tuo commento