Using ngUpgrade with Angular's AOT compiler to optimize performance

Update in December 2016: Added some information like downgrading an component with bindings.

ngUpgrade which is included in Angular 2+ (hereinafter just called Angular) allows for the creation of hybrid applications that contains both, AngularJS 1.x based and Angular 2+ based services and components. This helps to migrate an existing AngularJS 1.x application to Angular 2+ step by step. The downside of this approach is that an application needs to load both versions of Angular.

Fortunately, beginning with Angular 2.2.0 which came out in mid-November 2016, there is an implementation of ngUpgrade that allows for ahead of time compilation (AOT). That means that the size of the Angular part can be reduced to the constructs of the framework that are needed by using tree shaking.

In this post, I'm showing how to use this implementation by an example I've prepared for ngEurope. It contains several components and services written with AngularJS 1.x and Angular:

Demo Application

While a hybrid application is always bootstrapped as an AngularJS 1.x application, it can contain services and components written with both versions. To use an AngularJS 1.x building block (component or service) within an Angular building block, it needs to be upgraded. This means that it gets a wrapper that makes it look like an Angular component or service. For using an Angular building block within AngularJS 1.x it needs to get downgraded. This also means that a wrapper is generated. In this case, this wrapper makes it look like an AngularJX 1.x counterpart. The following picture demonstrates this. The arrows show whether the respective building block is up- or downgraded:

Up- and Downgrading Components and Services

The full sample used here can be found at github.

Downgrading an Angular Component to AngularJS 1.x

To downgrade an Angular Component to AngularJS 1.x, the sample uses the new method downgradeComponent that is located in the module @angular/upgrade/static. The result of this method can be registered as a directive with an AngularJS 1.x module:

import { downgradeComponent } from '@angular/upgrade/static';

var app = angular.module('flight-app', [...]);

[...]

app.directive('flightSearch', <any>downgradeComponent({ component: FlightSearchComponent }));

After this, the AngularJS 1.x part of the hybrid application can use this directive. The sample presented here uses it for instance within a template for a route:

$stateProvider
    [...]
	.state('flightBooking.flightSearch', {
	    url: '/flight',
	    template: '<flight-search></flight-search>'
	});

As this downgraded component is an Angular component, it has to be registered within an Angular-Module too.

@NgModule({
    imports: [
        BrowserModule,
        HttpModule,
        FormsModule,
        UpgradeModule
    ],
    declarations: [
        FlightSearchComponent
    ],
    entryComponents: [
        FlightSearchComponent
    ]
})
export class AppModule {
    ngDoBootstrap() {}
}

This module has to import the UpgradeModule. As it provides Angular building blocks for an application that is bootstrapped with AngularJS 1.x, it does not contain own root components. However, it needs to be bootstrapped too and to make Angular to bootstrap a module without at least one top level component, the module class needs to have an ngDoBootstrap method. Here it is vital, to define the FlightSearchComponent as an entry component so that the Angular Compiler creates the needed files.

Downgrading an Angular Component with Bindings to AngularJS 1.x

When downgrading an Angular Component with Bindings, the application has to pass the names of the inputs and outputs:

app.directive('passengerCard', <any>downgradeComponent({
                                        component: PassengerCardComponent,
                                        inputs: ['item', 'selectedItem'],
                                        outputs: ['selectedItemChange']
                                }));

In addition to that, the components has to be registered with the Angular Module:

@NgModule({
    imports: [
        BrowserModule,
        HttpModule,
        FormsModule,
        UpgradeModule
    ],
    declarations: [
        FlightSearchComponent,
        PassengerCardComponent
    ],
    entryComponents: [
        FlightSearchComponent,
        PassengerCardComponent
    ]
})
export class AppModule {
    ngDoBootstrap() {}
}

To use such an downgraded component with AngularJS 1.x, the template has to mark the attributes which are used with property- and event bindings. For this, the application typically uses brackets and parenthesis. Although, this is how Angular marks such attributes the template has to use kebab case like in AnglarJS 1.x applications:

<div ng-repeat="p in $ctrl.passenger" class="col-sm-4" style="padding:20px;">
    <passenger-card
            [item]="p"
            [selected-item]="$ctrl.selectedPassenger"
            (selected-item-change)="$ctrl.selectedPassenger = $event">
    </passenger-card>
</div>

Downgrading an Angular Service to AngularJS 1.x

The procedure to downgrade an Angular service to AngularJS 1.x is similar to the process for downgrading a component: for this, the module @angular/upgrade/static provides a method ``@angular/upgrade/static```. Its return value can be registered as an AngularJS 1.x factory:

import { downgradeInjectable } from '@angular/upgrade/static';

var app = angular.module('flight-app', [...]);

[...]

app.factory('passengerService', downgradeInjectable(PassengerService));

Then, the service can be injected in an AngularJS 1.x building block:

class PassengerSearchController {

    constructor(private passengerService: PassengerService) {
    }

    [...]
}

Here it is important to remember that AngularJS 1.x uses names and not types for injection. Because of this, the presented Controller has to name the constructor argument passengerService. The mentioned typed is irrelevant for dependency injection.

Again, as the PassengerService is an Angular Service it has be registered with an Angular Module:

@NgModule({
    imports: [
        BrowserModule,
        HttpModule,
        FormsModule,
        UpgradeModule
    ],
    declarations: [
        FlightSearchComponent
    ],
    entryComponents: [
        FlightSearchComponent
    ],
    providers: [
        PassengerService
    ]
})
export class AppModule {
    ngDoBootstrap() {}
}

Upgrading an AngularJS 1.x Component to Angular

Upgrading an AngularJS 1.x component to Angular is a bit more complicated. In this case, the application has to provide a wrapper for the upgraded component by subclassing UpgradeComponent:

import {UpgradeComponent} from "@angular/upgrade/static";
[...]

@Directive({selector: 'flight-card'})
export class FlightCard extends UpgradeComponent implements OnInit, OnChanges {

    @Input() item: Flight;
    @Input() selectedItem: Flight;
    @Output() selectedItemChange: EventEmitter<any>;

    constructor(elementRef: ElementRef, injector: Injector) {
        super('flightCard', elementRef, injector);
    }

    ngOnInit() { return super.ngOnInit(); }
    ngOnChanges(c) { return super.ngOnChanges(c); }

}

Unfortunately, this cannot be moved to a convenience function because it would prevent the compiler from finding the required metadata. The wrapper needs to have an input for each ingoing property of the AngularJS 1.x component and an output for each event. ngUpgrade will connect them to the respective counterparts in the AngularJS 1.x component. To tell ngUprade which AngularJS 1.x component to wrap, the constructor has to delegate its canonical name to the base constructor using super. In addition, super also needs an instance of ElementRef and Injector that can be obtained via dependeny injection.

In addition to that, the wrapper needs to implement life cycle hooks which are relevant for the AngularJS 1.x component. In any case, it needs to implement ngOnInit because ngUpgrade is picking up this method via reflection and using it to instantiate the wrapped component. Its fully sufficient to just make this method to delegate to the base implementation. To support data binding, the wrapper needs to have an ngOnChanges method for the same reason.

After this, the wrapper can be registered with an Angular 2 module.

import {UpgradeModule} from "@angular/upgrade/static";

@NgModule({
	imports: [
        [...],
        UpgradeModule
    ],
    [...],
    declarations: [
        FlightSearchComponent,
        FlightCard // <-- Upgraded Component
    ],
    [...]
})
export class AppModule {
    ngDoBootstrap() {}
}

After registering the wrapper, it can be used in the template of other Angular components:

<flight-card
        [item]="f"
        [selectedItem]="selectedFlight"
        (selectedItemChange)="selectedFlight = $event"></flight-card>

Upgrading an AngularJS 1.x Service to Angular

To upgrade an AngularJS 1.x service, the application has to to provide a function that takes an AngularJS 1.x injector and uses it to return the service in question:

export function createFlightService(injector) {
    return injector.get('flightService');
}

Again, this cannot be moved to a generic convenience function because this would prevent the AOT from finding the necessary metadata. To use this function in the Angular 2 part of the hybrid application, it is registered as a factory function using a provider:

@NgModule({
    [...]
    providers: [
        PassengerService,
        {
            provide: FlightService,
            useFactory: createFlightService,
            deps: ['$injector']
        },
        [...]
    ]
})
export class AppModule {
    ngDoBootstrap() {}
}

After this, Angular 2 can inject the service into consumers, e.g. components or services:

@Component({ [...] })
export class FlightSearchComponent {

    constructor(
        private flightService: FlightService, [...]) {
    }

    [...]
}

Bootstrapping

To bootstrap an hybrid application, the demo here presented uses a function bootstrap that has been "borrowed" from the unit tests of ngUpgrade. It bootstraps both, the AngularJS 1.x part as well as the Angular 2 part of the application:

import {PlatformRef, NgModuleFactory} from "@angular/core";
import {UpgradeModule} from "@angular/upgrade/static";
import {platformBrowser} from "@angular/platform-browser";
import {AppModuleNgFactory} from "../aot/app/app2.module.ngfactory";


// bootstrap function "borrowed" from the angular test cases
export function bootstrap(
    platform: PlatformRef, Ng2Module: NgModuleFactory<{}>, element: Element, ng1ModuleName: string) {
    // We bootstrap the Angular 2 module first; then when it is ready (async)
    // We bootstrap the Angular 1 module on the bootstrap element
    return platform.bootstrapModuleFactory(Ng2Module).then(ref => {
        let upgrade = ref.injector.get(UpgradeModule) as UpgradeModule;
        upgrade.bootstrap(element, [ng1ModuleName]);
        return upgrade;
    });
}

bootstrap(
    platformBrowser(),
    AppModuleNgFactory,
    document.body,
    'flight-app')
    .catch(err => console.error(err));

Please note, that AppModuleNgFactory is generated by the AOT Compiler. This is described in the next sections. Before this file has been generated, you could use null as a placeholder to avoid compilation errors.

Here it is important to note, that the AngularJS 1.x module is bootstrapped with the UpgradeModule's bootstrap method. This is a replacement for ng-app or angular.bootstrap. The passed NgModuleFactory is created by the AOT compiler when compiling the Angular 2 module.

AOT Compilation

To use the AOT compiler, the application should provide an (additional) tsconfig.json. This file is called tsconfig.aot.json in this sample. It contains an angularCompilerOptions property which tells the compiler where to place the generated files.

{
    "compilerOptions": {
        "target": "es5",
        "module": "es2015",
        "moduleResolution": "node",
        "sourceMap": true,
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "removeComments": false,
        "noImplicitAny": false,
        "suppressImplicitAnyIndexErrors": true,
        "typeRoots": [
            "typings/globals/"
        ]
    },

    "files": [
        "app/app2.module.ts"
    ],

    "angularCompilerOptions": {
        "genDir": "aot",
        "skipMetadataEmit" : true
    }
}

To allow tree shaking for optimizing the size of the Angular 2 part, the module pattern es2015 is choosen. For the compilation and for starting the sample, the file package.json defines some scripts. The most important one here is ngc which is starting the AOT compiler and passing the file tsconfig.aot.json:

[...]
"scripts": {
"webpack": "webpack",
"server": "live-server",
"start": "npm run server",
"ngc": "ngc -p tsconfig.aot.json",
"build": "npm run ngc && npm run webpack"
},
[...]

This sample also uses webpack for bundling, which is executed after the AOT compiler by the script build. To make this work, one needs to download the package @angular/compiler-cli. The start script calls the live-server and not the webpack-dev-server because the latter has not fully supported the AOT compiler for recompiling when this text was written.

Build and Starting the Application

To build and start the application described here, the following commands can be used:

npm run build
npm start

 

 
Sie wollen mehr zum Thema Using ngUpgrade with Angular's AOT compiler to optimize performance wissen? Hier können Sie eine Anfrage für eine unverbindliche Schulung ode Beratung bzw. einen Workshop erstellen.
 
Unverbindliche Anfrage
 
 

Schulung und Beratung

Angular 2

Datenbindung, Formulare, Validierung, Routing, HTTP, Komponenten, ...

Details

Angular 2: Advanced

Erweiterte Aspekte von Angular 2

Details

Reaktive Architekturen mit Angular und Redux

Dieses interaktive Seminar vermittelt, wie Sie reaktive Anwendungen mit Angular entwickeln können.

Details

Migration von AngularJS 1.x auf Angular 2

Bestehende Projekte auf Angular 2 migrieren, ngUpgrade, ...

Details

Angular 2 Review

Feedback und klärung offener Fragen, weiterführende Themen

Details

Angular 2 Workshop

Start ohne Umwege

Details

Weitere Schulungen ...