【Angular Universal】 domino を使ってクライアントサイドのエラーを軽減する


domino とは?

サーバーサイドレンダリング( SSR ) を行う場合、当然ですがクライアントのコードをサーバーで実行する必要があります。
クライアントの処理ではサーバーサイドでは存在しない WindowDocument といったグローバルオブジェクトを様々な場所で利用しているので、何も対策を行わないと window is not defined #830 等のエラーが発生してしまいます。

domino は、そういったオブジェクトのモックを生成してくれるライブラリです。
モックの Window 等を利用することで、クライアントサイドの処理にあまり手を加えることなく SSR を行うことが出来ます。

※ ただし、実際のプロパティとはずれている部分もあるので、domino を使ったからといってすべての処理がすんなり動くようになるわけではありません

使い方

window is not defined #830 であるように

import * as domino from 'domino';

const mockWindow = domino.createWindow(TEMPLATE);

global['window'] = mockWindow;
global['Window'] = domino.impl.Window;
global['document'] = mockWindow.document;

こんな感じで定義できます

domino を読み込む場所に注意

当初、domino はクライアントサイドでいう polyfill のようなものだと思い、深く考えずにサーバーサイドの最初の方で読み込んでいました。

しかし、そのせいで

domino

axios 依存ライブラリ

axios

という順番で実行されてしまい、中途半端な WindowDocument がグローバルに存在する環境下でサーバーサイドの処理が行われていました。
そしてたまたま、そういったオブジェクトで処理が分岐する依存ライブラリ ( axios ) にぶつかり、エラーが発生してしまいました。

よく考えれば当然ですが、domino の読み込みはなるべくレンダリング処理の直前で行うほうが良いと思います。
今回は axios でしたが、同様の処理を行っているライブラリは他にもあるのではないでしょうか…?

補足 axios のエラーについて

pathname: (urlParsingNode.pathname.charAt(0) === '/') ?
utils.isStandardBrowserEnv() ?
function isStandardBrowserEnv() {
  if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
    return false;
  }
  return (
    typeof window !== 'undefined' &&
    typeof document !== 'undefined'
  );
}

axios ではクライアントサイドのグローバルオブジェクトを確認して、現在の環境(サーバー or クライアント)を判断し、処理を分けているようである

今回は中途半端な Window 及び Document が存在していたため、クライアントサイドの処理が実行されエラーとなっていた

まとめ

最初にも述べましたが domino で生成できるグローバルオブジェクトは完全なものではありません。
しかし、基本的なプロパティは大体実装されているので、これを読み込むだけクライアントサイドの処理をあまり修正せずに SSR で利用することができます。

プロジェクトで使用しているライブラリによっては別途対応が必要なものもありますが、私のケースでは おおよそ流用できて便利でした ( ^ω^)

ひとり言

これからちょくちょく angular universal 系の tips を angular-universal タグにまとめていこうかなと思います。
(´-`).。oO(ある程度溜まったらチュートリアル的な記事を作ってまとめられたら良いなぁ)