💡

Nuxt3のData Fetchingを検証する

2023/10/04に公開

1. はじめに

弊社では一部ソフトウェアの開発に現在Nuxt3を使用しています。Nuxt3は、Vue.jsにServer Side Rendering(SSR)機能やその他色々を含めたフレームワークです。

Nuxtは非常に便利なフレームワークではあるのですが、SSRにおいては、レンダリングにまつわる全ての計算がサーバサイドまたはクライアントサイドのどちらか一方だけで実行されるわけではなく、ある処理はサーバサイドで実行され、別のある処理はクライアントサイドで実行され、……とかなり複雑です。

特に、ユーザデータや帳票データなどを別のサーバから取得するData Fetchingには注意が必要です。適切な実装を怠ると、クライアントとNuxt3のサーバでフェッチを二回リクエストしてしまうという問題があります。

下記の公式ドキュメントによると、$fetchではなくuseAsyncDatauseFetchを利用すればData fetchingはNuxt3のサーバサイドのみで実行され、クライアントにはNuxt3のサーバからデータが渡されるらしいのですが、この記事では、それが本当かどうかを調べた結果をまとめました。

https://nuxt.com/docs/getting-started/data-fetching

対象読者

  • Nuxt3の基礎はご存知の方
  • Nuxt3のSSRやData Fetchingの挙動の理解を深めたい方(と言ってもこの記事で説明するのは全容のごく一部分ですが。。。)

TL;DR

  • 公式の説明の通りでおおよそは間違っていないが、以下に注意
  • ボタン押下時の@clickなど、クライアントサイドでのみ実行されるスクリプト内のuseAsyncDatauseFetchではクライアントから直接データフェッチされる。
  • useFetchはURLの一致でキャッシュを区別するため、クライアントとサーバでURLが変わるような特殊なケースではクライアントとサーバ双方からリクエストが送信される。かつ、クライアントへのレスポンスは何故かnullになる。
  • serverオプションをfalseにすると、レスポンスはクライアント・サーバともにnullになる。

2. 検証の方法

  1. データのフェッチ先のダミーとして、/(path)をGETしたらHello, (path)!を返すAPIサーバを立てます。
  2. Nuxt3側では、process.clientの値やフェッチの関数に基づいて、フェッチするURLのパスを書き換えます。

例えばuseFetch("http://localhost:8000/useFetch on " + (process.client ? "client" : "server"))をサーバサイドで実行するとHello, useFetch on server!(を色々ラップしたオブジェクト)が返却されます。

APIサーバへのリクエストのパスや、返却されるメッセージの内容によって、Nuxt3サーバとクライアントのどちらからリクエストされたのかが区別できます。Data Fetchingの検証と言いつつ、一行のメッセージを返す機能しかありませんが、リクエストの送信元が区別できれば良いのでダミーとしては十分でしょう。

3. データフェッチ用のAPIサーバの実装

Node.jsでサクッと作ります。

api_server.js
const express = require("express");
const cors = require("cors");

const portNumber = 8000;

const app = express();
app.disable('etag'); // disable 304                                                                                                                                                                                   
app.use(cors());

app.use((req, res, next) => {
    console.log(req.url);
    next();
});

app.get("/:name", (req, res) => {
    res.status(200).send(`Hello, ${req.params.name}!`);
});

app.listen(portNumber);

console.log(`PortNumber is ${portNumber}`);
package.json
{
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.18.2"
  }
}

ライブラリのバージョンはお好みで大丈夫です。こちらを実行すると以下のようになります(Node.js自体のインストールに関しては説明省略)。

$ node api_server.js
PortNumber is 8000

# 別のコンソールから`curl http://localhost:8000/world`を実行するとリクエストのパスが出力される。
/world

4. Nuxt3プロジェクトの実装

npx nuxi initコマンドで、Nuxt3のプロジェクトを新規作成します(npx自体のインストールについては省略)。

$ npx nuxi init fetch_test   

✔ Which package manager would you like to use?
yarn
◐ Installing dependencies...                                                                                                                                                                                22:24:04
yarn install v1.22.19
warning ../package.json: No license field
info No lockfile found.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
warning vscode-languageclient@7.0.0: The engine "vscode" appears to be invalid.

(以下省略)

fetch_test配下に生成されたapp.vueを以下のように書き換えます。

app.vue
<template>
  <div>
    <!-- 押下するとフェッチ関数を実行するボタンを定義 -->
    <button @click="run_useAsyncData">useAsyncData</button>
    <!-- 他のフェッチ関数を実行するボタン(後述)を列挙 -->
  </div>
</template>

<script setup>

const url = (func_name) => {
  return `http://localhost:8000/${func_name} on ${process.client ? "client" : "server"}`;
}

const run_useAsyncData = async () => {
  console.log("run_useAsyncData");
  const { data } = await useAsyncData(
    'run_useAsyncData',
    () => {
      return $fetch(url("run_useAsyncData"));
    }
  );
  console.log(`Response : ${data.value}\n`);
};

// 他のフェッチ関数の定義(後述)を列挙


// ページのレンダリング時にデータをフェッチする。
await run_useAsyncData();
// 他のフェッチ関数の実行を列挙

</script>

<script setup>タグでは、まず前述の通りprocess.clientに基づいてパスを変えてURLを構築するurl関数を定義します。

次に、(ずらずら列挙すると見にくいので、まずは)useAsyncData関数でデータをフェッチするrun_useAsyncData関数を定義します。useAsyncData関数の第一引数は、実行結果をキャッシュするためのキーとのことです。第二引数は実際にデータをフェッチするための関数です。第二引数の中では$fetchを利用してデータをフェッチします。

セットアップの最後では、レンダリングの初期化処理としてawait run_useAsyncData()を実行します。

5. 検証結果

useAsyncData

それでは、fetch_testプロジェクトを実行します。

$ yarn dev
yarn run v1.22.19
$ nuxt dev
Nuxt 3.7.3 with Nitro 2.6.3                                                                                                                                                                                 22:31:51
                                                                                                                                                                                                            22:31:52
  ➜ Local:    http://localhost:3000/
  ➜ Network:  use --host to expose

✔ Nuxt DevTools enabled (v0.8.5), press Shift + Option + D in app to open  (experimental)                                                                                                                  22:32:02
ℹ Vite client warmed up in 599ms                                                                                                                                                                           22:32:05
✔ Nitro built in 347 ms 

Nuxtから与えられたURL(ここではhttp://localhost:3000/)をブラウザで開きます。

公式ドキュメントによれば、useAsyncDataはサーバサイドで実行されてクライアントにデータを転送するとのことですが、その通りの挙動になっているかどうかを確認しましょう。

ブラウザでページを開くと、APIサーバを実行したコンソールには以下のログがプリントされているはずです。

/run_useAsyncData%20on%20server

Nuxt3サーバからリクエストが届いていますが、クライアントからはリクエストがありません。

また、fetch_testプロジェクトを実行したコンソールには以下のログがプリントされているはずです。

run_useAsyncData
Response : Hello, run_useAsyncData on server!

Nuxt3サーバからリクエストを送信したのでこれは当然です。

最後に、ブラウザの開発者ツールのコンソールを確認してみると、コンソールにも同じログがプリントされているはずです。すなわち、クライアントからAPIサーバに直接リクエストを送信したのではなく、ドキュメント通りにNuxt3のサーバサイドからクライアントサイドにデータが渡されているようです。

それでは次に、ブラウザの画面上のボタンを押下してみてください。

今度はAPIサーバ側では以下のログがプリントされます。

/run_useAsyncData%20on%20client

また、fetch_testプロジェクトのコンソールには何もプリントされていないはずです。一方、ブラウザのコンソールには以下のログがプリントされたはずです。

run_useAsyncData
Response : Hello, run_useAsyncData on client!

つまり、ボタン押下など、ブラウザでのアクションに起因した処理はブラウザでのみ実行されるようです。また、そのような処理の中でuseAsyncDataを実行すると、クライアントからAPIサーバに直接通信するようです。

useFetch

それでは別のデータフェッチ関数についても検証していきましょう。

app.vue<script setup>内に次のコードを追加してください。また、run_useAsyncDataと同様にボタンも追加してください。

const run_useFetch = async () => {
  console.log("run_useFetch");
  const { data } = await useFetch(url("run_useFetch"));
  console.log(`Response : ${data.value}\n`);
};

await run_useFetch();

fetch_testを実行してページを開くと、コンソールには以下のログがプリントされます。

run_useFetch
Response : Hello, run_useFetch on server!

一方、クライアントのコンソールには以下のログがプリントされます。

run_useFetch
Response : null

APIサーバのコンソールには以下のログがプリントされます。

/run_useFetch%20on%20server
/run_useFetch%20on%20client

すなわち、Nuxt3サーバとクライアントそれぞれがAPIサーバに直接リクエストを送信したようです。

公式ドキュメントによるとuseFetchuseAsyncData(url, () => $fetch(url))とほぼ等価とのことです。一方、今回の検証コードではNuxt3サーバとクライアントで別々のURLを叩いているため、useFetchの中身のuseAsyncDataの第一引数が異なることがサーバ・クライアント双方からのリクエストの原因ではないかと思います。

更に、ブラウザのコンソールにはnullが返されていることについては筆者はまだよくわかっていません。このような、ページの初回レンダリング時のフェッチでサーバとクライアントで別々のURLを叩くような使い方は普通はしないと思いますが、時間のある時になぜこのような挙動になるのか調べてみたいところです。

なお、ボタンを押下した時の挙動はrun_useAsyncFetchと同様になります。

useAsyncData + server=false

const run_useAsyncData_only_client = async () => {
  console.log("run_useAsyncData_only_client");
  const { data } = await useAsyncData(
    'run_useAsyncData_only_client',
    async () => {
      return await $fetch(url("run_useAsyncData_only_client"));
    },
    { server: false },
  );
  console.log(`Response : ${data.value}\n`);
};

run_useAsyncData_only_client()

run_useAsyncDataとの違いは{ server: false },のみです。

fetch_testのコンソールには以下のログがプリントされます。

run_useAsyncData_only_client
Response : null

クライアントのコンソールには以下のログがプリントされます。

run_useAsyncData_only_client
Response : null

APIサーバのコンソールには以下のログがプリントされます。

/run_useAsyncData_only_client%20on%20client

つまり、Nuxt3サーバでは、run_useAsyncData_only_client自体は実行されているようですが、useAsyncDataの第二引数の関数は実行されていないようです。一方、クライアントでは、第二引数まで実行されてAPIサーバと通信してはいるものの、useFetchと同様に戻り値がnullになるようです。

ボタンを押下した場合は、前二つと同様に、クライアントからAPIサーバに直接通信されます。

$fetch

const run_$fetch = async () => {
  console.log("run_$fetch");
  const data = await $fetch(url("run_$fetch"));
  console.log(`Response : ${data}\n`);
};

await run_$fetch()

fetch_testのコンソールには以下のログがプリントされます。

run_$fetch
Response : Hello, run_$fetch! on server

クライアントのコンソールには以下のログがプリントされます。

run_$fetch
Response : Hello, run_$fetch! on client

APIサーバのコンソールには以下のログがプリントされます。

/server%20on%20run_$fetch
/client%20on%20run_$fetch

$fetchはNuxt3サーバとクライアントの両方で実行されるとのことなので、妥当な結果です。

ボタンを押下した場合は、他と同様に、クライアントからAPIサーバに直接通信されます。

6. 終わりに

Nuxt3のData Fetchingに関して、実際にコードを作成して挙動を確認しました。

筆者はNuxt歴がまだ浅く、あまり深い知識はありません。誤りや、明快な説明のドキュメントを発見された方は教えていただければ幸いです。

Discussion