28
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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

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

はじめに

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

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

それでも私のようにとりあえず試してみたい方はAngular 2 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は)綺麗に書きたい、と思うはずです。

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

:mag_right: Note

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

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

Angular Universalに依存しない形で判定

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

コード例:

@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
src/{browser.module.ts, node.module.ts}を見てもらえばわかるように、ブラウザとNodeそれぞれのMainModuleを作るのでそこでDIすることになります。
そしてServiceなどではコンストラクタでDIして値を利用できます。

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

コード例:

@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ではwebpack.config.tsを見てもらえば分かるように、ブラウザ・NodeともにCSSをraw-loaderで出力しています。
そのため読み込み時に2重でCSSが出力されてしまい、CSSフレームワークを使う場合やサービスで大きなCSSになると余計に通信量がかかってしまいます。

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

:mag_right: Note

この問題に対しては、グローバルにしたいスタイルはWebpackの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な綺麗なコードを目指したいと思います。

28
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?