AngularJS
Angular2

Angular 2への移行を助けるライブラリ angular-http-harness の紹介

More than 3 years have passed since last update.

どうも、らこです。物好きなみなさんはもうAngular 2への移行を始めていると思いますが、$httpの移行どうしてますか?

Angular 1の$httpからAngular 2のHttpへの移行を助けるライブラリ angular-http-harness を作ったので、何が問題で、何を解決できて、どう嬉しいのかを紹介します。


用語の統一


  • サービス: ビューから切り離されたアプリケーションまたはライブラリのロジック。


  • $http: Angular 1のHTTP通信モジュール


  • Http: Angular 2のHTTP通信モジュール


$httpの移行に伴う問題

ほぼすべてのAngular 1のアプリケーションは$httpを使っているんじゃないかと思うので今更説明の必要はないと思いますが、改めて言うと$httpはAngular 1で(たいていはAjaxのために)HTTP通信を行うのに便利なサービスです。

$httpの特徴は Promise ベースのAPIであることです。例えば簡単なGETリクエストは次のように書きます

$http.get("/sample.json")

.then(resp => {
console.log(resp);
})
.catch(error => {
console.error(error);
});

そして、$httpはほとんどの場合、抽象化や単体テストのために、ある程度のサービスにラップされて使われます。例えば上記のリクエストを行うサービスをSampleServiceとして定義すると次のようになります

class SampleService {

constructor($http) {
this.$http = $http;
}

getJSON() {
return this.$http.get("/sample.json");
}
}

このSampleServiceをAngular 1のangular.service()を通すことでDIで$httpへの依存が解決された状態で使用できるわけです。

angular.module("app.service").service("sampleService", ["$http", SampleService]);

class SampleController {

constructor(sampleService) {
this.sampleService = sampleService;
}
}


さあAngular 2に移行しよう

Angular 2に移行するとき、どうしてもディレクティブやコントローラーからコンポーネントへの移行に目が行きがちですが、サービス層のほうがどちらかというと重要です。適切にモジュール化が行われていれば、コンポーネントは基本的にはデータをビューに投影するのが主な役割なわけですから、データが間違っていれば意味がありませんし、そのデータを作るのはサービスです。なのでまずサービスが移行できなければコンポーネント化は進められません。

よくある移行戦略は一時的なコピーを作る方法です。既存のTestServiceをコピーしてNg2TestServiceを作り、そこでHttpに依存するようにします。そしてTestServiceに依存しているディレクティブやコントローラーもコピーしてNg2TestServiceに依存するように書き換える方法です。

Angular 1のコードはそのままに、同じ動きのAngular 2のコードを併存させていくので、アプリケーションを壊さずに段階的に移行できるのが利点です。ある程度の単位がコピーできたらその根っこの部分を書き換えてAngular 1のコードを捨てていくことができます。

ただしこの方法には問題があります。一時的にAngular 1のコードをコピーして変更していくので、現行のAngular 1アプリケーションの機能開発との差分が発生します。新しい機能が加わったり既存のコードに修正が入るたびにいちいちAngular 2側に反映するか、最後にまとめて差分をマージする必要があり、非常に大変です。これを防ぐには機能開発を完全に止めるしかなく、その期間の工数がすべて移行に割かれてしまうのが問題です。それでよいプロジェクトやチームであれば別ですが、普通はエンジニアがAngular 2化したいと言っても現行アプリの開発を止めることは許されないでしょう。

特に$httpはHTTP通信を行うので、クライアント側だけでなくサーバー側との協調が必要です。サーバー側のAPIに変更があった場合、サービスも対応しなければなりません。

つまり、Angular 2化を進めつつ、現行のアプリケーションの変更にも追従することが必要です。


angular-http-harness

そのために私が作ったのが angular-http-harness です。このライブラリはHTTP通信を行うサービスを、「DIエントリポイント」と「サービスロジック」に分割することができ、Angular 1とAngular 2で同じサービスを使えるようになります。つまり、共通のロジックを使っているので自動的にAngular 1側の変更が反映されるということです。


HttpHarnessクラス

angular-http-harnessが提供する機能はHttpHarnessクラスだけです。これはAngular 1の$httpとAngular 2のHttpの違いを吸収して、抽象化し、同じインタフェースで扱えるようにするラッパーです。

まずは既存のAngular 1のサービスをHttpHarnessを使うように書き換えます。そして、このクラスを直接使うのではなく、抽象クラスとしておきます。ぱっと見ただけではAngular 2でHttpを使っているサービスのコードとほとんど変わらないですね!

なお、ここから先のコードはAngular 2を踏まえてTypeScriptです。

export abstract class TestService {

private http: HttpHarness;

constructor(http: HttpHarness) {
this.http = http;
}

get(id: string): Observable<TestModel> {
return this.http.get(`/${id}`)
.map(resp => resp.json() as TestModel);
}
}

HttpHarnessは基本的にHttpと同じインタフェースを持っています。getpostメソッドの戻り値はObservable<Response>です。


Angular 1用サービスを作る

次に、Angular 1用のサービスを新しく作ります。Ng1TestServiceクラスを作り、先ほど作ったTestServiceを継承します。Ng1TestServiceクラスで記述するのはAngular 1特有の部分、つまりDIの解決だけです。コンストラクタで$httpを受け取り、それをHttpHarness.fromNg1($http)でラップして親クラスのTestServiceに渡します。

重要なのは、.service()関数で提供するのはTestServiceではなくNg1TestServiceであることです。サービスの名前("testService")は変わっていないので、サービスを利用する側(コントローラーやディレクティブ)は単にPromiseからObservableに対応するだけでよいです。RxJSにはObservable#toPromise()というメソッドもあるので、既存のコードをほとんど変えずに1メソッド加えるだけで良い場合がほとんどでしょう。(将来的にはObservableのsubscribeに置き換えるべきですが)

export class Ng1TestService extends TestService {

constructor($http: angular.IHttpService) {
super(HttpHarness.fromNg1($http));
}
}

angular.module("app").service("testService", ["$http", Ng1TestService]);

この状態でまずは現行のアプリケーションに影響がないことを確かめましょう。


Angular 2用サービスを作る

Angular 2用にサービスを作るのは簡単です。Ng2TestServiceクラスを作ってTestServiceクラスを継承し、コンストラクタで親クラスにHttpHarness.fromNg2(http)を渡すだけです。ロジックはすべてTestServiceが持っているので、Angular 2用にコピーして書き換える必要はありません

@Injectable()

export class Ng2TestService extends TestService {

constructor(http: Http) {
super(HttpHarness.fromNg2(http));
}
}


利点

angular-http-harnessでサービスのロジックを共通化し、DIのエントリポイントだけを分割することで、既存のアプリケーションに影響をあたえることなく、機能開発を止めることもなく、シームレスに移行作業を行うことができます。

また、DIが完全に独立しているので、angular2/upgradeのUpgradeAdapterを使った複雑なアップグレード、ダウングレードをする必要もなく、移行中のAngular 2のコードだけをテスト可能です。

angular-http-harnessのテストコードでは実際にAngular 1のAngular 2のサービスをそれぞれのDI機構でテストしています。ちなみに、このテストコードには私が作ったangular2-testing-microを使っていて、Angular 1とAngular 2を両方共karma + mocha + power-assertの構成でテストできるようになっています。


まとめ

完全に自作ライブラリの宣伝でしたが、Angular 2を普及させるにはどうしてもAngular 1からの移行は外せない問題です。特に大規模なアプリケーションになればなるほど、現行アプリケーションの開発を止めることなく、段階的、部分的に移行作業をする手法が必要です。

angular2/upgradeは便利ですが、無理にAngular 1のコードをAngular 2側で呼び出したり、その逆を行ったりして予期せぬ挙動に悩まされることが結構あります。

angular-http-harnessのようにコードの共通化とDIエントリポイントの分割を行うことで、場合によってはangular2/upgradeよりスムーズに移行ができるでしょう。

みなさんのAngular 2移行作業が少しでも楽になることを祈っています。:)