Nuxt3のData Fetchingを検証する
1. はじめに
弊社では一部ソフトウェアの開発に現在Nuxt3を使用しています。Nuxt3は、Vue.jsにServer Side Rendering(SSR)機能やその他色々を含めたフレームワークです。
Nuxtは非常に便利なフレームワークではあるのですが、SSRにおいては、レンダリングにまつわる全ての計算がサーバサイドまたはクライアントサイドのどちらか一方だけで実行されるわけではなく、ある処理はサーバサイドで実行され、別のある処理はクライアントサイドで実行され、……とかなり複雑です。
特に、ユーザデータや帳票データなどを別のサーバから取得するData Fetchingには注意が必要です。適切な実装を怠ると、クライアントとNuxt3のサーバでフェッチを二回リクエストしてしまうという問題があります。
下記の公式ドキュメントによると、$fetch
ではなくuseAsyncData
やuseFetch
を利用すればData fetchingはNuxt3のサーバサイドのみで実行され、クライアントにはNuxt3のサーバからデータが渡されるらしいのですが、この記事では、それが本当かどうかを調べた結果をまとめました。
対象読者
- Nuxt3の基礎はご存知の方
- Nuxt3のSSRやData Fetchingの挙動の理解を深めたい方(と言ってもこの記事で説明するのは全容のごく一部分ですが。。。)
TL;DR
- 公式の説明の通りでおおよそは間違っていないが、以下に注意。
- ボタン押下時の
@click
など、クライアントサイドでのみ実行されるスクリプト内のuseAsyncData
・useFetch
ではクライアントから直接データフェッチされる。 -
useFetch
はURLの一致でキャッシュを区別するため、クライアントとサーバでURLが変わるような特殊なケースではクライアントとサーバ双方からリクエストが送信される。かつ、クライアントへのレスポンスは何故かnull
になる。 -
server
オプションをfalse
にすると、レスポンスはクライアント・サーバともにnull
になる。
2. 検証の方法
- データのフェッチ先のダミーとして、
/(path)
をGETしたらHello, (path)!
を返すAPIサーバを立てます。 - 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でサクッと作ります。
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}`);
{
"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
を以下のように書き換えます。
<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サーバに直接リクエストを送信したようです。
公式ドキュメントによるとuseFetch
はuseAsyncData(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