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自体の開発者によると、完全に通常のデフォルトの手順であると考える価値があります。 彼を恐れる必要はありません。
計画
アクションのシーケンスは次のようになりました。
- ハイブリッドアプリケーションの初期化:角度をブートストラップするブートストラップ角度。 すべてがそのまま残りますが、(ハイブリッドモードが機能している間)遅くなり、長く開始するようになります。 コントローラーを
head
に投げる機会はもうありません。タイトル/ファビコン/メタタグのすべての作業は、頭の必要な要素と直接やり取りするサービスに送信されます。 - アングルへのサービスの転送:最も簡単。 書き換えられたサービスは、まだコンポーネントを実行しているAngularJSからすぐに利用可能になります。 依存関係のない最も単純なものから始まり、より複雑なものへ。
- フクロウの残りの部分を描画します:基本コンポーネント(GUIおよび他のコンポーネント/ディレクティブを使用しない他のすべて)を転送します。 可能であれば、ユニット単位でコンポーネントを下から上に移動します。
- 羽をとかす:ページのコンポーネントを転送し、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のインポート)
アップグレードされたコンポーネントがtransglud( ng-content
)を使用する場合、角度テンプレートからコンポーネントを使用するとき:
- マルチスロットのトランスクロスは機能せず、1つの
ng-content
介してすべてを1つのピースに転送する機能のみが機能します。 - そのようなコンポーネントの翻訳では、UIビューをスローできません。 動作しません(ビューポートコンポーネントをアップグレードしようとすると中断しました)。
- コンポーネントがこのように使用される場合、そのアップグレードを使用されているすべての場所のアップグレードに延期するか、すでにアップグレードされたコンポーネントの並列操作のためにそのコピーを作成します。
角度成分で角度成分を使用する場合、入力は通常の角度成分のように( []
および()
を使用して)書き込まれますが、 kebab-case
<vim-angular-component [some-input]="" (some-output)=""> </vim-angular-component>
このようなテンプレートをアングルで書き換えるときは、camelCaseでケバブケースを編集します。
乗車ではないので、 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
を使用した場合、これを構造ディレクティブに置き換えて、コンポーネントを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で慣れなければならない新しい規則について説明します 。