AngularJSからAngularへの移行:要素を転送するための目標、計画、規則(1/3)







1月、Skyengでは、VimboxプラットフォームのAngularJSからAngular 4への移行を完了しました。準備と移行中に、計画、問題解決、新しい作業規則に関する多くのメモを蓄積し、Habréに関する3つの記事で共有することにしました。 私たちのメモが、動き始めたばかりのVimboxプロジェクトと構造的に類似した有用なものになることを願っています。







なぜこれが必要なのですか?



まず、Angularはすべてにおいて、AngularJSよりも優れています。より速く、簡単で、便利で、バグが少ないです(たとえば、テンプレートを入力すると、それらと戦うのに役立ちます)。 これについて多くのことが述べられ、書かれていますが、繰り返すことには意味がありません。 これはAngular 2でも理解できましたが、1年前に移行を開始するのは怖かったです:Googleが後方互換性なしで次のバージョンですべてを逆さまにすることを決定したらどうなるでしょうか? 私たちには大きなプロジェクトがあり、本質的に新しいフレームワークへの移行には深刻なリソースが必要であり、2年ごとにそれをしたくありません。 Angular 4では、これ以上革命が起こらないことを期待できます。つまり、移行の時が来たということです。







次に、プラットフォームで使用されているテクノロジーを更新したかった。 「何かが壊れていない場合は修復しない」という原則に従ってこれが行われない場合、ある時点で、プラットフォームがゼロから書き直された場合にのみそれ以上の進歩が可能な境界を越えます。 遅かれ早かれ、いずれにせよ、Angularに切り替える必要がありますが、これを早めるほど、移行は安くなります(コードの量は常に増加しており、新しいテクノロジの利点をより早く得ることができます)。







最後に、3番目の重要な理由:開発者。 AngularJS-合格したステージ。タスクを実行しますが、開発は行われず、開発もされません。 プラットフォームは常に成長しています。 強力な開発者で構成される大規模なチームは存在せず、強力な開発者は常に新しいテクノロジーに関心があり、単に時代遅れのフレームワークに対処することに興味はありません。 Angularに切り替えると、優秀な候補者にとって欠員がより面白くなります。 次の2〜3年で、それらは非常に重要になります。







どのように進めますか?



移行はパラレルモードで実行できます。プラットフォームはAngularJSで実行され、最初から記述して新しいバージョンをテストし、ある時点でトグルスイッチを切り替えるだけです。 2番目のオプションは、AngularJSとAngularの両方が同時に機能するプロダクションで直接変更が発生する場合のハイブリッドモードです。 幸いなことに、このモードはよく考えられ、 文書化されています







ハイブリッド移行モードとパラレル移行モードの選択は、製品の開発状況によって異なります。 アクションプランを準備していた開発者は、別の会社で並列アプローチの経験がありましたが、その場合、依存関係は少なく(コードはほぼ同じでしたが)、最も重要なことに、1か月間すべての開発を停止し、移行のみを処理する機会がありました。 モードの選択は、そのような贅沢を買う余裕があるかどうかに依存します。







私たちにとって、並行移行にはリスクがありました:新しいバージョンの準備中に、すべての開発が停止し、移動の期間をどれだけうまく計算しても、プロセスが引きずられる可能性があり、何かに遭遇し、次に何をすべきか理解できません。 ハイブリッドモードでは、この状況では、停止してソリューションを穏やかに探すことができます。これは、現在の実稼働バージョンがまだ稼働しているためです。 効率的に機能せず、少し難しくなりますが、プロセスは停止しません。 並行して、対応する損失を伴うロールバックが発生していました。 私たちの移行プロセスが本当に引きずられたことに注目する価値があります-412時間を計画しましたが、実際には2倍(830)になりました。 しかし、同時に、何も止まらず、新しい機能が絶えず展開され、すべてが正常に機能しました。







一般的に、ハイブリッド遷移は不可抗力ではなく、Angular自体の開発者によると、完全に通常のデフォルトの手順であると考える価値があります。 彼を恐れる必要はありません。







計画



アクションのシーケンスは次のようになりました。







  1. ハイブリッドアプリケーションの初期化:角度をブートストラップするブートストラップ角度。 すべてがそのまま残りますが、(ハイブリッドモードが機能している間)遅くなり、長く開始するようになります。 コントローラーをhead



    に投げる機会はもうありません。タイトル/ファビコン/メタタグのすべての作業は、頭の必要な要素と直接やり取りするサービスに送信されます。
  2. アングルへのサービスの転送:最も簡単。 書き換えられたサービスは、まだコンポーネントを実行しているAngularJSからすぐに利用可能になります。 依存関係のない最も単純なものから始まり、より複雑なものへ。
  3. フクロウの残りの部分を描画します:基本コンポーネント(GUIおよび他のコンポーネント/ディレクティブを使用しない他のすべて)を転送します。 可能であれば、ユニット単位でコンポーネントを下から上に移動します。
  4. 羽をとかす:ページのコンポーネントを転送し、AngularJSを切り取ります。


転送ルール



さて、ついに約束の技術的詳細に進みましょう。 これらの記録を少し掃除し、プラットフォームのみに関する不必要な詳細を削除しました。 これらは普遍的なソリューションではありませんが、誰かが発生する問題を解決するのに役立つかもしれません。







テキストの壁を塞がないように、ネタバレの下にすべてを隠します。







個々のアイテムを転送する方法



モジュール

何かをアップグレードし始めているモジュールに角度モジュールがない場合、それを作成してメインアプリケーションモジュールにフックします。







 import {NgModule} from "@angular/core"; @NgModule({ // }); export class SmthModule {} @NgModule({ imports: [ ... SmthModule, ], }); export class AppModule {}
      
      





角度モジュールがまだ生きている場合、新しいモジュールには.new



接尾辞が付けられます。 角の古いモジュールと一緒に接尾辞を切り取ります。







サービス

良いケースでは、デコレータを追加し、エクスポートからdefault



を削除し、インポートを編集し(デフォルトを削除したため)、モジュールの角度にインポートし、モジュールの角度でダウングレードします:







 import {Injectable} from "@angular/core"; @Injectable() export class SmthService { ... } // angular module @NgModule({ providers: [ ... SmthService, ], }); // angularjs module import {downgradeInjectable} from "@angular/upgrade/static"; ... .factory("vim.smth", downgradeInjectable(SmthService))
      
      





サービスは、Angurjarsの古い名前で引き続き利用でき、追加の構成は必要ありません。







良いオプションは、すべてのインジェクションサービスが既にAngularに移行しているため、 templateCache



compiler



特定のものは使用されないことを意味します。







残りの95%のケースでは、まず注入されたものをアップグレードし、あらゆる種類の奇妙なサービスなどを取り除きます。







成分

メタデータを使用してデコレータをコントローラに証明し、デコレータの入力/出力を配置して、クラスの先頭に転送します。







 import {Component, Input, Output, EventEmitter} from "@angular/core"; @Component({ //   `-`     ,   camelCase selector: "vim-smth", //       require("./smth.html") templateUrl: "smth.html", }) export class SmthComponent { @Input() smth1: string; @Output() smthAction = new EventEmitter<void>(); ... } // angular module @NgModule({ declarations: [ ... SmthComponent, ], //         ,         exports: [ ... SmthComponent, ], }); // angularjs module import {downgradeInjectable} from "@angular/upgrade/static"; ... .directive("vimSmth", downgradeComponent({ component: SmthComponent }) as ng.IDirectiveFactory)
      
      





注入されたすべてのサービスは、すべてコンポーネント(それらをフックする方法-Anythingの下)を必要とし、テンプレート内で使用されるすべてのコンポーネント/ディレクティブ/フィルターは格納庫にある必要があります。







テンプレートで使用されるすべてのコンポーネント変数はpublic



として宣言する必要があります。そうでない場合は、AoTアセンブリに分類されます。







コンポーネントが(入力を介して)上記のコンポーネントから出力用のすべてのデータを受け取った場合、 changeDetection: ChangeDetectionStrategy.OnPush



をメタデータに大胆に書き込みます。 これは、コンポーネントの入力のいずれかが変更された場合にのみテンプレートをデータと同期する(このコンポーネントの変更検出を開始する)ことを角度に伝えます。 理想的には、ほとんどのコンポーネントがこのモードになっている必要があります(ただし、サービスを介して出力用のデータを受け取る非常に大きなコンポーネントのため、ほとんどそうではありません)。







指令

コンポーネントと同じですが、テンプレートと@Directive



デコレータはありません。 そこにあるモジュールにスローされ、他のモジュールのコンポーネントで使用するためにエクスポートされるものは同じでなければなりません。







camelCaseのセレクターは、コンポーネントテンプレートでも使用されます。







フィルター

これで@Pipe



なり、 PipeTransform



インターフェイスを実装するPipeTransform



ます。 コンポーネント/ディレクティブがあるモジュールに自分自身を投げ込み、他のモジュールで使用する場合はエクスポートする必要もあります。







camelCaseのセレクターは、コンポーネントテンプレートでも使用されます。







角度のディレクティブとフィルターは、角度のコンポーネントテンプレートでは使用できません。逆の場合も同様です。 フレームワーク間では、サービスとコンポーネントのみがスローされます。







エクスポート/インポートおよびインターフェース

まず、エクスポートのデフォルトを取り除きます。なぜなら、 AoTコンパイラーはそれを使用できません。







第二に、モジュールの現在の構造(非常に大きい)とインターフェースの使用(クラスがある同じファイルにヒープを置く)のために、そのようなインターフェースのインポートとデコレーターでの使用で面白いバグをキャッチしました:インターフェースがエクスポートを含むファイルからインポートされた場合インターフェースだけでなく、たとえばクラス/定数も使用されます。このようなインターフェースは、デコレータの横の入力に使用され( @Input() smth: ISmth



)、コンパイラはインポートエラーexport 'ISmth' was not found



。 これは、すべてのインターフェースを別のファイルに転送する(モジュールが大きいためにファイルが1ダースの画面に表示されるために悪い)か、インターフェースをクラスに置き換えることで修正できます。 クラスで置き換えることは機能しません。 複数の親から継承することはできません。







選択したソリューション:各モジュールにinterface



ディレクトリを作成しinterface



ディレクトリには、対応するインターフェイス(部屋、ステップ、コンテンツ、ワークブック、宿題など)を含む本質的に名前の付いたファイルが置かれます。 したがって、ローカルではなく使用されるすべてのインターフェイスがそこに置かれ、そのようなファイルディレクトリからインポートされます。







問題のより詳細な説明:

https://github.com/angular/angular-cli/issues/2034#issuecomment-302666897

https://github.com/webpack/webpack/issues/2977#issuecomment-245898520







機能(transklud、パラメーターの受け渡し、svgのインポート)



Transclub機能

アップグレードされたコンポーネントがtransglud( ng-content



)を使用する場合、角度テンプレートからコンポーネントを使用するとき:







  • マルチスロットのトランスクロスは機能せず、1つのng-content



    介してすべてを1つのピースに転送する機能のみが機能します。
  • そのようなコンポーネントの翻訳では、UIビューをスローできません。 動作しません(ビューポートコンポーネントをアップグレードしようとすると中断しました)。
  • コンポーネントがこのように使用される場合、そのアップグレードを使用されているすべての場所のアップグレードに延期するか、すでにアップグレードされたコンポーネントの並列操作のためにそのコピーを作成します。


パラメータ転送の機能

角度成分で角度成分を使用する場合、入力は通常の角度成分のように( []



および()



を使用して)書き込まれますが、 kebab-case









 <vim-angular-component [some-input]="" (some-output)=""> </vim-angular-component>
      
      





このようなテンプレートをアングルで書き換えるときは、camelCaseでケバブケースを編集します。







写真/ svgテンプレートが必要

乗車ではないので、 AoTコンパイラーはそれを誓います。 したがって、同じファイルをtsファイルにインポートし、コンポーネントのコンポーネントを介して転送します。







だった:







 <span> ${require('!html-loader!image-webpack-loader?{}!./images/icon.svg')} </span>
      
      





になりました:







 const imageIcon = require<string>("!html-loader!image-webpack-loader?{}!./images/icon.svg"); public imageIcon = imageIcon; <span [innerHTML]="imageIcon | vimBaseSafeHtml"> </span>
      
      





またはimg経由で使用するため







だった:







 <img ng-src="${require('./images/icon.svg')}" />
      
      





になりました:







 const imageIcon = require<string>("./images/icon.svg"); public imageIcon = imageIcon; <img [src]="imageIcon | vimBaseSafeUrl" />
      
      





動的なコンポーネントとパターン



$コンパイルなしの生活

文字列からの$compile



がないので、 $compile



はもうありません(実際、小さなハックがありますが、ここでは$compile



なしで95%のケースに対応する方法を示し$compile



)。







動的に挿入されたコンポーネントは、次のようにスローされます。







 @Component({...}) class DynamicComponent {} @NgModule({ declarations: [ ... DynamicComponent, ], entryComponents: [ DynamicComponent, ], }) class SomeModule {} //  @Component({ ... template: ` <vim-base-dynamic-component [component]="dynamicComponent"></vim-base-dynamic-component> ` }) class SomeComponent { public dynamicComponent = DynamicComponent; }
      
      





挿入されたコンポーネントのクラスは、サービス、入力、またはその他の方法でロールできます。







vim-base-dynamic-component



は、入力/出力をサポートする他のコンポーネントの動的挿入用に既に作成されたvim-base-dynamic-component



です(将来、必要に応じて)。







動的なtemplateUrlはありません

条件ごとに異なるテンプレートを出力する必要があり、そのために動的なtemplateUrl



を使用した場合、これを構造ディレクティブに置き換えて、コンポーネントを3つに分割します。 モバイル/非モバイルの出力を分割する例:







リクエスト/データ処理

モバイル向けのマッピング

デスクトップ用ディスプレイ







最初のコンポーネントには最小限のテンプレートがあり、データの操作、ユーザーアクションなどの処理に取り組んでいます(テンプレートは、簡潔さのため、コンポーネントのコンポーネントを個別のhtmlファイルとtemplateUrl



代わりに ''に置くのが理にかなっています)。 例:







 @Component({ selector: "...", template: ` <some-component-mobile *vimBaseIfMobile="true" [data]="data" (changeSmth)="onChangeSmth($event)"> </some-component-mobile> <some-component-desktop *vimBaseIfMobile="false" [data]="data" (changeSmth)="onChangeSmth($event)"> </some-component-desktop> `, })
      
      





vimBaseIfMobile



は、内部条件と渡されたパラメーターに従って対応するコンポーネントを表示する構造ディレクティブ(この場合はngIf



直接の類似物)です。







携帯電話とデスクトップのコンポーネントは、入力を介してデータを受信し、出力を介していくつかのイベントを送信し、必要なものの出力のみを処理します。 すべての複雑なロジック、処理、データの変更-それらを表示するメインコンポーネント。 そのようなコンポーネント(dextop / mobile)では、 changeDetection: ChangeDetectionStrategy.OnPush



安全に記述できます。







Angular Servicesの使用/ Angular Servicesのコンポーネント/コンポーネント



サービス/要因/プロバイダー

app/entries/angularjs-services-upgrade.ts



を開き、既存のコピーと貼り付けの例に従って(このファイル内のすべて):







 // EXAMPLE: copy-paste, fix naming/params, add to module providers at the bottom, use // ----- import LoaderService from "../service/loader"; // NOTE: this function MUST be provided and exported for AoT compilation export function loaderServiceFactory(i: any) { return i.get(LoaderService.ID); } const loaderServiceProvider = { provide: LoaderService, useFactory: loaderServiceFactory, deps: [ "$injector" ] }; // ----- @NgModule({ providers: [ loaderServiceProvider, ] }) export class AngularJSServicesUpgrade {}
      
      





すなわち 既存のブロックをコピーし、必要なサービスをインポートし、そのための定数/関数の名前を編集し、その中で使用されるサービスとその名前を編集します(ほとんどの場合、 SmthService.ID



代わりに、格納庫でサービスにアクセス( SmthService.ID



する名前を挿入する必要があります)、追加新しい定数smthServiceProvider



をファイルの最後にあるプロバイダーのリストにsmthServiceProvider



します。







このようなサービスは、ネイティブのAngularとして使用されます。クラスごとにコンストラクタに単純に注入できます。







成分

元のコンポーネントを含むファイルに(最初に)次のスタブを配置します。これにより、コンポーネントを角度環境にスローできます。







 import {Directive, ElementRef, Injector, Input, Output, EventEmitter} from "@angular/core"; import {UpgradeComponent} from "@angular/upgrade/static"; @Directive({ /* tslint:disable:directive-selector */ selector: "vim-smth" }) /* tslint:disable:directive-class-suffix */ export class SmthComponent extends UpgradeComponent { @Input() smth: boolean; @Output() someAction: EventEmitter<string>; constructor(elementRef: ElementRef, injector: Injector) { super("vimSmth", elementRef, injector); } } @NgModule({ declarations: [ ... SmthComponent, ] }) export class SmthModule {
      
      





この場合、 Component



代わりにDirective



デコレータが使用されることに注意してください。これは、Angularがこれを処理する方法の機能です。







すべての入力/出力(元のコンポーネントからのバインダー)を登録しdeclarations



対応するモジュールのdeclarations



コンポーネントを登録することを忘れないでください。







将来、このコンポーネントをアップグレードすると、そのようなスタブは角度の実際のコンポーネントになります。







コンポーネント(または古いコンポーネントディレクティブ)がコントローラー/リンク関数に$attrs



を注入する場合、そのようなコンポーネントは格納庫から格納庫にキャストできず、格納庫のアップグレードされたコピーの隣にアップグレードまたは配置する必要があります。







tslintエラーを無効にするには、セレクターの名前とディレクティブのデコレーターに対するクラスの不一致を誓わないことが必要です。 これらの行(コメント)は、コンポーネントのアップグレード後に削除する必要があります。









  • $q



    Promise



    サービスを使用すると、ネイティブPromise



    置き換えられます。 finally



    はありませんが、これはcore.js/es7.promise.finally



    によって修正され、現在は修正されています。 また、遅延はありません。ts-deferredが追加され、毎回自転車を書かないようにします。
  • $timeout



    $interval



    代わりに、ネイティブwindow.setTimeout



    window.setInterval



    $interval



    使用します。
  • ng-show="visible"



    代わりに、属性ng-show="visible"



    バインド[hidden]="!visible"



    ;
  • track by



    常にメソッドである必要があります(メソッドのTrack



    後置を忘れないでください):


 *ngFor="let item of items; trackBy: itemTrack" public itemTrack(_index: number, item: IItem): number { return item.id; }
      
      





  • 99%の場合、 $digest



    $apply



    $evalAsync



    などは、置換なしで切り取られます。
  • サービスインジェクションの場合、コンストラクタコンストラクconstructor(private someService: SomeService)



    に書き込むだけで、角度自体がどこから取得するかを理解します。
  • ディレクティブ内で、それがハングする要素はconstructor(private element: ElementRef)



    AfterViewInit



    からアクセス可能で、 AfterViewInit



    フックで初期化されます( ElementRef



    はDOMオブジェクトそのものではなく、 this.element.nativeElement



    によってアクセス可能this.element.nativeElement



    )。
  • ng-include



    置換なしでng-include



    なく、コンポーネントの動的作成を使用します。
  • angular.extend



    angular.merge



    angular.forEach



    などが欠落しており、ネイティブのjsとlodashを使用しています。
  • angular.element



    とそのすべてのメソッドが欠落しています。 @ViewChild/@ContentChild



    を使用し、ネイティブjsを処理します。
  • OnPush



    してコンポーネントの検出チェンジャーをプルする必要がある場合- private changeDetectorRef: ChangeDetectorRef



    挿入しprivate changeDetectorRef: ChangeDetectorRef



    およびプルprivate changeDetectorRef: ChangeDetectorRef



    this.changeDetectorRef.markForCheck()



    ;
  • テンプレートから$ctrl.



    を見つけました$ctrl.



    -名前によるsv-youおよびメソッドへの直接アクセス。
  • ng-bind-html="smth"



    -> [innerHTML]="smth"



  • $sce



    import {DomSanitizer} from "@angular/platform-browser";



    > import {DomSanitizer} from "@angular/platform-browser";



  • ng-pural



    > [ngPlural]



    https://angular.io/api/common/NgPlural
  • ngClass



    はそれができません


 [ngClass]="{ [ styles.active ]: visible, [ styles.smth ]: smth }"
      
      





したがって、配列に置き換えます







 [ngClass]="[ visible ? styles.active : '', smth ? styles.smth : '' ]"
      
      





  • ui-router



    サービスのクラスは@uirouter/core



    からインポートされ、古い$



    プレフィックスなしで挿入されます


 import {StateService, TransitionService} from "@uirouter/core"; constructor(stateService: StateService, transitionService: TransitionService) {
      
      





  • コンポーネントのデータ属性は、 attr.data-smth=""



    または[attr.data-smth]=""



    として登録されます。
  • コンポーネント内のrequire



    /ディレクティブは、現在のコンポーネントコンストラクターcontructor(private parentComponent: ParentComponent)



    のコンストラクターに直接コンポーネントクラスを挿入することで置き換えられます。 Angular自身は、これがコンポーネントであることを確認し、それをフックします。 微調整のために、 @Self



    (親間の検索)、 @Self



    (コンポーネント上で直接検索)、 @Optional



    (存在しない場合と存在しない場合があり、変数は未定義)の@Optional



    があります。 複数の@Host() @Optional() parentComponent: ParentComponent



    スローできます。 コンポーネント/ディレクティブをコンポーネント/ディレクティブに再利用できます。
  • 双方向バインディングは、そのコンポーネントでより明示的になり、同じ名前と接尾辞Change



    Output



    を必要としOutput





 export class SmthComponent { @Input() variable: string; @Output() variableChange = new EventEmitter<string>(); <vim-smth [(variable)]="localVar"></vim-smth>
      
      





  • 角度成分の可能な半透明の角度成分。 名前付きのtranslucをチェックする必要があります:動作するかどうか(セレクターで行われる角度で)


 <!-- angular --> <ng-content></ng-content> <!-- angularjs --> <vim-angular-component> transcluded data </vim-angular-component>
      
      





次のパートでは、ハイブリッドモードでの作業機能 と、Angularで慣れなければならない新しい規則について説明します








All Articles