フューチャー技術ブログ

Vue.js連載始めます & Nuxtの通信パターンも見てみる

Vue.js連載企画を始めます。今年は勤労感謝の日が土曜日で、勤労に感謝できなくて残念でしたね。

Date Name Title
11/25(月) 渋川よしき Nuxtの通信パターンも見てみる(この記事)
11/26(火) 村田靖拓さん 2015年頃のフロントエンジニアだってvoid(0)のワクワクを理解したい
11/27(水) 大岩潤矢さん Vue3・Nuxt3アプリをPWA化する方法 2024年版
11/28(木) 永井優斗さん Vue Fes Japan 2024報告
11/29(金) 山本竜玄さん Deno × Vueを触ってみた(2024年冬)

Nuxtの通信パターンも見てみる

フロントエンドフレームワークからサーバーにアクセスするパターンという記事を書いたところ、ちょっとバズったので、そういえば調べきれてなかったNuxtの話でも書こうかな、と思って調べてみた記事です。

Nuxt3の通信機能

Nuxt3のドキュメントには以下の3つの基本の通信のための機能があります。

関数 機能
$fetch() ofetchというブラウザAPIのfetch()互換のライブラリの関数
useAsyncData() データ取得のライフサイクル管理やキャッシュ管理をするラッパー
useFetch() $fetch() + useAsyncData()

現在サポートされているNode.js18以降はみなオプションをつかわずにfetch()が使えるのに、なぜ$fetch()なんてものを別に用意しているかというと、単なるfetch()ではなく、ヘッダーを付与したりプリセットが設定できるようなcreate()メソッドが使えたり、拡張されていたりします。何もしなくても、サーバー側で実行すると、ホスト名を省略できるなども設定されています。

https://nuxt.com/docs/guide/recipes/custom-usefetch

useAsyncData()そのものには通信機能はなく、中のコールバックで$fetch()を呼びます。ただし、通信をキャッシュして2度目以降は呼ばないようにしたり、通信中かどうかのフラグだったり、通信結果のデータだったりを返してくれます。単なるasyncな情報取得関数をこの中で呼ぶだけで表示管理やキャッシュが行えてしまうという優れものです。OpenAPIで作ったクライアントをラップしたりといった使い方もできるでしょう。

<script setup lang="ts">
const { data, status, error, refresh, clear } = await useAsyncData(
'mountains', // キャッシュのキー
() => $fetch('https://api.nuxtjs.dev/mountains')
)
</script>

useFetch()は、上の2つをまとめて呼ぶヘルパー関数です。コードが短くなります。

<script setup lang="ts">
const { data, status, error, refresh, clear } = await useFetch(
'https://api.nuxtjs.dev/mountains'
)
</script>

これ以外に、useAsyncData()と、useLazyFetch()という派生の関数もあります。

NuxtのuseFetch()/useAsyncData()のライフサイクル

$fetch単体では、通常のfetch()とだいたい同じなので特別なことはないのですが、他の2つは末尾にオブジェクト型でオプションを追加すると、動作が大きく変わります。例えば、server: falseをつけると、サーバーサイドレンダリング時においても、サーバーからリクエストを行わず、ブラウザの表示後にリクエストされるようになります。

const { data, status, error, refresh, clear } = await useFetch(
'https://api.nuxtjs.dev/mountains',
{ server: false } // これを追加
)

オプションはいろいろあります。

  • server:サーバー上のデータを取得するかどうか(デフォルトはtrue
  • lazy:クライアント側のナビゲーションをブロックする代わりに、ルートをロードした後に非同期関数を解決するかどうか(デフォルトはfalse)、trueにすると、動作がuseLazyFetch()useLazyAsyncData()と同じになる
  • immediatefalseに設定すると、リクエストがすぐに起動できなくなります。(デフォルトはtrue
  • dedupe: すでに保留中のサーバー呼び出しがあった場合の動作
    • 'cancel': 呼び出し中の古いリクエストをキャンセルする(こちらがデフォルト)
    • 'defer': 新しい呼び出しの方をキャンセルする
  • watch: 何かしらのリアクティブを設定すると、それが変更されたときに再リクエストを行う
  • default: デフォルト値を返す

デフォルトではサーバー側のレンダリング時にリクエストを行います。server: falseにすると、ブラウザからリクエストが飛ぶようになります。あるいは、そのコードが書かれたコンポーネントがClientOnlyコンポーネントでラップされた中に置かれていた場合は、それはすべてクライアント側でおこなれわれています。

defaultのデフォルト値設定と、lazy: trueもしくは、Lazyがつく方のメソッドを使う、immediate: falseを組み合わせると、まず初期値を返し、後から結果を返すことができます。これがserver: falseであれば前のエントリーのStale-While-Revalidateのようになりますし、server: trueであればサーバーコンポーネントのような動きになります。

初期値はdefaultを設定すれば最初から返せますし、useFetch()useAsyncData()が返すstatusを見ることで、未ロードかどうかをハンドリングできます。

これらのオプションをうまく使い分けると多くのパターンが実現できることがわかります。

少し問題

ただ、色々試していて、サーバーアクセスする2つのコンポーネントがあった場合に、それぞれの通信でブロックしているような動作をしていました。わかりやすくするためにサーバーAPI側で1秒間のウェイトをかけていたのですが、コンポーネントが2つあると初期表示が2秒になりました。<Suspense>とか使ってもカバーできず、直列に待っているようでした。検索してみたら・・・このissueですかね。

Next.jsで同じようなプログラムを作ってみたところ、2つのコンポーネントが並列でリクエストを送っても(URLなどは変えてキャッシュされないようにして)、1秒ですみました。このあたりの並列処理とかはまだNext.jsに一日の長がありますね。

通信するコンポーネントが複数あると遅くなるので、一箇所で行なってPromise.all()で並列で待つとかをすれば良いとは思いますが・・・・

あと、experimentalなサーバーコンポーネントを試してみたけども、どうもクライアント側で動いているような感じでした。

まとめ

Nuxtの通信周りのAPIをさらっとみて実験等をしてみました。これ1つで、さまざまなパターンに対応できる機能で、これはReactとかでも欲しいな、とちょっと思いました。

なお、最初、間違ってページ遷移時のリンクを<NuxtLink to="遷移先">ではなく、<a href="遷移先">と書いてしまい、ページ遷移後のレンダリングも全てがサーバーサイドレンダリングになってしまい、「サーバーコンポーネントと言っているReactよりもかなり前衛的ですごいじゃん!!!!」と勘違いしてしまい、方針変更してそちらについて書こうと思ってNuxt2との動作比較とかも調べたりしたのですが、<NuxtLink>の存在に気づいて書き換えたら、Next.js 12以前と同じような動きになって、原稿を全消ししたりしました。