Nuxt3でデータの取得先をクライアントサイドから隠すには
1. はじめに
Nuxt3は、Vue.jsにServer Side Rendering(SSR)機能やその他色々を含めた、非常に便利なフレームワークです。ただ、サーバサイドとクライアントサイドの連携の仕組みを理解して使いこなすのは中々に難しいです。
課題の一つとして、特に何も工夫しないと、画面を構築するためのデータのフェッチ先のURLやAPIシークレットキーなどがクライアントサイドにも共有されてしまうことが挙げられます。この記事では、データの取得先に関する情報をクライアントサイドから隠す方法を検証します。
まず前提として、サーバサイドからしか読めない変数の値を定義するだけなら簡単で、runtimeConfig
のprivate機能を利用するだけです。
しかし、URLなどが隠されてしまうと、クライアントサイドが外部のサーバからデータを直接フェッチすることは当然できなくなります。クライアントサイドにデータの取得先の情報を隠しながら、サーバサイドで取得したデータをクライアントサイドにうまく渡すにはどうしたら良いでしょうか。
この記事では、筆者なりに検証したいくつかの方法を紹介します。より良い方法や他のわかりやすい解説記事などをご存知の方はぜひ教えてください。
また、以下の記事で検証した機能も一部利用します。よければ併せてご覧ください。
対象読者
- Nuxt3の基礎はご存知の方
- Nuxt3のSSRやData Fetchingの挙動の理解を深めたい方
TL;DR
- サーバサイドレンダリング時のフェッチでは、
runtimeConfig
に定義したURLを普通にuseAsyncData
に渡せば良い。サーバサイドのみが外部からデータをフェッチし、フェッチされたデータはサーバサイドからクライアントサイドに渡される。クライアントサイドでは通信が発生しないため、クライアントサイドから読めない変数に定義されたURLは無視される。 - しかし、
<NuxtLink>
でのページ遷移時や、ブラウザでのボタン押下時のスクリプトなどでは、クライアントサイドが外部と直接通信するため上の方法では不十分。server/api
以下にプロキシAPIを追加し、その中でruntimeConfig
のURLを参照すれば、データの取得先を隠蔽したまま、サーバサイド・クライアントサイドどちらからもデータがフェッチできる。 - サーバサイドからのフェッチのために、フェッチ先のAPIに必要な認証情報をクライアントからサーバに共有するには、認証情報をCookieに保存して
useCookie
を使用する方法がある。サーバサイドにおいてuseCookie
を使用すると、クライアントサイドからページがリクエストされた際のリクエストヘッダのCookieにアクセスできる。
2. データフェッチ用のAPIサーバの準備
まず、データフェッチ先の準備として、何かをリクエストしたら何かをレスポンスするダミーサーバを用意します。リクエストやレスポンスの中身は何でも構いません。
この記事の各種コードの実行結果の確認には、/(path)
をGETしたらHello, (path)!
を返すAPIサーバを作成して使用しました。細かい実装については前の記事をご覧ください。
3. Nuxt3プロジェクトの準備
次に、検証コードを動作させるためのNuxt3プロジェクトを準備します。
まずは新しいNuxt3プロジェクトを生成します。
$ npx nuxi@latest init client
次に、プロジェクト内にpages
ディレクトリを作成します。
$ cd client
$ mkdir pages
pages
ディレクトリ以下にfoo.vue
・bar.vue
などを保存してプロジェクトを起動すると、(baseURL)/foo
・(baseURL)/bar
等のURLでページにアクセスできます。
最後に、nuxt.config.ts
を以下のように書き換えます。appServerURL
の値は、データフェッチ用のAPIサーバのURLに合わせて書き換えてください。
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
ssr: true,
runtimeConfig: {
apiServerURL: "http://localhost:8000",
},
})
runtimeConfig
の中にキーバリューを定義しておくと、以下のように参照することができます。このキーの値は、サーバサイドではnuxt.config.ts
で設定した値になり、クライアントサイドではundefined
になります。
const runtimeConfig = useRuntimeConfig();
// サーバサイドでは"http://localhost:8000"、クライアントサイドではundefinedになる。
console.log(runtimeConfig.apiServerURL);
ちなみに、nuxt.config.ts
において、runtimeConfig.public
以下に定義したキーバリューはクライアントサイドからも参照できます。
4. 検証
それでは、クライアントサイドに宛先を隠してデータをフェッチするためのコードを色々と検証してみましょう。
useAsyncData
を普通に使用する
まずは、useAsyncData
を使ってみます。pages/app1.vue
というテキストファイルを以下の内容で作成してください。
<template>
<div>
<p>{{ response.message }}</p>
</div>
</template>
<script setup>
const runtimeConfig = useRuntimeConfig();
console.log(`API Server : ${runtimeConfig.apiServerURL}`);
const response = { message: "" };
const fetchMessage = async () => {
console.log("in fetchMessage");
const { data } = await useAsyncData(
'fetchMessage',
() => {
console.log("in useAsyncData");
return $fetch(`${runtimeConfig.apiServerURL}/server`);
}
);
response.message = data.value;
};
await fetchMessage();
console.log(response.message);
</script>
前回の記事でも確認した通り、<script setup>
内でuseAsyncData
を使用すると、サーバサイドでデータがフェッチされ、フェッチされたデータはNuxt3サーバからクライアントに共有されます。
yarn dev
などでプロジェクトを実行し、/app1
をブラウザで開いて実行結果を確認すると、まずサーバサイドのログは以下のようになっているはずです。
API Server : http://localhost:8000
in fetchMessage
in useAsyncData
Hello, server!
一方、クライアントサイドでは、ブラウザの画面に「Hello, server!」が表示され、開発者ツールからログを確認すると、以下のようになっているはずです。
API Server : undefined
in fetchMessage
Hello, server!
すなわち、クライアントサイドにはruntimeConfig.apiServerURL
の真の値が隠されていますが、useAsyncData
の機能によってデータが共有されていることがわかります。画面もきちんと描画されています。
useAsyncData
をサーバサイドでのみ使用する
今度は、fetchMessage
をサーバサイドでのみ実行してみましょう。結論から言うと不適切な方法なのですが、SSRの挙動の理解が深まると考え、載せてみました。
pages/app1.vue
をpages/app2.vue
にコピーして、最後の部分を書き換えます。
<script setup>
// 略
// await fetchMessage();
if (process.server) {
await fetchMessage();
}
// 略
</script>
/app2
をブラウザで開いて実行結果を確認すると、まずサーバサイドのログは/app1
と変わらないはずです。
API Server : http://localhost:8000
in fetchMessage
in useAsyncData
Hello, server!
一方、クライアントサイドについては、まずコンソールのログは以下のようになっているはずです。
API Server : undefined
in fetchMessage
undefined
response.message
がundefined
のままです。クライアントサイドにはresponse.message
に値を再代入する処理が一つもないため当然ではあるのですが、ブラウザの画面には「Hello, server!」が表示されているはずです。
これは、サーバサイドおよびクライアントサイドの処理の順序が以下のようになっているためだと思われます。
- サーバサイドで
<script setup>
実行(=データフェッチ) - サーバサイドでレンダリング
- レンダリング結果をクライアントに送信
- クライアントサイドで
<script setup>
実行
1番の終了時点でサーバサイドのresponse.message
には"Hello, server!"が代入されているため、2番のレンダリングにもそれが反映されるものの、4番でクライアントサイドで改めて<script setup>
を実行する際にはクライアントサイドのresponse.message
はundefined
のままなので、このような結果になったのではないかと思います。
ページ遷移時のデータフェッチでも取得先を隠す
SSRモードでは、ページのURLを直接開いた場合はサーバサイドでレンダリングした結果がクライアントサイドに渡されるのですが、<NuxtLink>
タグでページ遷移した場合にはクライアントサイドでレンダリングが行われます。よって、前節までの方法は、複数の.vue
ファイルから構成されるサービスでは不都合です。
試しに、以下のpages/app3.vue
を作成してください。/app1
へのリンクを持つだけのページです。
<template>
<div>
<NuxtLink to="/app1">To app1</NuxtLink>
</div>
</template>
このページを開いて、リンクをクリックして/app1
に移動すると、サーバサイドのコンソールにはログが何も表示されず、<script setup>
がサーバサイドで動いていないことがわかります。
また、ブラウザのコンソールにはGET http://localhost:3000/undefined/server 404 (Page not found: /undefined/server)
のようなメッセージが表示されているはずです。クライアントサイドから外部にリクエストを送ろうとして、runtimeConfig
に定義したURLがundefined
であるために失敗していることがわかります。
この問題を解決するためには、サーバサイドにAPIを実装してプロキシとして利用します。
サンプルのコードを作って検証してみましょう。まず、プロジェクトにserver/api/my_fetch
ディレクトリを作成して、server/api/my_fetch/[name].js
を以下のように作成します。
const runtimeConfig = useRuntimeConfig();
export default defineEventHandler((event) => {
const name = getRouterParam(event, 'name')
console.log(`API Server : ${runtimeConfig.apiServerURL}`);
return $fetch(`${runtimeConfig.apiServerURL}/${name}`);
});
/api/my_fetch/[name]
にアクセスすると、本来のデータフェッチ先にリクエストを送るAPIです。
そして、pages/app4_1.vue
・pages/app4_2.vue
を次のように作成します。
<template>
<div>
<p>{{ response.message }}</p>
</div>
</template>
<script setup>
const response = { message: undefined };
const fetchMessage = async () => {
console.log("in fetchMessage");
const { data } = await useAsyncData(
'fetchMessage',
() => {
console.log("in useAsyncData");
return $fetch("/api/my_fetch/server");
}
);
response.message = data.value;
};
await fetchMessage();
console.log(response.message);
</script>
<template>
<div>
<NuxtLink to="/app4_1">To app4_1</NuxtLink>
</div>
</template>
app4_1.vue
は、app1.vue
のデータのフェッチ先を/api/my_fetch/server
に置き換え、不要なコードを取り除いただけのものです。また、app4_2.vue
は/app4_1
に遷移するリンクがあるだけのページです。
サーバを立ち上げて/app4_1
を直接開くと、サーバサイド・クライアントサイドそれぞれのコンソールに以下のログが表示されます。
API Server : http://localhost/8000
in fetchMessage
in useAsyncData
Hello, server!
in fetchMessage
Hello, server!
また、ブラウザの画面には正しく「Hello, server!」と表示されています。
サーバサイドのログに「API Server : http://localhost/8000
」が一つだけあり、クライアントサイドにはありません。よって、普段のuseAsyncData
と同じく、新しく作成したAPIへのリクエストは一回しかなく、かつそれがサーバサイドで実行されたことがわかります。
次に、/app4_2
を開いてからリンクをクリックして/app4_1
に移動すると、今度はログは以下のようになっているはずです。
API Server : http://localhost/8000
in fetchMessage
in useAsyncData
Hello, server!
fetchMessage
関数内の$fetch
はクライアントサイドで実行されているものの、大元のAPIサーバのURLを隠蔽したままmy_fetch
を経由してデータを取得できていることがわかります。
server/api
以下にプロキシを実装する方法は、ページ遷移時に限らず、ブラウザでのボタン押下などをトリガーとした動的なデータフェッチの際にURLを隠蔽する場合にも有効です。
クライアントサイドの認証情報をサーバサイドに渡す
Nuxt3を本格的なサービスの構築に利用する場合は、単にURLを叩くだけでなく、ログインして取得した認証情報を要求するAPIを使用するケースも多いと思います。
クライアントサイドから直接データをフェッチする分には、単に$fetch
の引数で認証情報をリクエストヘッダを渡せば十分です。しかし、サーバサイドからデータをフェッチするには、認証情報をクライアントサイドからサーバサイドに共有する必要があります。しかし、通常ではサーバサイドのスクリプトがクライアントサイドより先に実行されるため、認証情報の受け渡しに何かしらの工夫が必要です。
方法の一つは、useRequestHeaders
およびCookieを利用することです。useRequestHeaders
によって、ページの表示をサーバサイドにリクエストした際のヘッダ情報が取得できるため、その中からブラウザのCookieをパースして、認証情報を取り出すことができます。
まず、次のようにserver/api/my_fetch2/[name].js
を作成してください。
const runtimeConfig = useRuntimeConfig();
export default defineEventHandler((event) => {
const name = getRouterParam(event, 'name')
const headers = getHeaders(event);
const authorization = headers["authorization"]; // lower case
console.log(`API Server : ${runtimeConfig.apiServerURL}`);
console.log(`Authorization: ${authorization}`);
return authorization;
});
リクエストヘッダからauthorization
キーの値を取り出し、それをそのままレスポンスします。この関数ではダミーサーバと通信していませんが、フェッチ先に送信する認証情報がプロキシAPIで取得できているかどうかさえ確かめられれば十分なので、このように実装しました。
また、以下のようにpages/app5_1.vue
・pages/app5_2.vue
を作成してください。
<template>
<button @click="increment">{{ counter }}</button>
<p><NuxtLink to="/app5_2">app5_2</NuxtLink></p>
</template>
<script setup>
const counter = useCookie("counter");
counter.value = 0;
const increment = () => {
console.log("increment");
counter.value++;
}
</script>
/app5_1
は、ボタンを押した回数をCookieに保存するページです。また、/app5_2
へのリンクも表示します。
<template>
<div>
<p>{{ response.message }}</p>
</div>
</template>
<script setup>
const response = { message: undefined };
const parseCookies = (cookieStr) => {
return cookieStr
.split("; ")
.map((v) => v.split("="))
.reduce((d, kv) => {
d[kv[0]] = kv[1];
return d;
}, {});
};
const fetchMessage = async () => {
console.log("in fetchMessage");
const { data } = await useAsyncData(
'fetchMessage2',
() => {
console.log("in useAsyncData");
var counter = undefined;
if (process.server) {
const cookieStr = useRequestHeaders(["cookie"])["cookie"];
const cookieDict = parseCookies(cookieStr);
counter = cookieDict["counter"];
} else {
counter = useCookie("counter").value;
}
console.log(`counter = ${counter}`);
const headers = { headers: { Authorization: counter } };
return $fetch("/api/my_fetch2/server", headers);
}
);
response.message = data.value;
};
await fetchMessage();
</script>
/app5_2
は、/api/my_fetch2/server
をリクエストし、レスポンスを表示するページです。ただし、送信元がクライアントサイドの場合は、Cookieからcounter
の値を取り出して、それをAuthorizationヘッダに設定します。一方、送信元がサーバサイドの場合は、ページのリクエスト時にクライアントサイドのブラウザから自動的に送信されたCookieをuseRequestHeaders
によって取り出し、それを解析してcounter
の値を取り出してAuthorizationヘッダに設定します。
それでは、プロジェクトを実行して/app5_1
を開き、好みの回数だけボタンをクリックしてから、/app5_2
へのリンクをクリックするか、/app5_2
を直接表示してみてください。ブラウザの画面やサーバサイドのログにボタンをクリックした回数が表示されているはずです。サーバサイドとクライアントサイドのどちらからのリクエストでも、ブラウザのクッキーから認証情報を取り出せていることがわかります。
ちなみに、maxAge
・expires
をuseCookie
に指定しない場合、NuxtのCookieの寿命はセッションまでであることにご注意ください。
……と、いう方法を先に紹介してみたものの、実は、useCookie
はサーバサイドでも使用することができます。
/*
var counter = undefined;
if (process.server) {
const cookieStr = useRequestHeaders(["cookie"])["cookie"];
const cookieDict = parseCookies(cookieStr);
counter = cookieDict["counter"];
} else {
counter = useCookie("counter").value;
}
*/
// これだけで良い。
const counter = useCookie("counter").value;
useCookie
の実装の中身を見てみると、先に紹介した方法のように、サーバサイドではリクエストヘッダを利用してCookieを構築する処理が実装されていました。わざわざ自分で実装しなくとも、NuxtのuseCookie
が同じことをやってくれるようです。ただ、仕組みを理解しておいた方がいざという時に役立つ可能性があるかもしれません。
5. 終わりに
Nuxt3のData Fetchingに関して、フェッチ先をクライアントから隠蔽する方法について、実際にコードを作成して挙動を確認しました。
全ての検証結果をまとめると、フェッチ先をクライアントから隠蔽するには、以下のシンプルな方法で十分であるという結論になるかと思います。
-
server/api
以下にデータのフェッチ先と通信するプロキシAPIを実装し、useAsyncData
からはこのAPIを叩く。 - 認証情報はCookieに保存すれば、
useCookie
でサーバサイド・クライアントサイドともにアクセスできる。
筆者はNuxt歴がまだ浅く、あまり深い知識はありません。誤りや、明快な説明のドキュメントを発見された方は教えていただければ幸いです。
Discussion