【Nuxt】ビルド時に静的API化して、プリレンダリングする方法

どーも、ぐるたか@guru_takaです。

ヘッドレスCMSなどからコンテンツをAPIで取得する場合、静的ジェネレーターでプリレンダリングしたとしても、クライアント側でヘッドレスCMSにアクセスします。

プリレンダリングした私のポートフォリオでネットワークを確認してみると、以下GIFのようにページ遷移すると、ヘッドレスCMSにAPIを叩きにいっているんですよね。

Reactの静的ジェネレーター「Gatsby.js」であれば、良しなにやってくれるようですが、Nuxtの場合、2020/3/16時点では、自分で静的API化する必要があります

ここでは、Nuxtで静的API化する方法をハマりどころも含め、紹介していきます。

全体の処理の流れ

STEP.1
ビルド前にAPIで取得したデータをJSON化してアセットに出力
STEP.2
プリフェッチに設定
STEP.3
axiosでJSONデータにアクセスして取得
STEP.4
プリレンダリング中もJSONデータを取得できるようにする
STEP.5
axiosのbase urlをプリレンダリング中とデプロイ先で分ける

STEP.1:ビルド前にAPIで取得したデータをJSON化してアセットに出力

まずはコードから紹介!ここでは、ヘッドレスCMS「contentful」をサンプルとして使っています。

nuxt.config.js〜/modules/api.jsを編集、作成します。

~/modules/api.js
module.exports = function apiModule(moduleOptions) {
  this.nuxt.hook('build:before', async ({ app }) => {
    // contentful 初期化
    const contentful = require('contentful')

    const client = contentful.createClient({
      space: process.env.CTF_SPACE_ID,
      accessToken: process.env.CTF_PREVIEW_ACCESS_TOKEN,
    })

    // contentfulからデータ取得
    let datas = await client.getEntries({
        content_type: process.env.CTF_TUTORIAL_POST_TYPE_ID,
        order: 'fields.order'
    })

    // JSON生成
    this.options.build.plugins.push({
      apply(compiler) {
        compiler.plugin('emit', (compilation, cb) => {
          compilation.assets[`api/datas.json`] = {
            source: () =>
              JSON.stringify({ apiDatas: datas.items.map((item) => item.fields) }),
            size: () => {}
          }
          cb()
        })
      }
    })

  })
}
nuxt.config.js
  modules: ['~/modules/api'],

すると、ビルド前に~~~~~/_nuxt/api/datas.jsonというJSONデータが自動で生成されます。

$npm run devで開発環境を立ち上げてみると、http://localhost:3000/_nuxt/api/datas.jsonにJSONデータが表示されているはずです!

参考 モジュール - NuxtJS

STEP.2:プリフェッチに設定

続いて、JSONデータをプリフェッチに設定します。プリフェッチとは、リソース先読みのための仕組み「Resource Hints」の1つの手段で、静的コンテンツを事前に取得する技術になります。

超ざっくりいえば、プレフェッチを使うと、JSONデータを描画前に先読みするので、爆速になるわけです。

詳細は以下2つの記事が参考になるので、ぜひチェックしてみて下さい。

参考 ロードを高速化するprefetch - Qiita 参考 link要素によるResource Hintsを使用してリソースの先読みを行う

先程のコーディングに追記すると、こんな感じ!

~/modules/api.js
module.exports = function apiModule(moduleOptions) {
  this.nuxt.hook('build:before', async ({ app }) => {
    // contentful 初期化
    const contentful = require('contentful')

    const client = contentful.createClient({
      space: process.env.CTF_SPACE_ID,
      accessToken: process.env.CTF_PREVIEW_ACCESS_TOKEN,
    })

    // contentfulからデータ取得
    let datas = await client.getEntries({
        content_type: process.env.SAMPLE,
        order: 'fields.order'
    })

    // JSON生成
    this.options.build.plugins.push({
      apply(compiler) {
        compiler.plugin('emit', (compilation, cb) => {
          compilation.assets[`api/datas.json`] = {
            source: () =>
              JSON.stringify({ apiDatas: datas.items.map((item) => item.fields) }),
            size: () => {}
          }
          cb()
        })
      }
    })

   ///////////
    //追記
    // link rel="prefetch"にJSONを追加
    ///////////

    this.options.head.link = [
      ...this.options.head.link,
      {
        rel: 'prefetch',
        href: `${this.options.build.publicPath}api/datas.json`,
        as: 'fetch'
      }
    ]
  })
}

STEP3:axiosでJSONデータにアクセスして取得

続いて、生成されたJSONデータを開発環境で取得してみます。

まずはaxiosをインストールします。

$ npm install @nuxtjs/axios
nuxt.config.js
module.exports = {
  modules: [
    '@nuxtjs/axios',
    '~/modules/api'
  ],
}

後は以下のように、asyncDataでJSONデータを取得するだけです!

~/pages/index.vue
~~~~~~~~

  async asyncData({ app, env, route }) {
    const { data } = await app.$axios.get(`/_nuxt/api/datas.json`)

    return {
      sample:data
    }
  }

~~~~~~~~

STEP4:プリレンダリング中にJSONデータから情報を取得できるようにする

最後に、プリレンダリングして、JSONデータを取得して動的にページ生成しなくていはいけません。

まずはコードから紹介します。

~/modules/api.js
module.exports = function apiModule(moduleOptions) {
  this.nuxt.hook('build:before', async ({ app }) => {
    // contentful 初期化
    const contentful = require('contentful')

    const client = contentful.createClient({
      space: process.env.CTF_SPACE_ID,
      accessToken: process.env.CTF_PREVIEW_ACCESS_TOKEN,
    })

    // contentfulからデータ取得
    let datas = await client.getEntries({
        content_type: process.env.SAMPLE,
        order: 'fields.order'
    })

    // JSON生成
    this.options.build.plugins.push({
      apply(compiler) {
        compiler.plugin('emit', (compilation, cb) => {
          compilation.assets[`api/datas.json`] = {
            source: () =>
              JSON.stringify({ apiDatas: datas.items.map((item) => item.fields) }),
            size: () => {}
          }
          cb()
        })
      }
    })


    // link rel="prefetch"にJSONを追加

    this.options.head.link = [
      ...this.options.head.link,
      {
        rel: 'prefetch',
        href: `${this.options.build.publicPath}api/datas.json`,
        as: 'fetch'
      }
    ]

   ///////////
    //追記
    ///////////

    // dev時はここで終了
    if (this.options.dev) return

    // generate時にexpress立てて、json取得できるようにする
    this.nuxt.hook('build:done', (generator) => {
      console.log('**[generate]** opening server connection')
      const express = require('express')
      const app = express()

      //dist内でサーバーたてる感じ
      app.use(express.static(this.options.generate.dir))

      const server = app.listen(process.env.PORT || 3000)

      this.nuxt.hook('generate:done', () => {
        console.log('**[generate]** closing server connection')
        server.close()
      })
    })
  })
}

STEP3でaxiosを使ったように、JSONデータを取得するには、”http://localhost:3000/_nuxt/api/datas.json”にアクセスしなくていはいけません。

しかし今のままでは、プリレンダリング中にローカルサーバーを立ち上げていないので、axiosでアクセスできず、エラーが出ます。

なので、expressを使ってローカルサーバーを立ち上げ、プリレンダリング中もデータ取得できるようにしています!

ここまでのコードは以下の記事を参考にしています!
参考 Firebase、Flamelink、Nuxt、Netlify、PWAを使ってJAMstackなブログを作る - Qiita

STEP5:axiosのbase urlをプリレンダリング中とデプロイ先で分ける

これで一安心かと思いきや、最後に1つやらなくはいけません。

それは、axisoのbase urlを本番環境用のURLでも使えるようにすることです。

今のままでは、どこかにデプロイした後も、クライアント側でhttp://localhost:3000/_nuxt/api/datas.jsonにアクセスしてしまい、エラーが生じます。

なので

  • プリレンダリング中:localhost:3000
  • 本番環境:https://~~~~~~~~.com

に分けれるようにします。

設定方法は以下のとおりです。

~/plugins/axios.js
// https://qiita.com/penton310/items/4312ddbe015476fd67ff
// nuxt.config.jsでprocess.static && process.serverが使えないので、
export default function({ $axios }) {
  $axios.setBaseURL(
    (process.static && process.server) || process.env.NODE_ENV !== 'production'
      ? 'http://localhost:3000'
      : `https://~~~~~~~~~~~.com`
  )
}

nuxt.config.js
  plugins: [
    '~/plugins/axios.js',
  ],

npm run generatenpm run devのときは、localhostにアクセス、本番環境では設定したURLがaxisoのbase urlになるようなプラグインを作ればOKです。

これで静的API化が完了!!!

参考 プラグイン - NuxtJS

参考リンク

参考 Firebase、Flamelink、Nuxt、Netlify、PWAを使ってJAMstackなブログを作る - Qiita 参考 Nuxtでビルド時にAPIを静的化して、完全にサーバーへのリクエストをなくすト - Qiita 参考 【FULL STATIC】 Nuxt generate<4つのアプローチまとめ> - Qiita

4 COMMENTS

u1

とても、参考になりました。ありがとうございます。blogなどを構築する際に、post以外にも、カテゴリーや固定ページなどのAPIを取ってくる必要があるかと思うのですが、2つ以上のAPIをビルド時に静的化するには、どう書き換えたら良いのでしょうか?教えた頂けましたら助かります。

ぐるたか

コメント、ありがとうございます。

以下のように複数のデータを取得→JSON生成→rel="prefetch"に追加すれば動くと思います!

  // contentfulからデータ取得
    let datas1 = await client.getEntries({
        content_type: process.env.SAMPLE,
        order: 'fields.order'
    }

   let datas2 = await client.getEntries({
        content_type: process.env.SAMPLE2,
        order: 'fields.order'
    })

    // JSON生成
    this.options.build.plugins.push({
      apply(compiler) {
        compiler.plugin('emit', (compilation, cb) => {
          compilation.assets[`api/datas1.json`] = {
            source: () =>
              JSON.stringify({ apiDatas: datas1.items.map((item) => item.fields) }),
            size: () => {}
          }
          compilation.assets[`api/datas2.json`] = {
            source: () =>
              JSON.stringify({ apiDatas: datas2.items.map((item) => item.fields) }),
            size: () => {}
          }
          cb()
        })
      }
    })

   // link rel="prefetch"にJSONを追加

    this.options.head.link = [
      ...this.options.head.link,
      {
        rel: 'prefetch',
        href: `${this.options.build.publicPath}api/datas1.json`,
        as: 'fetch'
      },
      {
        rel: 'prefetch',
        href: `${this.options.build.publicPath}api/datas2.json`,
        as: 'fetch'
      }
    ]

コメントを残す