サーバーサイドレンダリングできないモジュールの扱い方


はじめに

SvelteNext.jsSapper というフレームワークで画面を作っていたのですが、そこで Handsontable を使おうとして詰まったので対応方法をメモ書きとして残しておきます。

Sapper に限らず、Next.js、Nuxt.js など最近のフレームワークでは、デフォルトでサーバーサイドレンダリングが有効になっています。ところが、そこで使いたいモジュールの多くは、window や document などブラウザ特有のオブジェクトに依存しているため、そのまま import してサーバーサイドレンダリングを行うと実行時エラーで落ちてしまいます。

無理矢理に window や document を定義して回避することも考えられますが、その手の処理はブラウザの種類を判定することが目的だったりする場合も多く、サーバーサイドで動けばいいという発想には難があります。

ここでは、Sapper での対応方法を説明しますが、手法自体はサーバーサイドレンダリングを行う場合に共通した考え方ではないかと思います。

対応方針

ブラウザ特有のオブジェクトに依存するモジュールは、サーバーサイドレンダリング完了後に動的にロードして表示すればいい。

具体的な対処法

コンポーネントの初期化時に動的 import 関数を使い、モジュールを動的ロードします。ここでは Svelte の oncreate でロードしていますが、React であれば componentDidMount で同じことができます。

注意が必要なのは、oncreate を async にする場合には、onupdate や ondestroy も async にしなければならないという点です。合わせないとインスタンス初期化前に後続の onupdate が呼び出され this.instance が undefined の状態で実行されてしまいます。

2018/9/1 追記 当初うまくいっていたように見えたのですが、どうもうまく動作していないので、現時点では下記は間違いかもしれません。地道に Promise を重ねるしかないのか……

<div ref:elem></div>

<script>
export default {
  async oncreate() {
    const Handsontable = await import('hansontable');
    this.instance = new Handsontable(this.refs.elem, this.get());
  },
  async onupdate({changed, current, previous}) {
    this.instance.updateSettings(current);
  },
  async ondestroy() {
    this.instance.destroy();
  }
}
</script>

謝辞

実のところ、この記事はAndrew Smith さんのブログのアイデアをまるパクリして書かれています。externals も webpack.IgnorePlugin もうまくいかず途方にくれていたので、非常に助かりました。