De wereld van de frontend ontwikkeling is continu in beweging. Frameworks schieten als paddenstoelen uit de grond, paradigma’s die aan het verschuiven zijn en de mogelijkheid om een robuuste architectuur neer te zetten; het is geen toekomstmuziek meer. Dit zijn actuele onderwerpen waarover nagedacht dient te worden bij het opzetten van een nieuwe applicatie of de herbouw van een bestaande. Zo is één van de nieuwkomers op de markt versie 2 van het immens populaire Angular (https://angular.io/) framework van Google.
Aangezien er talloze projecten met de eerste versie van dit framework zijn ontwikkeld, is er met de komst van versie 2.0 ook de vraag ontstaan naar een mogelijk migratiepad. Hoe kun je dit structureel aanpakken, wat zijn de mogelijkheden en waar zitten de meeste knelpunten?
Het doel van dit artikel is dan ook niet om in te gaan op de interne werkwijze van Angular 2, maar hoe jouw bestaande Angular 1 (enterprise) applicatie gemigreerd kan worden naar versie 2.
Waarom migreren?
De eerste stabiele versie van Angular 1 dateert van oktober 2010. Dat is in de hedendaagse frontend wereld prehistorisch te noemen. Sindsdien zijn de ontwikkelingen rondom Javascript enorm toegenomen. Zo is er de nieuwe ES6 (http://es6-features.org) (beter bekend als ECMAScript 2015), de meest recente versie van Javascript. Ook zijn er ontwikkelingen rondom het reactive programming. Dat komt neer op het functioneel en asynchroon programmeren met observeerbare stromen van data. (http://reactivex.io/)
Daar waar Angular 1 gebouwd is met technieken, die leunen op two-way databinding, gebruik maakt van de zogeheten digest cycle en een abstractie biedt, die voor veel als overdone aanvoelt, is er met Angular 2 vanaf de start gekozen voor een minimalistische component based aanpak. Door gebruik te maken van Typescript, (een getypeerde superset van Javascript) (www.typescriptlang.org/) en de eerdergenoemde observables is het framework niet alleen state-of-the-art, maar ook klaar voor de toekomst en biedt het in alle opzichten voordelen ten opzichte van versie 1.
Op het moment van schrijven is versie 1.5.7 de meest recente versie. Voornamelijk het afgelopen half jaar is goed te zien dat de ontwikkelaars functionaliteit toevoegen, die de migratiestap vereenvoudigen. Aangezien Angular 2 met zogeheten componenten werkt, speelt de huidige versie hier goed op in door gebruik te maken van de nieuwe component directive.
Samengevat zijn dit de grootste voordelen:
- Sterk verbeterde performance
- Gebaseerd op components
- Betere dependency injection
- Server side rendering
- Het gebruik van de shadow dom
- Verbeterde testbaarheid
Migratie, maar hoe dan?
Het spreekwoord “een goed begin is het halve werk” is in deze case zeker van toepassing, want de voorbereiding is namelijk minstens zo belangrijk als de daadwerkelijke migratie. De voorbereiding is samen te vatten onder deze vier punten:
- Het volgen van een style guide.
- Het gebruik van een module loader
- Het migreren naar Typescript
- De nieuwe component directives gebruiken
Style guide
De eerste stap, die genomen moet worden in de voorbereiding, is het toepassen van een style guide (https://angular.io/docs/ts/latest/guide/style-guide.html). Een style guide is een collectie patronen en best practices rondom het organiseren en ontwikkelen van een applicatie met Angular 1. Ook beschrijft het document veelgemaakte fouten, zodat iemand anders deze niet meer hoeft te maken. Ondanks dat de style guide veel regels en tips bevat, springen deze twee er in het bijzonder uit:
- De “rule of 1”
Elk component zou zijn eigen bestand moeten hebben. Dit zorgt ervoor, dat het navigeren tussen verschillende componenten gemakkelijker wordt en het zorgt ervoor dat ze gemakkelijker te unit testen zijn. Aangeraden wordt om het component niet meer dan 400 regels code te laten bevatten. - De “folders by feature” structuur
Hierbij gelden dezelfde regels als het voorgenoemde punt, echter is het van toepassing op een hoger abstractieniveau. Dat betekent dat de verschillende onderdelen van de applicatie onderverdeeld dienen te worden in verschillende mappen en modules.
Module loader
Het toepassen van een style guide zorgt (onder andere door de bovengenoemde regels) voor een projectstructuur, die bestaat uit een groot aantal kleine modules. En hoewel dit enerzijds voor meer structuur zorgt, is het niet praktisch wanneer je deze bestanden individueel in de juiste volgorde moet inladen op een HTML-pagina. Voor deze taak is een module loader goed in te zetten. De bekendste op het moment van schrijven zijn: SystemJS (https://github.com/systemjs/systemjs), webpack (http://webpack.github.io/) en browserify (http://browserify.org/). Door het gebruik van een module loader kan er gebruik gemaakt worden van de ‘import’ en ‘export’ functionaliteit, die het modulesysteem van ES6 en Typescript biedt. Met deze functionaliteit kan er specifiek worden opgegeven welke modules er moeten worden geïmporteerd of geëxporteerd voor andere modules, waarbij de module loader ervoor zorgt dat dit in de juiste volgorde gaat.
Typescript
Alhoewel het gebruik van Typescript binnen een Angular 2 applicatie niet vereist is, biedt het de gebruiker dermate veel voordelen, dat ervoor gekozen is om er een hoofdstuk aan te wijden. Nu zal er niet diep worden ingegaan op Typescript zelf, omdat dit genoeg stof bevat om een apart artikel over te schrijven, maar wordt de focus op het migratie-aspect gelegd.
Aangezien TypeScript een zogeheten superset is van ES6, vereist het in de basis niet veel meer dan het installeren van de TypeScript compiler en het hernoemen van de ‘.js’ bestanden naar ‘.ts’.
De voordelen van het gebruik van TypeScript zijn:
- De mogelijkheid tot het importeren en exporteren van modules
- Het gebruik van Type annotaties, dat ervoor zorgt dat er buildtime error checking kan plaatsvinden en de mogelijkheden tot goede auto-complete.
- De nieuwste JavaScript ES6 functies kunnen gebruikt worden, waaronder de “arrow” functie, let, default functie parameters en destructuring.
- Services en controllers worden “classes”.
Component directives
Een Angular 2 applicatie is op hoog niveau een compositie van componenten. Mm de migratie te vergemakkelijken biedt Angular 1 de mogelijkheid om hetzelfde te bewerkstelligen via de “component directive”. Deze directives beschikken over:
- Een template
- Een controller
- Input/Output bindings
Zolang de componenten met de component API worden ontwikkeld, zitten ze qua functionaliteit dicht tegen de Angular 2 variant aan. Hierdoor is migreren makkelijker, kost het minder boilerplate code en biedt het een aantal default waardes voor attributen, zoals de “scope en restrict”, maar ook de mogelijkheid tot het inhaken op de lifecycle methoden waaronder “ngOnInit”.
Een component directive geschreven met de component API ziet er als volgt uit:
De upgrade adapter
Voor de daadwerkelijke migratie is de upgrade module ontwikkeld. Deze module maakt het mogelijk om iteratief en incrementeel een bestaande Angular 1 applicatie module voor module te upgraden.
Deze module zorgt ervoor, dat er twee versies van het framework tegelijkertijd actief zijn en met elkaar kunnen communiceren. Dit gebeurt op drie niveaus:
- Dependency injection
- DOM
- Change detection
Dependency injection
Dependency injection staat centraal bij zowel Angular 1 als Angular 2, maar de interne werking verschilt echter op een aantal belangrijke punten:
- Dependency injection tokens zijn altijd strings in Angular 1. In het geval van Angular 2 zijn dit voornamelijk classes, maar kunnen het ook string zijn.
- Angular 1 kent precies één zogeheten “injector”. Zelfs in een grote applicatie wordt alle code in een enkele namespace gezet, waar bij Angular 2 gebruik gemaakt wordt van een hiërarchische boom van “injectors” met aan de top een “root injector” met daaronder een injector voor elk component.
De upgrade adapter neemt de verschillen weg tussen deze twee frameworks en zorgt ervoor dat er gemakkelijk gebruik gemaakt kan worden van de aangeboden dependency injection. De wisselwerking tussen de frameworks is te realiseren door de bestaande componenten te up- of te downgraden. Hierbij wordt gebruik gemaakt van de “string token” om de gewenste dependency expliciet te maken.
DOM
Tijdens de migratie bevat het “Document Object Model” (DOM) componenten en directives van zowel Angular 1 en 2. Deze componenten communiceren via de input en output bindings van de desbetreffende frameworks en worden samengebracht door de bovengenoemde upgrade adapter en geïnjecteerde dependencies.
Om te begrijpen wat er in een de DOM van een hybride applicatie gebeurd, is het belangrijk om twee dingen te begrijpen.
- Elk element in de DOM behoort tot één van de twee frameworks, waarbij het andere framework het desbetreffende element negeert.
- Het root element is altijd een Angular 1 template.
De hybride applicatie wordt gestart als Angular 1 applicatie. Pas wanneer er een Angular 2 component wordt gebruikt, neemt dit framework het over voor het specifieke element.
Indien er componenten uit Angular 2 binnen een Angular 1 applicatie gebruikt worden (en vice versa), vindt er een omschakeling tussen de grenzen van de twee frameworks plaats. Deze omschakeling is echter enkel van toepassing op de onderliggende elementen van het component. In de volgende situatie, waarbij we het Angular 2 component ‘<angular-2-article></angular-2-article>’ gebruiken vanuit Angular 1, wordt het element door Angular 1 gemanaged, aangezien deze is aangemaakt in een Angular 1 template. Dit betekent dat er enkel Angular 1 directives aan toegevoegd kunnen worden.
Change detection
Het detecteren van verandering in een app is net als de voorgenoemde onderdelen volledig op de schop gegaan met het ontwikkelen van Angular 2. Daar waar in Angular 1 voor elk event automatisch de ‘scope.$apply’ methode werd aangeroepen, waarna de “change detection” in werking treedt, en de bestaande bindings worden geüpdatet, doet Angular 2 dit anders. Omdat Angular 2 code in zogeheten “zones” (https://github.com/angular/zone.js) draait, heeft het framework meer kennis van de lifecycle en zorgt er vervolgens voor dat de change detection gestart wordt, zonder dat ‘scope.$apply’ aangeroepen dient te worden.
Ook op het vlak van change detection zorgt de upgrade adapter ervoor dat de verschillen worden weggenomen tussen de twee versies van het Angular framework. Omdat elk event van de applicatie in een Angular 2-zone wordt uitgevoerd – onafhankelijk of het gegenereerd is vanuit Angular 1 of 2 – wordt de change detection na elk event gestart door de desbetreffende zone.
Daarnaast zorgt de upgrade adapter ervoor, dat de ‘$rootScope.$apply’ functie wordt aangeroepen. De Angular 1 change detection wordt dan gestart.
De noodzaak voor het aanroepen van de ‘$apply’ functie is daarom niet meer nodig, aangezien de upgrade adapter dit voor ons afhandelt. Bestaande ‘$apply’ aanroepen hoeven niet expliciet verwijderd te worden, want ze zullen simpelweg niet werken in een hybride applicatie.
Tijdens het upgraden worden de bindings, die gedefinieerd zijn in het Angular 1 component, direct overgenomen door de change detection van Angular 2. Deze waarden worden gebruikt als input en op de scope of controller gezet, wanneer deze wijzigt.
Tot slot
In dit artikel zijn de belangrijkste punten om de migratie van een Angular 1 applicatie voor te bereiden voorbijgekomen. Evenals, op hoog niveau, de werking van de upgrade adapter.
Het uitvoeren van een daadwerkelijke migratie is uitvoerig beschreven op de website van Angular en is om die reden niet opgenomen in dit artikel. Deze handleiding is te vinden op:
https://angular.io/docs/ts/latest/guide/upgrade.html
Wat wij hebben ervaren tijdens een migratie voor één van onze klanten is, dat de voorbereiding minstens net zo belangrijk is als een goede uitvoering. Door de herstructurering van de bestaande applicatie is er al een performancewinst geboekt en is de leesbaarheid toegenomen. Doordat er een upgrade container om de bestaande applicatie heen wordt gezet, is het niet nodig om de migratie als big-bang door te voeren, maar kan dit incrementeel en iteratief per component gebeuren. Er dient echter wel rekening te worden gehouden met een performance impact vanwege het feit dat er twee versies van het Angular framework simultaan worden uitgevoerd.
Met inachtneming van de aandachtspunten, ben ik van mening dat het een goede keuze is om te starten met de migratie. Angular 2 is in alle opzichten (Typescript, performance, observables, testing) beter dan Angular 1. Het is mogelijk om incrementeel te upgraden, wanneer je met een bestaande Angular 1 applicatie werkt, dankzij de upgrade ondersteuning vanuit het Angular team. De combinatie van deze twee maakt het de inzet meer dan waard en verdient het zichzelf op termijn terug in de stabiliteit en performance van een app, die klaar is voor de toekomst.