NgUpgrade. Jak zmigrować projekt z AngularJS do Angular
Pomimo tego, że pierwsza stabilna wersja Angular 2 została opublikowana w trzecim kwartale 2016, a w chwili obecnej Angular doczekał się już wersji 6, wielu z nas nadal utrzymuje i rozwija aplikacje napisane w AngularJS (Angular 1.x). W tym artykule pokażę Wam, jak zmigrować projekt z AngularJS do Angulara za pomocą NgUpgrade.
Anna Gizińska. JavaScript Developer z ponad 10 letnim doświadczeniem zdobytym w takim firmach jak Citibank, Samsung, czy GFT. Od najmłodszych lat zafascynowana technologiami wykorzystywanymi do tworzenia aplikacji webowych i mobilnych. Po godzinach fanka gier komputerowych i startupów. Autorka test-yourself.com — aplikacji webowej i mobilnej, która umożliwia sprawdzenie poziomu swojej wiedzy w zakresie różnych zagadnień programistycznej i popularnych technologii.
Migracja istniejącej aplikacji napisanej w AngularJS do najnowszej wersji Angular może stanowić nie lada wyzwanie. W szczególności jeśli nasza aplikacja posiada wiele komponentów, serwisów i widoków. Wielu osobom ten proces kojarzy się z przepisaniem aplikacji od zera. Nie każdy zespół może jednak pozwolić sobie na kilka miesięcy przestoju w dostarczaniu nowych funkcjonalności, tylko po to, aby całkowicie przepisać kod.
Z pomocą przychodzą nam twórcy Angular ze swoim rozwiązaniem o nazwie ng-upgrade. NgUpgrade jest modułem, umożliwiającym równoczesne uruchomienie kodu napisanego w AngularJS i Angular. Ogólna idea polega na uruchomieniu hybrydowej aplikacji napisanej w Angular, która następnie uruchamia aplikację AngularJS. Dodatkowo ng-upgrade dostarcza nam kilka przydatnych funkcji i klas, które pozwalają upgradować i downgradować komponenty, dyrektywy czy serwisu tak, aby mogły być one używane w obu aplikacjach.
Dzięki zastosowaniu ng-upgrade możemy stopniowo przepisywać istniejący kod z AngularJS na Angular, bez blokowania procesu dostarczania nowych oraz rozwijania istniejących funkcjonalności. Zanim jednak przystąpimy do samej migracji warto się do niej odpowiednio przygotować.
Przygotowanie do migracji
Spis treści
Aktualizacja do AngularJS 1.5+
Jeśli nasza aplikacja używa AngularJS w wersji starszej niż 1.5, w pierwszej kolejności powinniśmy zaktualizować AngularJS do najnowszej wersji. Z wersją AngularJS 1.5 zostało dodanych szereg znaczących aktualizacji, które mają na celu ułatwienie migracji na Angular. Najważniejszą z nich jest dodanie metody component().
W Angular niemal wszystko jest komponentem, dlatego też po udanej aktualizacji, kolejnym krokiem jest migracja kontrolerów i niektórych dyrektyw na komponenty.
AngularJS directive:
angular.module('gamesApp', []) .directive('gamesList', function () { return { restrict: 'E', templateUrl: ./games-list.html', scope: { items: '<', onClick: '&' }, controller: function () {} } });
component:
angular.module('gamesApp', []) .component('gamesList', { templateUrl: './games-list.html’', bindings: { items: '<', onClick: '&' }, controller: function () {} });
Migracja do TypeScript
Angular używa TypeScript’a, więc warto go wdrożyć do naszego projektu zanim przystąpimy do migracji. Typescript to rozszerzenie JavaScript stworzone przez firmę Microsoft, cieszące się dużą popularnością wśród webdeveloperów. Dostarcza nam wielu nowych możliwości takich jak statyczne typowanie, interfejsy, klasy i dziedziczenie, enumy, moduły i wiele więcej. To, w jakim stopniu będziemy wykorzystywać benefity wiążące się z korzystania z TypeScript’a zależy od nas.
Absolutne minimum to:
- dodanie do naszego projektu kompilatora TypeScript,
- zmiana rozszerzeń z .js na .ts,
- migracja komponentów i serwisów na klasy.
TypeScript nie jest obowiązkowy i nie musimy z niego korzystać pisząc kod w Angular, aczkolwiek dostarcza nam on wiele korzyści, których nie da nam czysty JavaScript.
Manualne bootowanie
Jeśli używaliśmy atrybutu ng-app do automatycznego uruchamiania naszej aplikacji, powinniśmy przełączyć się na bootowanie manualne. Moduł ng-upgrade wymaga uruchomienia hybrydowej aplikacji ręcznie, dlatego też warto przed rozpoczęciem migracji przetestować, jak nasza aplikacja zachowa się po zaimplementowaniu wspomnianej zmiany.
angular.bootstrap(document.body, ['gamesApp'], { strictDi: true });
Modularność
Jeśli do tej pory nasz projekt nie korzystał z modułów, warto je zaimplementować. Kompilator TypeScript’a umożliwia nam wybór spośród kilku modułów docelowych, tj. CommonJS, AMD, UMD, System itp. Wystarczy, że wybierzemy odpowiadający nam moduł, a kompilator TypeScript wygeneruje kod w odpowiedniej formie.
Możemy także skorzystać z takich narzędzi jak webpack czy browserify.
ng-uprade
Instalacja
Gdy nasz projekt jest już gotowy do migracji, możemy przystąpić do instalacji Angular, jego dependencji oraz samego ng-upgrade:
Wymagane minimum to:
- @angular/common — niezbędne serwisy i dyrektywy np. HttpClient,
- @angular/compiler — kompilator templatek,
- @angular/core — rdzeń Angular, zawiera m.in. dekoratory dla komponentów, serwisów i modułów,
- @angular/forms — moduł niezbędny przy implementacji formularzy w Angular. Zawiera między innymi. ngModel,
- @angular/platform-browser — moduł wymagany do pracy z DOM i przeglądarką,
- @angular/platform-browser–dynamic,
- core-js – polyfill dla es6,
- rxjs – Reactive JS. Zawiera np. observables,
- zone.js – polyfill dla zone.js,
- @angular/upgrade — moduł ng-upgrade.
Bootowanie
Gdy już wszystkie wymagane moduły znajdują się w package.json oraz w katalogu node_modules, zabieramy się za modyfikację naszego kodu odpowiedzialnego za manualne bootowanie aplikacji. Dotychczasowy kod, który uruchamiał aplikację AngularJS, zamieniamy na kod, który uruchomi naszą aplikację hybrydową. Taka aplikacja składa się z modułu ngModule, który nie posiada własności bootstrap. W przypadku typowej aplikacji napisanej w Angular, w tablicy bootstrap znalazłby się komponent, stanowiący korzeń (root) naszej aplikacji. Zamiast tego, używamy metody ngDoBootstrap, w której znajduje się kod odpowiedzialny za ręczne uruchomienie aplikacji napisanej w AngularJS.
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { UpgradeModule } from '@angular/upgrade/static'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; @NgModule({ imports: [ BrowserModule, UpgradeModule ] }) export class AppModule { constructor(private upgrade: UpgradeModule) { } ngDoBootstrap() { this.upgrade.bootstrap(document.body, ['gamesApp'], { strictDi: true }); } } platformBrowserDynamic().bootstrapModule(AppModule);
Jeśli aplikacja uruchamia się bez żadnych problemów, możemy przystąpić do stopniowej migracji komponentów i serwisów.
Migracja komponentu
Do tworzenia komponentów w Angular używamy klas oraz dekoratora @Component. Dekorator pozwala dostarczyć metadane, które określają w jaki sposób ma się zachowywać dany komponent, np. jak ma wyglądać jego templatka, jakie ma mieć style CSS, jaki ma mieć selektor itp.
AngularJS component:
class GamesListComponent {} module.component('gamesList', { templateUrl: './games-list.html', bindings: { items: '<', onClick: ‘&’ }, controller: GamesListComponent })
Angular component:
import { Component, Input, Output } from “@angular/core”; @Component({ selector: ‘games-list’, templateUrl: “./games-list.html” }) export class GamesListComponent { @Input() items; @Output() onClick = new EventEmitter(); }
Migracja serwisu
Tworząc serwis w Angular oznaczamy jego klasę dekoratorem @Injectable. Dekorator sprawia, że klasa staje się dostępna dla injectora.
AngularJS serwis:
class GamesService { get() { return [{ id: 1, name: 'GTA' }, { id: 2, name: 'World of Warcraft' }]; } } module.service("gamesService", GamesService);
Angular serwis:
import { Injectable } from ‘@angular/core’; @Injectable() class GamesService { get() { return [{ id: 1, name: 'GTA' }, { id: 2, name: 'World of Warcraft' }]; } }
Downgrading
Jeśli chcemy użyć komponentu, dyrektywy lub serwisu napisanego w Angular w kodzie napisanym w AngularJS, musimy go zdowngradować. Taki scenariusz może dotyczyć zarówno nowych komponentów napisanych w Angular lub tych, które dopiero co zostały zmigrowane. Do tego celu używamy funkcji dostarczanych z modułem ng-upgrade – downgradeComponent dla komponentów lub downgradeInjectable dla serwisów.
import { downgradeComponent } from '@angular/upgrade/static'; angular.module('gamesApp').directive( 'navbarMenu', downgradeComponent({ component: NavbarComponent }) );
Upgrading
Jeśli chcemy użyć komponentu, dyrektywy lub serwisu napisanego w AngularJS w kodzie napisanym w Angular, musimy go upgradować. W przypadku komponentu, aby to osiągnąć, tworzymy nową dyrektywę, której klasa dziedziczy z klasy UpgradeComponent dostępnej z modułem ng-upgrade, a następnie wywołujemy metodę super w konstruktorze tej klasy.
game-details.component.ts:
... @Directive({ selector: 'game-details' }) export class GameDetailsDirective extends UpgradeComponent { @Input() game; constructor(elementRef: ElementRef, injector: Injector) { super('gameDetails', elementRef, injector); } }
app.module.ts:
import { GameDetailsDirective } from "./game-details/game-details"; @NgModule({ declarations: [ ... GameDetailsDirective, ... ] }) export class AppModule { ... }
Migracja z AngularJS na Angular nie wiąże się tylko i wyłącznie z migracją samego kodu aplikacji. Oprócz tego musimy stawić czoło migracji wszelkiego rodzaju testów, które mamy w naszym projekcie i które używają zmigrowanego kodu. Często wiąże się to z aktualizacją plików konfiguracyjnych, decydujących w jaki sposób takie testy są uruchamiane, a także kodu samych testów.
Przykładowym problemem, który możemy napotkać podczas uruchamiania testów e2e na zmigrowanym kodzie jest błąd z przekroczeniem limitu czasu ładowania aplikacji Angular. Problem ten może wystąpić jeśli w naszym kodzie używamy funkcji setInterval.
Na stackoverflow oraz na githubie możemy znaleźć wiele odpowiedzi, które pomogą obejść napotkane problemy do czasu aż kod ng-upgrade i protractor nie zostanie odpowiednio poprawiony.
Proces migracji aplikacji z AngularJS do Angular może być zniechęcający, ponieważ wiąże się z wieloma krokami przed i podczas samej migracji. Jednak korzyści płynące z używania Angular są warte poświęconego czasu.