Sie sind hier: Weblog

Using ngUpgrade with Angulars AOT to optimize performance

Foto ,
15.11.2016 01:00:00

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, Im showing how to use this implementation by an example Ive prepared for ngEurope. It contains several components and services written with AngularJS 1.x and Angular:

Overview of the 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>
    });

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.

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() {}
}

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.

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:

// 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));

Here it is important to note, that the AngularJS 1.x module is bootstrapped with the UpgradeModules 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