AngularJSからAngularへの移行:ハイブリッドモードの問題と解決策(2/3)







ハイブリッドモードでの移行は自然な手順であり、Angularチームによって十分に準備され、説明されています。 ただし、実際には、その場で対処する必要がある困難とギャグがあります。 今日のAngularへの移行に関する記事の続きでは、Skyengチームが遭遇した問題について話し、ソリューションを共有します。







最初の部分3番目の部分







文字列からの動的コンパイル



angularjsでは、すべてが非常に簡単です:







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





そして、Angularでは、そうではありません。







最初の解決策は、JiTコンパイラーを介して角度からオプションを取得することです。 これは、実動アセンブリーでは、静的コンポーネントのAoTコンパイルにもかかわらず、重いコンパイラーがドラッグして動的テンプレートをアセンブルすることを意味します。 次のようになります。







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





そして、すべてが比較的良いようです(バンドル内の厚いコンパイラは、他のライブラリの山とプロジェクト自体のコードによって、それがtodoリスト以外のものである場合でもまだ平準化されています)が、ここで具体的にこの問題を入力しました:









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







かなり大きなスライドですが、演習スライドの1つを6秒でコンパイルします。 3秒間、理解できないダウンタイムがあるという事実にもかかわらず。 この問題の答えから判断すると、今後数か月間状況は変わらず、別の解決策を探す必要がありました。







また、この場合、AoTアセンブリ中に既にコンパイルされたスライドで使用されているコンポーネントのファクトリを使用できないことが判明しました。 JiTコンパイラキャッシュにデータを入力する方法はありません。 このようなコンポーネントは、AoTビルド中のバックエンドと最初のスライドのコンパイル時のランタイムで、基本的に2回コンパイルされました。







haste の2番目の解決策は、angularjsから$compile



を使用してテンプレートを$compile



することです(ハイブリッドとアンギュラーがまだあります)。







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





角度コンポーネントは、角度からDynamicTemplateComponent



アップグレードバージョンを使用し、 $compile



サービスを使用して、角度からすべてのコンポーネントがダウングレードされたテンプレートを構築しました。 このような短いレイヤーは、角度-> anglejs($コンパイル)->角度です。







このオプションには問題がほとんどありません。たとえば、角度からアセンブリコンポーネントを介してコンポーネントを注入することはできませんが、主なことは、アップグレードの終了後、角度を切り取ると機能しないことです。







追加のグーグルと角度のギターで人々を殺すことは、 3番目の解決策につながりました:そのような場合の角度のオフサイトで直接使用されるもののトピックに関するバリエーション、すなわち、テンプレートをDOMに直接挿入し、見つかったタグの上にすべての既知のコンポーネントを手動で初期化する。 リンクのコード







既知の各コンポーネント(サービスでCONTENT_COMPONENTS



トークンを取得)ごとに、DOMに到着したテンプレートをそのまま挿入し、対応するDOMノードを探して初期化します。







マイナスのうち:









しかし、一般に、動的テンプレートを初期化するかなり機敏な方法があります。これは、基本的に、JiTコンパイラと同様に、特に角度のないツールを使用して遅延なしでタスクを解決します。







これは停止できるように思えますが、私たちにとっては、角度がコンテンツ投影でどのように機能するかによって、問題は完全には解決されていません。 特定の条件下でのみいくつかのコンポーネントのコンテンツを初期化する必要があります(スポイラーの種類によって)。これは通常のng-content



を使用する場合は不可能であり、コンテンツのアセンブル方法の特性によりng-template



挿入できません。 将来的には、より柔軟なソリューションを探します。おそらく、htmlコンテンツをJSON構造に置き換えます。これに応じて、スライドをコンテンツの一部の動的な表示/非表示を考慮した通常の角度コンポーネントでレンダリングします( ng-content



代わりに自己記述コンポーネントを使用する必要がありng-content



)。







4番目のオプションは誰かに適しているかもしれません。これは、angular 6- @angular/elements



リリースでベータとして正式に利用可能になります。 これらは、角度を介して実装されるカスタム要素です。 何らかのタグで登録し、このタグを何らかの方法でDOMに挿入すると、通常の機能をすべて備えた本格的な角度付きコンポーネントが自動的に初期化されます。 制限事項-そのような要素のイベントを介したメインアプリケーションとの相互作用。







これらに関する情報は、これまでのところ、ng会議からのいくつかのスピーチ、これらのスピーチに関する記事、および技術的なデモの形でのみ利用可能です。









角度のあるサイトは、最初のバージョンの@angular/elements



を使用して、現在のビルドメソッドの代わりにそれらに切り替えることを直ちに計画します。









変更検出



ハイブリッドでは、角度と角度の間のCDの動作にいくつかの不快な問題があります。







Angular ZoneのAngularJS



ハイブリッドの初期化後すぐに、angularjsコードがangleゾーンで実行されるという事実のためにパフォーマンスの低下が発生し、 setTimeout



/ setInterval



と、angularjsコードと使用されるサードパーティライブラリからのその他の非同期アクションにより、角度CDのティックがプルされます。これは、 $digest angularjs



ます。 つまり 以前の場合、サードパーティのライブラリのアクティビティからの余分なダイジェストを心配することはできませんでした。 anglejsはCDを明示的にキックする必要がありますが、今ではくしゃみごとに機能します。







これは、 NgZone



サービスを(ダウングレード経由で)転送し、 NgZone



でサードパーティライブラリまたはネイティブタイムアウトの初期化を処理することで修復されます。 将来、彼らはハイブリッドを初期化して、角と角のCDが原則的に互いに引っ張らないようにすることを約束します(角は角ゾーンの外側で機能します)。異なるピース間の相互作用のために、対応するフレームワークのCDを明示的に引っ張る必要があります。







downgradeComponentおよびChangeDetectionStrategy.OnPush



ダウングレードされたコンポーネントはOnPush



正しく動作しません-入力を変更するときに、このコンポーネントのCDがぎくしゃくしません。 コード







changeDetection: ChangeDetectionStrategy.OnPush,



angular.component



すると、カウンターが正しく更新されます。







ソリューションから、コンポーネントからOnPush



を削除するのは、角度コンポーネントのテンプレートで使用されている場合のみです。







UIルーター



元々、新しい格納庫で動作するui-routerがあり、ハイブリッドモードで動作するためのハックがたくさんありました。 彼はアプリケーションのブートストラップと分度器の問題について多くの騒ぎがありました。







その結果、このような初期化ハッキングに遭遇しました。







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





および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));
      
      





ルーターの公式ドキュメントによれば明らかでない場所がいくつかあります。たとえば、ルーティングの角の部分でOnEnter



/ OnEnter



フックにOnEnter



ような注入を使用する場合です。







 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, },
      
      





これに関する情報は、ui-routerのgitterチャネルを介して取得する必要がありましたが、その一部はすでにドキュメントに含まれています。







分度器



分度器を通して、e2eテストがたくさんあります。 ハイブリッドモードの問題のうち、彼らはwaitForAngular



メソッドがwaitForAngular



落ちたという事実にのみ遭遇しwaitForAngular



。 QAチームは、ハッキングのいくつかを掘り、また、ページのメインアクティビティがいつ停止したかを理解するために、アクティブなapiリクエストのカウンターを含むヘッダーにメタタグを実装するように依頼しました。







カウンターは、ng4に登場するHttpClientインターセプターによって作成されました。







 @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 { }
      
      





このストーリー最後で 、Angularでの作業に慣れるのに役立つ新しい規則を共有します。








All Articles