毎回叩くのはイヤだ

前の記事のコードでは結局APIリクエストが発生していました。いろいろ調べてみると、どうやらuseAsyncDataでの問題だと固執してしまっていて、そこでなんとかしてしまおうと思ってたことに原因がありました。

目標としては、

  • 記事一覧でデータを引っ張ってきてるのに個別記事でAPIを毎回叩きたくない
  • 各記事のpayloadはそのページだけのデータにしたい

ですので、上記の目標を達成すべく0ベースで改めて着手。

記事一覧のデータの取得

Prerenderingの時になんとかしないといけないわけですが、何度も書いているとおりNuxt2の手法は使えません。というわけで、以下のようなプラグインを書きました。型のところは適宜読み替えてください。

import { Contents } from 'newt-client-js'
import { Article } from '~/types/contents'

interface CachedData {
  [key: string]: Contents<Article> | Article
}

const cachedData: CachedData = {}

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('app:created', async (vueApp) => {
    if (!cachedData['foo-bar']) {
      cachedData['foo-bar'] = await vueApp.$nuxt.$newtClient.getContents({
        appUid: 'foo',
        modelUid: 'bar',
      })
      cachedData['foo-bar'].items.forEach((article) => {
        cachedData[article.slug] = article
      })
    }
    const slug = vueApp.$nuxt.ssrContext?.url.split('/').slice(-1)[0]
    if (cachedData[slug]) {
      vueApp.$nuxt.payload.data[slug] = cachedData[slug]
    }
  })
})

当初は、ビルド時ということでnuxt.config.tsに nitro:build:before でイケるかと思いましたが、他のプラグインも使いたいんで NuxtApp が使えないのはよろしくない。[1] 結局他にgenerate時のhookがないのでクロール時に処理するために app:created を使用。ちなみに app:rendered では後述の理由でNGです。

さて、cacheData.server.tsの名前の通りclientでは作動しません。ということでSSGにおいてはビルド時のみでの処理となります。まずは記事一覧を1度だけ取得して cachedData に格納し、次に個別記事に振り分けて同じく一旦格納します。そしてurlからslugを抽出してそのkey名で個別記事のデータをpayloadに放り込んでおけば、各フォルダに個別記事のpayload.jsを出力してくれます。

この書き方はsetup内部での処理ではありませんのでuseFetch等の各Composableが使えないことに注意です。newt-client-jsは内部でAxiosを使っているので可能な方法でもあります。

Payloadの利用

今回は前作った useArticle で処理します。直接 [slug].vue に書いてもOK。

export const useArticle = async (slug: string) => {
  const nuxtApp = useNuxtApp()
  if (process.server && nuxtApp.payload.data[slug]) {
    return nuxtApp.payload.data[slug]
  }
  const { data: article } = await useAsyncData(
    slug,
    async () => {
      return await nuxtApp.$newtClient.getFirstContent({
        appUid: 'foo',
        modelUid: 'bar',
        query: {
          slug,
        },
      })
    },
  )
  return article.value
}

if (process.server && nuxtApp.payload.data[slug]) ~ の下りを追加しました。無くても useAsyncData で拾ってくれるんじゃないかと思ったんですが、そううまくはいかなかったです。というわけでビルド時はpayloadを直接返すようにします。生成後は useAsyncData できちんとpayloadを使ってくれます。というか、再Fetchする予定がなければもはや useAsyncData の箇所は必要ないですね。

ちなみに前述のプラグインを app:rendered で処理すると、returnすべきpayloadの格納が間に合わないです。

まとめ

各ドキュメントやissueを見てみましたが、ズバリこれだ!という解決策が見当たらなかったので今回のようないささか強引な回避策となりました。とはいえ、とりあえず各動作に問題はないようですし目標も達成できましたのでこれでヨシとします。

今後のアップデートでまた良い方法が見つかるかもしれません。

また、他にもっとスマートな方法がありましたら教えていただきたいです。

ではでは。


  1. 実際のコードでは記事本文(body)を整形するために、この中で別のプラグインを呼んでます。 ↩︎