どうも、らこです。物好きなみなさんはもう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
と同じインタフェースを持っています。get
やpost
メソッドの戻り値は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移行作業が少しでも楽になることを祈っています。:)