📜 ⬆️ ⬇️

The transition from AngularJS to Angular: problems and solutions of the hybrid mode (2/3)


The transition in a hybrid mode is a natural procedure, well prepared and described by the Angular team. However, in practice there are difficulties and gaps that have to be solved on the fly. In today's continuation of our article about the migration to Angular, we’ll tell you about the problems that the Skyeng team faced and share our solutions.


The first part , the third part .


Dynamic string compilation


In angularjs, it's very simple:


const compiledContent = this.$compile(template)(scope); this.$element.append(compiledContent); 

And in Angular, not really.


The first solution is to take a variant from the angulyar through the JiT compiler. It implies that in the production of the assembly, despite the AoT compilation of static components, the heavy compiler still drags to build dynamic templates. It looks something like this:


 //    import {NgModule, Compiler} from "@angular/core"; import {JitCompilerFactory} from "@angular/compiler"; export function compilerFactory() { return new JitCompilerFactory([{ useDebug: false, useJit: true }]).createCompiler(); } @NgModule({ providers: [ { provide: Compiler, useFactory: compilerFactory }, ... ], declarations: [ DynamicTemplateComponent, ] }) export class DynamicModule { } //  import { Component, Input, Injector, Compiler, ReflectiveInjector, ViewContainerRef, NgModule, ModuleWithProviders, ComponentRef, OnInit, OnChanges, SimpleChanges, } from "@angular/core"; import {COMPILER_PROVIDERS} from "@angular/compiler"; @Component({ selector: "vim-base-dynamic-template", template: "", }) export class DynamicTemplateComponent implements OnInit, OnChanges { @Input() moduleImports?: ModuleWithProviders[]; @Input() template: string; private componentRef: ComponentRef<any> | null = null; private dynamicCompiler: Compiler; private dynamicInjector: Injector; constructor( private injector: Injector, private viewContainerRef: ViewContainerRef, ) { } public ngOnInit() { this.dynamicInjector = ReflectiveInjector.resolveAndCreate(COMPILER_PROVIDERS, this.injector); this.dynamicCompiler = this.injector.get(Compiler); this.compileComponent(this.template, this.moduleImports); } public ngOnChanges(changes: SimpleChanges) { if (this.dynamicCompiler && changes.template) { this.compileComponent(this.template, this.moduleImports); } } private compileComponent(template: string, imports: ModuleWithProviders[] = []): void { if (this.componentRef) { this.componentRef.destroy(); } const component = Component({ template })(class {}); const module = NgModule({ imports, declarations: [ component ] })(class {}); this.dynamicCompiler.compileModuleAndAllComponentsAsync(module) .then(factories => factories.componentFactories.filter(factory => factory.componentType === component)[0]) .then(componentFactory => { this.componentRef = this.viewContainerRef.createComponent( componentFactory, null, this.viewContainerRef.injector ); }); } } 

And everything seems to be relatively good (the thick compiler in the bundle is nevertheless leveled by a mountain of other libs and the code of the project itself, if this is more than the todo list), but here we entered this particular problem:



https://github.com/angular/iss/issues/19902


Six seconds to compile one of our slides with exercises, albeit a rather large one. Given that three seconds goes incomprehensible simple. Judging by the answer in the issue, the situation in the coming months will not change, and we had to find another solution.


It also turned out that in this case we cannot use the factories already compiled during AoT assembly of the components used in the slides, since There is no possibility to fill the JiT compiler cache. Such components were essentially compiled two times - on the back-end with the AoT build and in run-time when compiling the first slide.


The second quick-and-easy solution was to compile templates via $compile from angularjs (we still have hybrid and angularis):


 class DynamicTemplateController { static $inject = [ "$compile", "$element", "$scope", ]; public template: string; private compiledScope: ng.IScope; constructor( private $compile: ng.ICompileService, private $element: ng.IAugmentedJQuery, private $scope: ng.IScope, ) { } public $onChanges() { this.compileTemplate(); } private compileTemplate(): void { if (this.compiledScope) { this.compiledScope.$destroy(); this.$element.empty(); } this.compiledScope = this.$scope.$new(true); this.$element.append(this.$compile(this.template)(this.compiledScope)); } } 

The Angular component used an upgraded version of DynamicTemplateComponent from Angular, which used the $compile service to build a template in which all the components were downgraded from the Angular. Such a short layer of angular -> angularjs ($ compile) -> angular.


This option has some problems, for example, the impossibility of injecting components through an assembler component from angularis, but the main thing is that it will not work after the end of the upgrade and the cutting of the angularis.


Additional googling and populating the people in the gitter of the angulyar led to a third solution : variations on what is used directly on the off-site of the angulyar for such a case, namely inserting the template directly into the DOM and manually initializing all known components on top of the found tags. Code by reference .


We insert the incoming template into the DOM as is, for each known component (we obtain the CONTENT_COMPONENTS token in the service), we search for the corresponding DOM nodes and initialize.


Of the minuses:



But in general, we have a rather smart way to initialize a dynamic template, which basically solves our problem specifically with angulyar tools and without lags, as with the JiT compiler.


It would seem that this can be stopped, but for us the problem has not been completely solved because of how the angular works with content projection. We need the contents of some components (by type of spoilers) to initialize only under certain conditions, which is impossible when using the usual ng-content , and we cannot insert the ng-template due to the way the content is assembled. In the future, we will look for a more flexible solution, perhaps, we will replace the html content with a JSON structure, according to which we will render the slide with ordinary angural components, taking into account the dynamic display / hiding of a part of the content (will require the use of self-written components instead of ng-content ).


Someone can approach the fourth option , which will be officially available as a beta with the release of angular 6 - @angular/elements . These are custom elements implemented through the angular. We register by some tag, in any way we insert this tag into the DOM, and a full-fledged angular component with all the usual functionality is automatically initialized on it. Of the restrictions - interaction with the main application only through events on such an element.


Information on them is currently available only in the form of several speeches from ng-conferences, articles on these speeches and technical demos:



The Angulyar site plans right away, with the first version of @angular/elements , to switch to them instead of the current build method:



Change detection


In the hybrid, there are several unpleasant problems with the work of the CD between angular and angularis, namely:


AngularJS in the Angular zone


Immediately after hybrid initialization, we will get a performance drop due to the fact that angularjs code will run in the angular zone, and any setTimeout / setInterval and other asynchronous actions from the angularjs code and from the thirdparty libraries used will pull the CD angular tick, which yank $digest angularjs . Those. if earlier we could not worry about extra digests from the activity of third-party libs, since angularjs requires explicit kicking of the CD, now it will work for every sneeze.


It is repaired by forwarding NgZone service to angularjs (through downgrade) and revising the initialization of third-party libs or native timeouts in ngZone.runOutsideAngular . In the future, they promise the possibility of initializing a hybrid so that the CD of the angulyar and the angulyazh do not pull each other in principle (the angularis will work outside the angular area), and for the interaction between different pieces it will be necessary to clearly pull the CD of the corresponding framework.


downgradeComponent and ChangeDetectionStrategy.OnPush


Downgrade components do not work correctly with OnPush - when you change inputs, the CD on this component does not twitch. Code


If you comment out changeDetection: ChangeDetectionStrategy.OnPush, at angular.component , then the counter will be updated correctly


From solutions, only remove OnPush from the component while it is being used in the patterns of the angular components.


UI Router


We originally had a ui-router that works with a new angular and has a bunch of hacks for working in a hybrid mode. With him there was a lot of fuss over the bootstrap of the application and problems with the protractor.


As a result, we came to such initialization hacks:


 import {NgModuleRef} from "@angular/core"; import {UpgradeModule} from "@angular/upgrade/static"; import {UrlService} from "@uirouter/core"; import {getUIRouter} from "@uirouter/angular-hybrid"; import {UrlRouterProvider} from "@uirouter/angularjs"; export function deferAndSyncUiRouter(angularjsModule: ng.IModule): void { angularjsModule .config([ "$urlServiceProvider", ($urlServiceProvider: UrlRouterProvider) => $urlServiceProvider.deferIntercept()]) // NOTE: uglyhack due to bug with protractor https://github.com/ui-router/angular-hybrid/issues/39 .run([ "$$angularInjector", $$angularInjector => { const url: UrlService = getUIRouter($$angularInjector).urlService; url.listen(); url.sync(); }]); } export function bootstrapWithUiRouter(platformRef: NgModuleRef<any>, angularjsModule: ng.IModule): void { const injector = platformRef.injector; const upgradeModule = injector.get(UpgradeModule); upgradeModule.bootstrap(document.body, [ angularjsModule.name ], { strictDi: true }); } 

and in main.ts:


 import angular from "angular"; import {platformBrowserDynamic} from "@angular/platform-browser-dynamic"; import {setAngularLib} from "@angular/upgrade/static"; import {AppMainOldModule} from "./app.module.main"; import {deferAndSyncUiRouter, bootstrapWithUiRouter} from "../bootstrap-with-ui-router"; import {AppMainModule} from "./app.module.main.new"; // NOTE: uglyhack https://github.com/angular/angular/issues/16484#issuecomment-298852692 setAngularLib(angular); // TODO: remove after upgrade deferAndSyncUiRouter(AppMainOldModule); platformBrowserDynamic() .bootstrapModule(AppMainModule) // TODO: remove after upgrade .then(platformRef => bootstrapWithUiRouter(platformRef, AppMainOldModule)); 

There are places that are not obvious even according to the official documentation of the router, for example, using angularjs-like injections for OnEnter / OnExit hooks in the angular part of the routing:


 testBaseOnEnter.$inject = [ "$transition$" ]; export function testBaseOnEnter(transition: Transition) { const roomsService = transition.injector().get<RoomsService>(RoomsService); ... } // test page { name: ROOMS_TEST_STATES.base, url: "/test/{hash:[az]{8}}?tool&studentId", ... onEnter: testBaseOnEnter, }, 

Information about this had to be obtained through the gitter ui-router channel, part of it was already included in the documentation.


Protractor


A bunch of e2e tests work through a protractor. Of the problems in the hybrid mode, we faced only the fact that the waitForAngular method had completely waitForAngular . The QA team cracked some of their hacks, and also asked us to implement a meta-tag in the header with a counter of active api requests, so that, on the basis of this, we could understand when the main activity on the page stopped.


The counter was made through the HttpClient Interseptors that appeared in ng4:


 @Injectable() export class PendingApiCallsCounterInterceptor implements HttpInterceptor { constructor( private pendingApiCallsCounterService: PendingApiCallsCounterService, ) { } public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { this.pendingApiCallsCounterService.increment(); return next.handle(req) .finally(() => this.pendingApiCallsCounterService.decrement()); } } @Injectable() export class PendingApiCallsCounterService { private apiCallsCounter = 0; private counterElement: HTMLMetaElement; constructor() { this.counterElement = document.createElement("meta"); this.counterElement.name = COUNTER_ELEMENT_NAME; document.head.appendChild(this.counterElement); this.updateCounter(); } public decrement(): void { this.apiCallsCounter -= 1; this.updateCounter(); } public increment(): void { this.apiCallsCounter += 1; this.updateCounter(); } private updateCounter(): void { this.counterElement.setAttribute("content", this.apiCallsCounter.toString()); } } @NgModule({ providers: [ { provide: HTTP_INTERCEPTORS, useClass: PendingApiCallsCounterInterceptor, multi: true }, PendingApiCallsCounterService, ] }) export class AppModule { } 

At the end of this story, we share new conventions that help the team get used to working at Angular.


')

Source: https://habr.com/ru/post/348512/


All Articles