お布団宇宙ねこ

にゃーん

AngularJS から Angular への移行手法について振り返る

こちらは Angular Advent Calendar 2022 23 日目の記事です。

GMOペパボで働き、現在進行形で6,7年ほど AngularJS および Angular で構築されたWebアプリケーション開発に携わっています。(参考までに私が携わっている開発の一部が下記ブログで見れます)

今回は昨年LTSが終了した AngularJS を Angular に移行するための二つの機能について私が使ってみた感想を交えつつ振り返ってみたいと思います。

ngUpgrade による段階的な移行

Angularの公式ガイドを見ると AngularJS から Angular への移行方法として ngUpgrade というライブラリを提供しています。

ngUpgerade ライブラリにある UpgradeModule を利用することで Angular を AngularJS のアプリケーションに組み込むことができ、そこから AngularJS と Angular を同時に動かしつつ AngularJS のコンポーネントを Angular に一つずつ移行することで段階的に移行させられるというものです。

例えば Angular のコンポーネントやサービスを AngularJS アプリケーション内で使いたい場合は以下のように downgradeComponent , downgradeInjectable を使うことで AngularJS にダウングレードして使うことができます。

import { downgradeComponent, downgradeInjectable } from '@angular/upgrade/static';

angular.module("exampleApp", [])
  .directive(
    "hogeButtonComponent",
    downgradeComponent({ component: hogeButtonComponent }) as angular.IDirectiveFactory
  )
  .factory(
    "PiyoService",
    downgradeInjectable(PiyoService)
  );

このようにコンポーネントやサービス単位で置換ができるため AngularJS と Angular のコードの二重管理を避けつつ Angular のコードが書けるため、私が開発を担当しているプロダクトにも当初この手法を取り入れて移行アップグレードを進めていました。しかし、プロダクトの複雑さも相まってか簡単には移行できませんでした。

(これは個人的な感想であり ngUpgrade による移行手法を下げる意図はありません)

ngUpgrade では移行のための特殊なコード記述が多い

上記のコード例のように Angular のコードを AngularJS にダウングレードすること自体は難しくありません。

一方で、 AngularJS のコンポーネントやサービスを Angular 側で利用したい場合が少し厄介です。

Angular のコンポーネントに AngularJS のコンポーネントを含めなければならない例を考えてみましょう。この場合は UpgradeComponent を利用してコンポーネントをアップグレードする必要があります。

コンポーネントクラスは以下のようになりますが、アップグレードになった途端に記述量が増えました。

// AngularJS Code
class MessageBoxController {}

export const messageBox = {
  template: "<p>{{ message }}</p>",
  bindings: {
    message: "<"
  },
  controller: MessageBoxController
};

// Angular Code
import { Directive, ElementRef, Injector, Input } from '@angular/core';
import { UpgradeComponent } from '@angular/upgrade/static';

@Directive({
  selector: 'message-box'
})
export class MessageBoxDirective extends UpgradeComponent {
  @Input() message;

  constructor(elementRef: ElementRef, injector: Injector) {
    super('messageBox', elementRef, injector);
  }
}

また、サービスのアップグレードの場合はファクトリプロバイダーを利用して以下のような記述をする必要があります。

import { userStore } from 'services/user-store';

export function userStoreServiceFactory(i: any) {
  return i.get('userStore');
}

export const userServiceProvider = {
  provide: userStore,
  useFactory: userStoreServiceFactory,
  deps: ['$injector']
}

ボトムアップで常に AngularJS に依存しないコンポーネントやサービスを作ることができるなら上記のようなアップグレードの記述が必要になることはありません。

しかし、私が担当するプロダクトでは大量のDIや状態管理を担うサービスや適切な粒度でコンポーネント化されていないテンプレートを持つコントローラーやコンポーネントが多くあったためアップグレードが必須でした。そのため ngUpgrade を利用することで新しく作るコンポーネントやサービスを Angular で書くことはできましたが、一方で組み込み先の AngularJS のことを常に考慮しつつ設計・実装しなければなりませんでした。

このように担当プロダクトではアップグレード時の移行コストが低くないため移行作業が難航していました。

Angular Elements による段階的な移行

Angular のコンポーネントを Custom Elements に変換する Angular Elements という機能を利用することでも AngularJS からの段階的な移行が実現できます。

Angular Elements によって Custom Elements としてパッケージ化されて閉じられることで外側がどんなアプリケーションであっても組み込めるので AngularJS アプリケーションにも利用できるわけです。

具体的な移行方法については lacolaco さんの記事で紹介されているため今回は割愛させていただきます。

Angular ElementsによるAngularJSの段階的アップグレード戦略 | lacolaco/tech

移行のためのコードとして純粋な Angular のコードが書ける

ngUpgrade による移行で問題点として挙げていたダウングレード・アップグレードのような特殊なコード記述が不要となることで移行先のコードは純粋な Angular のコードを書ける点はとても大きなメリットだと感じました。ある程度の規模のプロダクトを特殊なコード記述のルールを作ることなく移行を進められることは実装する開発者やそのコードを読むレビュアーという視点で見た時に大変助かりました。

一方で Angular Elements を利用したとしても移行途中の場合は AngularJS とのデータのやり取りが発生しますが、以下のような AngularJS とのやり取りを担うコンポーネントを間に噛ませることで AngularJS への依存度を下げることはできると考えています。(担当プロダクトではこのような役割を持ったコンポーネントを Adapter コンポーネントと呼んでいます)

@Component({
    template: `<foo-container></foo-container>`
})
class FooAdapterComponent {
  // 1. AngularJS -> Angular
  // 属性を経由してデータを渡す
  @Input() hoge: any;

    // 2-1. Angular -> AngularJS
  // イベントを発火して AngularJS 側がイベントリスナーで受け取る
  @Output() event = new EventEmitter();
  @ViewChild(FooContainer) container: FooContainer;

  ngOnInit() {
    // 2-2. AngularJS -> Angular
    // userChange: EventTarget的な役割を担う
    window.userChange.addEventListener(event => {
            this.container.changeUser(event.user);
    });
  }
}

1で AngularJS から Angular に属性を経由してデータを渡したい場合は ngProp を利用して以下のように渡せます。

<foo-element ng-prop-hoge="piyopiyo"></foo-element>

まとめ

AngularJS を Angular に移行するための二つの機能について ngUpgerade と Angular Elements をそれぞれ振り返りました。

適切な粒度で分割されたコンポーネントなど AngularJS の各ロールが疎結合な状態であれば ngUpgerade による移行でも十分かもしれませんが、個人的にはそれを考慮してもほとんど移行を意識させずに Angular のコーディングができる Angular Elements は魅了的でした。

今回のような移行以外でも様々な用途で活躍できる Angular Elements をこれからも推していきたいと思います。