NuxtのasyncDataで静的ファイルを読み込む

#概要

Nuxt.jsを使って色々作っていると、generate(静的出力)時の初期表示内容にJSONファイルの内容を取り込んで表示したい、という場面に出くわします(多分)。
そんなときにどうやって取り込めばいいのかという方法を書き残しておきます。

#方法

asyncDataメソッドを使います。
また、ページが動的ルーティングの場合はroutesオプションも使う必要がでてきます。

これら2つで要件は満たせるのですが、その中でどうやって読み込まれるかによって出力されるJS(ランタイム=初期表示以降に読み込まれる内容)が変わってくるので使い分ける必要があります。

#サンプルケース

説明/理解しやすいように、以下のサンプルケースを用いて説明をしていきます。

  • Nuxt.jsを使ってブログシステムを作成する
  • 記事はMarkdownで書き、それらを変換した個別のJSONファイルがあるものとする
  • 初期表示時に記事の内容をHTMLに含めておきたい
    • 余計なランタイムコストをかけずにイニシャルビューを早く出したい

#コンテンツがひとつのJSファイルとなるパターン

asyncDataメソッドの中でrequireを使いJSONファイルを読み込むと、generate時にそれらは一つのJSファイルとして出力されます。

サンプルケースの場合、例えば以下のような記事ページコンポーネントがあったとします。

<!-- /pages/posts/_name.vue -->
<script>
export default {
  asyncData({ params }) {
    return {
      post: require(`~/posts/${params.name}.json`)
    }
  }
}
</script>

<template>
  <article class="content" v-html="post"></article>
</template>

そして以下のような記事のJSONファイルを用意します。

// /posts/awesome-cooking.json
{
  "name": "awesome-cooking",
  "content": "<p>とても素晴らしい料理の説明</p>"
}
// /posts/how-to-master-js.json
{
  "name": "how-to-master-js",
  "content": "<h1>JSをマスターするには</h1>"
}

そしてnuxt.config.jsgenerate.routesに、["/posts/awesome-cooking", "/posts/how-to-master-js"]を渡してそれぞれのJSONを読み込ませるようにします。

この状態でNuxtのgenerate機能を用いて静的ファイル出力を行うと、以下のファイルが出力されます。

  • awesome-cooking.jsonの内容が含まれたawesome-cooking.html
  • how-to-master-js.jsonの内容が含まれたhow-to-master-js.html
  • awesome-cooking.jsonhow-to-master-js.json両方の内容が含まれたJSファイル

肝心なのはこの、「両方の内容が含まれた」という点です。
仮に/posts/awesome-cookingにだけアクセスしたとしても、how-to-master-js.jsonのデータまでダウンロードすることになります。

これがもし、記事数が1000あった場合、JSONファイル1000個分の内容が詰まったJSファイルが記事ページ表示時に読み込まれることになり、かなりの非効率となります。

ただ、JSONファイルの総サイズが小さい場合、初期ロードが少し遅くなる代わりにページ遷移時(routerでの遷移時)の読み込み時間がなくなる、というメリットにもなりえます。

この方式は、記事数が少ない場合や初期ロードよりも遷移時の待ち時間を重視する場合、また利用するユーザあたりの訪問ページ数が高い場合等に使うべきでしょう。

#コンテンツごとにJSファイルが分割されるパターン

上のパターンとは逆に、それぞれのコンテンツ(JSONファイルの内容)を別々のJSファイルに分けて、そのページに必要なコンテンツだけをダウンロードするパターンです。
この場合は、asyncDataメソッド内でdynamic importを用いてJSONファイルを読み込みます。

<script>
export default {
  async asyncData({ params }) {
    const post = await import(`~/posts/${params.name}.json`)

    return { post }
  }
}
</script>

<template>
  <article class="content" v-html="post"></article>
</template>

このコンポーネントを使って上のパターンと同じように静的ファイルを生成すると、以下のファイルが出力されます。

  • awesome-cooking.jsonの内容が含まれたawesome-cooking.html
  • how-to-master-js.jsonの内容が含まれたhow-to-master-js.html
  • awesome-cooking.jsonの内容が含まれたJSファイル
  • how-to-master-js.jsonの内容が含まれたJSファイル

上のパターンとは異なり、1JSON -> 1JSでファイルが出力されます。

この方式を取るメリットは、やはり「必要なデータだけダウンロードする」ことでしょう。
/posts/awesome-cookingにアクセスした際にはawesome-cooking.jsonの内容だけが、/posts/how-to-master-jsにアクセスした場合はhow-to-master-jsの内容だけがダウンロードされるため、通信量が必要最低限になります。

一方でデメリットとして、ルート遷移時に通信が発生してしまいます。

この方式は、記事数がそこそこある場合や、利用するユーザあたりの訪問ページ数が少ない(全ページの30%に満たない等)場合に使うべきでしょう。ブログなんかはこちらを使ったほうがいいですね。

#まとめ

ざっくりとまとめると、

  • データ量が少ない場合はrequireを使ってJSONを読み込む
  • データ量が多い場合はdynamic importを使ってJSONを読み込む

それぞれの方式にはメリット/デメリットがあるので、要件に合わせて使い分けていきましょう。