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


どうも、らこです。物好きなみなさんはもう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リクエストは次のように書きます

```js
$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](https://github.com/laco0416/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です。

```ts
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`に置き換えるべきですが)

```ts
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用にコピーして書き換える必要はありません

```ts
@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の[テストコード](https://github.com/laco0416/angular-http-harness/tree/master/test)では実際にAngular 1のAngular 2のサービスをそれぞれのDI機構でテストしています。ちなみに、このテストコードには私が作った[angular2-testing-micro](https://github.com/laco0416/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移行作業が少しでも楽になることを祈っています。:)