Angular Universalを試して数か月経ったので学びを書いていく

(この内容は社内LTで使ったものを文章化して引き伸ばしたものです。)
(ゆっくり書いていたらAngular 4 RCが出てしまいました。。。)

Angular Universalを使って、隙間時間にちまちまと社内向けのサービスを開発しました。
その間に試していて、詰まったり引っかかったりしたことを連々と書いていきます。

## はじめに
そもそもAngular Universalとは何かに関してはQuramyさんのこちらの[Angular2のServer Side Renderingに触れてみる](http://qiita.com/Quramy/items/8389e14fa480f9edeaf7)にお任せしたいと思います。
タイトルの通りAngularでServer Side Rendering(SSR)を行うためのモジュール群です。

Angular Universalのモジュール自体はまだRC1の段階で、
npmに公開されているものはAngular 2自体のバージョンアップ(2.4)に追従できていない状態です。
正式版のリリースはAngular 4と同じく3月の予定らしいのでもう少しだけ待ちましょう。

それでも私のようにとりあえず試してみたい方は[Angular 2 Universal Starter](github.com/angular/universal-starter)をcloneして動かしてみてください。
どんなものか分かると思います。
今回作ったサービスもAngular 2 Universal Starterをもとにして作っていきました。

次から現状のAngular Universalでのポイントを書いていきます。
**ポイントに関して間違いがあればコメントをお願いします。**

## ポイント1: SSR失敗はエラーにならない
「SSRしている割に遅いな、Angularが重たいからだろうか」と思ったらSSRできていなかった、ということがありました。

原因はNode側のMainModuleとなる`node.module.ts`内でtypoしていたことでした。
SSRは途中で失敗したものの、express自体はクラッシュせずにサーバとして生きていたため、クライアント側でレンダリングしていた、ということです。

### :mag_right: Note
今回は関数の返り値の型を書かずにいたためにコンパイルエラーをすり抜けた、
しかも`tslint`も常にwatchで動かしていれば気づけた問題でした。

ただ逆を言えばLintだけでなく、TypeScriptのように型があればエラーも減らせる、
ということなので皆さんTypeScriptを書きましょう。

## ポイント2.1: ブラウザとNodeの差異
Universalなコードを書く際に気をつけたいのがブラウザとNodeの差異です。
特にそれぞれにしか存在しないオブジェクトを触りたい、ということが往々にしてあると思います。
そしてできれば可能な限り(`Component`は)綺麗に書きたい、と思うはずです。

今回開発していた際には、`Notification`や`SessionStorage`を使いたいという状況でした。


### :mag_right: Note
おそらくこういう場合Angularとしてはある程度`Component`を綺麗にしつつ`Service`を作るのが正しいだろう、と思い実装しました。
こうすることで`Component`はブラウザとNodeの差異を気にする必要がなくなりました。
ただ結局`Service`でブラウザとNodeの差異を気にする必要があるということです。

ここで差異をどうするかに関しては結果は一緒ですが2通りあると思っています。


####  Angular Universalに依存しない形で判定
よくある条件式である `if (typeof window === 'undefined') { }` などでブラウザかNodeを判定します。
この場合は`Service`を含めて、今の段階から`NgModule`として外部公開でき、
別の個所でAngularのAPIが変わらない限り後方互換を維持できます。

コード例:

```ts
@Injectable
class NotificationService {
    constructor() {}

    requestPermission() {
        if (this.isBrowser && this.isPermitted) {
            window.Notification && Notification.requestPermission();
        }
    }

    get isBrowser(): boolean {
        return typeof window !== 'undefined';
    }

    get isPermitted(): boolean {
        return window.Notification && Notification.permission === 'granted';
    }
}
```


#### Angular Universalに依存する形で判定
Angular Universalには判定用の値が含まれているので以下のようにimportすれば良いです。

- `import {isBrowser, isNode} from 'angular2-universal/browser'`
- `import {isBrowser, isNode} from 'angular2-universal/node'`

どちらからimportするのかに関しては[Angular 2 Universal Starter](github.com/angular/universal-starter)の
`src/{browser.module.ts, node.module.ts}`を見てもらえばわかるように、ブラウザとNodeそれぞれの`MainModule`を作るのでそこでDIすることになります。
そして`Service`などではコンストラクタでDIして値を利用できます。

この場合は`NgModule`として、一部を外部公開することなくアプリケーションに閉じる場合に有効です。
(今後AngularのCoreにUniversalが含まれるようになるのであれば、こちらで記述しても外部公開はできますが後方互換はできないと思われます。)

コード例:

```ts
@Injectable
class NotificationService {
    constructor(@Inject('isBrowser') private isBrowser) {}

    requestPermission() {
        if (this.isBrowser && this.isPermitted) {
            window.Notification && Notification.requestPermission();
        }
    }
    get isPermitted(): boolean {
        return window.Notification && Notification.permission === 'granted';
    }
}
```


## ポイント2.2: Angular 2モジュールの扱い
ポイント2.1に近いですが、こちらは`npm`で提供されているモジュールを使う話です。

`npm`には`angular2-`や`ng2-`というプレフィックスで様々なモジュールが提供されています。
しかし殆どがUniversalを意識していないため、実際に使おうとすると以下のエラーに遭遇することが多々あります。
```
ReferenceError: window is not defined
```

### :mag_right: Note
これに関して、ポイント2.1にもあった`isBrowser, isNode`の値をDIし、テンプレート内や`Component`などで使い回避できる場合もあります。
しかし回避できない場合もあるので、自分で直してPRということもあるかと思います。

ただし判定し出し分けするということはSSRできないものが増えるため、初期表示が遅くなる欠点があります。
今回は気にしないことにしましたが、実際のアプリケーションでは何か対応する必要が出てくると思います。


## ポイント3: CSSの扱い
[Angular 2 Universal Starter](github.com/angular/universal-starter)では`webpack.config.ts`を見てもらえば分かるように、ブラウザ・NodeともにCSSを`raw-loader`で出力しています。
そのため読み込み時に2重でCSSが出力されてしまい、CSSフレームワークを使う場合やサービスで大きなCSSになると余計に通信量がかかってしまいます。

またAngular 4では修正されていると思いますが、`AppComponent`で`encapsulation: ViewEncapsulation.None`として
グローバルなスタイルを当てようとした場合に開発時は問題なし、しかしAOTビルドをした場合にビルドは通るものの実行時にエラーになります。

### :mag_right: Note
この問題に対しては、グローバルにしたいスタイルはWebpackの[extract-text-webpack-plugin](https://github.com/webpack-contrib/extract-text-webpack-plugin)で結合したCSSファイルにするという手段を取りました。
そしてページに紐づくCSSは別途`raw-loader`で出力するという形にしました。

実装例は以下のコミットで確認できます。
https://github.com/nana4gonta/universal-starter/commit/7fc3fdd3fdc5ee041e5535af2863c4442804a955


## ポイント4: Express側の非同期処理
今回のコードではExpress側はAngularのSSRだけでなく、MySQLや別のAPIから取得したデータを返すAPIサーバの役割もあったため非同期処理が必要でした。
Angular 4が出る前でTypeScriptは2.0系なので、非同期処理の方法としては以下が考えられました。

- Promise
- co + Generator
- (Angularの依存関係にある)RxJS

### :mag_right: Note
Express側でしか使わないため、依存を増やさないようにとRxJSで実装を進めましたが悪手でした。
MySQLからデータを引く・挿入するなどあまりRxの恩恵を得られず、逆に不慣れなため汚いコードになってしまいました。

この点に関しては、Promiseでも同様になると思われるので、`co + Generator`または、Angular 4であれば`async/await`でいい感じのコードになるのではと思います。
適材適所が大事でした。

## まとめ
自分が試した際にはAngular 2.0系 + Angular Universal RCという環境であったため

- TypeScript 2.1で追加された機能の恩恵にあやかれない
- Angular 2.2, 2.3, 2.4の恩恵にあやかれない

という二重苦と今まで上げた4つのポイントで時間を食う羽目になりましたが、なかなか楽しい開発ができました。
Angular 4の対応を行う前にソースコードが公開されると思いますが、次のためにもAngular 4に対応するだけでなくUniversalな綺麗なコードを目指したいと思います。