AngularJS を Angular に移行: ng-annotate 相当の機能を TypeScrpt ファイルに適用
2017/12/14
  • このエントリーをはてなブックマークに追加

ソフトウェアエンジニアの花岡です。現在、私は弊社サービス Kabuku MMS を AngularJS から Angular に移行しようとしています。その際、ng-annotate 相当の機能を TypeScript ファイルに適用する必要があったためその背景からご紹介します。

作ったものは https://github.com/kabuku/ts-ng-annotate です。

背景

現在 Kabuku MMS は AngularJS 1.5 を利用しています。コードベースは TypeScript 化済みです。Angular への移行では Upgrading from AngularJS に従い ngUpgrade を利用することにしました。

ngUpgrade を使えば、AngularJS と Angular のコードを同居させてコンポーネントやサービスを互いに利用することができます。そのため以下のステップでの移行が可能です。

  1. 既存の AngularJS のコードベースをなるべくそのままインポートして動作させる(本記事はここで遭遇した問題のひとつについてです)
  2. 既存の AngularJS のコードベースを段階的に Angular に移行していく

Angular への移行作業を進めながらも、継続して既存のコードベースをメンテナンスできることが大きな利点です。

また、Upgrading from AngularJS では SystemJS を使っていますが、Angular CLI を使ってふつうの Angular アプリケーション化することにしました。

ここで問題だったのが Angular CLI で ng-annotate が使えないというものです。

ng-annotate とは

AngularJS では Dependency Injection(DI)のために関数やコンストラクタのパラメータ名が利用されます。たとえば

angular.module('myapp').factory('MyService', function($http) {});

のような MyService を定義すると、$http というパラメータ名をもとに $http サービスが DI されます。このままでは minify したときにパラメータ名も minify されてしまい DI できなくなるため

angular.module('myapp').factory('MyService', ['$http', function($http) {}]);

のように annotate する必要があります(Dependency Injection 参照)。しかしこのような annotation を手で書くのはメンテナンス性の観点から考えて避けたいものです。

ng-annotate を使えば、minify の前に自動で annotate させることができます。ng-annotate への入力は JavaScript である必要がありますが、TypeScript のコードベースに対しても transpile 後に ng-annotate すれば問題ありません。実際そのようにして現在は使っています。

問題点と対策

しかし Angular CLI では ng-annotate が使えません(以下参照)。

そこで transpile の前に ng-annotate 相当の機能を適用できないか、やってみた結果が ts-ng-annotate です。

ts-ng-annotate を既存の AngularJS のコードベースに適用して、その結果をバージョン管理するという使い方を想定しています。AngularJS の dependency annotation はノイズでしかないため本来はコミットに含めたくはないところですが、そのコンポーネント/サービスを Angular に移行し終わるまでの暫定的な処理として受け入れています。

作ったもの

ts-ng-annotate は以下をもとに自動で annotate します。

  1. angular.Module のメソッド呼び出し
    • ただし AngularJS Style Guide の Definitions (aka Setters) に従っていること、かつ
    • インラインの関数式/アロー関数引数に対して
  2. 関数式/アロー関数の 'ngInject' prologue
  3. コンストラクタの 'ngInject' prologue

たとえば

angular.module('myapp').factory('MyService', ($http) => {});

は 1 により

angular.module('myapp').factory('MyService', ['$http', ($http) => {}]);

のように annotate されます。また

class MyService {
    constructor($http) {
        'ngInject';
    }
}

は 3 により

class MyService {
    static $inject = ['$http'];
    constructor($http) {
        'ngInject';
    }
}

のように annotate されます。

さらに、関数式/アロー関数の一部の annotation に対して autofix 機能があります。たとえば

angular.module('myapp')
    .factory('MyService1', ['$b', ($a) => {}])
    .factory('MyService2', ['$b', '$c', ($a) => {}])
    .factory('MyService3', ['$b', ($a, $b) => {}])
    .factory('MyService4', ['$b', () => {}]);

angular.module('myapp')
    .factory('MyService1', ['$a', ($a) => {}])
    .factory('MyService2', ['$a', ($a) => {}])
    .factory('MyService3', ['$a', '$b', ($a, $b) => {}])
    .factory('MyService4', () => {});

のように autofix されます。

またこのコードのように、angular.module のメソッド呼び出しに関してはメソッドチェインもサポートしています。

最初は AST を変更して出力するという発想でしたが、改行が除去されながら pretty printされ差分が膨大になったため諦めて、https://github.com/Microsoft/TypeScript/issues/7580#issuecomment-198552002 を参考に、annotate する位置を見つけてソースコードの後ろから順に書き換えていくようにしました。

課題

Type Checker の利用

class MyService {
    constructor($http) {
    }
}
angular.module('myapp').factory('MyService', MyService);

のような 'ngInject' prologue のないコードも angular.Module メソッドの呼び出しをもとに Type Checker を使えばサポートできそうな気がしています。この辺りは Architectural Overview がわかりやすかったです。

Custom Transformer 化

https://github.com/Microsoft/TypeScript/issues/14419 にあるように Custom Transformer が tsconfig.json で使えるようになれば(すぐにはできないみたいですが)、transpile 時に annotate することができます。

コードから annotation(ノイズ)がなくなるためこれが一番いい形だと思います。もしかしたら ng-annotate(babel-plugin-angularjs-annotate) を使うことも可能になるでしょうか。

まとめ

ng-annotate と比べると一部の機能のみのサポートですが、Kabuku MMS では十分でしたのでこれで Angular への移行を進めています。

最後に、前述した課題の項目はもちろん細かなところからでも PR いただけると幸いです。