PINIAすごい!SSRすごい!
動かして身につく。RailsAPIとNuxt.jsで作るJWT(JSONWebToken)ログイン認証【22時間超解説】を参考にしました。
教材はNuxt2なので古いので、学んでいるNuxt3に組み替えて作っておりました。
ですが理解半分でログイン認証機能をpiniaやCookieに保管する方法で行っていっていたところ、エラーがでてつまりに詰まりました。
リロードすると必ずハイドレーションエラーがでる
chunk-JSACULTC.js?v=18d07a08:1503 [Vue warn]: Hydration text content mismatch on
数日かかって私がハマったpointで、解決できた方法です。
リロードや離脱時にストアデータが吹っ飛ぶ問題
基本的にHTTPはステートレスに設計されているので、リロードやページ遷移によってデータが吹っ飛びます。
そのときにNuxtで起こる問題点です。
仕組みを理解しておかないとエラーが起こります。
①:ハイドレーションエラーが起こる
piniaのデータをリアクティブに表示した部分でハイドレーションエラーが起こります。
なぜならpiniaはクライアントサイドのメモリに保管されるからです。
②:pluginsやmiddlewareでpiniaの情報を扱うとエラーが起こる
piniaのデータを扱って、pluginsやmiddlewareで初期化時やページ遷移時に、サーバー側で判定処理を行うとエラーが起こります。
なぜならpiniaはクライアントサイドのメモリに保管されるからです。(大事なので2回)
PINIAの永続化でもいい?
リロードや初期化のこと考えるならPINIAの永続化すればええやん。
と思った方、確かにそうかもしれません。
ショッピングカートの情報など、そもそもバレても重大性が低いものであれば、セキュリティ的にそれでもいいかもしれません。
ただし、永続化の仕組みはセッションかローカルストレージに一時的に避難させる仕組みです。
重要なクライアントサイドの情報が丸裸で見えるのは嫌だったので、メモリに保管する実装しました。
あと、SSRを理解しておかないと、NuxtやらNextやら最近のモダンフロントエンドを使いこなせないと思い、勉強してみました。
nuxtのSSR機能が理解できてなかった
Nuxt3の特徴としてサーバーサイドレンダリング(SSR)の機能がデフォルトで使用されています。
これはサーバーサイドで先に、次にクライアントサイドでHTMLの描画とJavascriptの実行を2回行っています。
なので、PINIAの情報をscriptタグ内やtemplateタグ内に使用すると、サーバーサイドレンダリングをするときにはもちろんPINIAの情報は保持していないので、ハイドレーションエラーになっていしまいます。
Nuxtのpluginsやmiddlewareについて
Nuxtはプラグインはアプリケーション初期化時(リロードや初回訪問)、ミドルウェアはページ遷移時に起動します。
デフォルトのSSRは、先にサーバー側で先にpluginsやmiddlewareが起動し、その後クライアントで動きます。
ログイン状態を検証するために、piniaやCookie等の検証でよく使用されます。
が、ここで問題点。認証情報はクライアントに情報を持ちます。SSRでデータを扱えません。
pinia・Cookieの検証ロジックをそこに組み込んだ場合、デフォルトのままでは必ずエラーがでます。
繰り返しますがデフォルトのSSRは先にサーバー側で先にpluginsやmiddlewareが起動し、その後クライアントで動きます。
データをNuxtに保持していないので、サーバー側で検証するのは不可能です。
データをNuxtに保持していないので、サーバー側で検証するのは不可能なのでエラーが起こります。
ここでポイント整理
- piniaはそもそもクライアントサイドのメモリに保存するものでサーバーサイドには保持していない
- NuxtのSSRのプラグイン・ミドルウェアは初期化時にサーバーサイドが先に、そしてクライアントサイドで2回発動する。
なのでpiniaの情報を扱うと、その情報はクライアントにしかないため、ハイドレーションエラーやロジックに差異が起こるのは当たり前。 - クライアントサイドにしか持たないpiniaの情報をSSRでレンダリングして先に渡すのは不可能
- そもそも論、認証情報はクライアントの確認であって、レンダリングのためにNuxtに持つ意味はあまりない
(fetchしたらできなくもないが、無理やりSSRを実行することが目的になってしまっている) - 永続化はローカルストレージかセッションストレージに情報を一時的に移しているだけなので、トークンをそこに保存するのはリスクがある
やったこと 対策
①pluginsは○○.client.tsとして実行する
ファイル名の付け方で、クライアントサイドだけで実行するようにします.
リロードや再訪問時はストア情報は消えています なのでクッキー情報をもとにfetchし、クライアントのメモリ上に保存することで、ログインしたままの実装が可能になりました
import { useAuthStore } from '~~/stores/auth';
//リロード時や初期アクセス時にリフレッシュトークンを持っていたらログイン状態にする
//セッション情報が改ざんされた場合、Rails側でセッション情報はさくじょ
export default defineNuxtPlugin( async () =>{
console.log("plugins refreshTokenInit")
const config = useRuntimeConfig()
const auth = useAuthStore();
const refresh = async() =>{
await $fetch(
config.public.apiOrigin+'/api/v1/auth_token/refresh',
{
method:"POST",
credentials: 'include',
body:{},
headers:{
'X-Requested-With': 'XMLHttpRequest',
'Authorization': `Bearer ${auth.auth.token}`,
},
}
).then(response =>{
auth.setAuth(response)
console.log(response)
}).catch(error=>{
})
}
async function initFunc(){
const response = await refresh()
}
await initFunc()
})
globalのmiddlewareもクライアントサイドのみにする
globalmiddlewareもpinia情報を扱うのでクライアントサイドだけで実装したかったのです。
if (process.server)
return
サーバー側での処理をスキップさせることができます。
import { useAuthStore } from '~~/stores/auth';
export default defineNuxtRouteMiddleware( (to,from) =>{
const auth = useAuthStore();
if (process.server) return
//以下にクライアント側で行いたいロジック記載
}
これでpiniaの情報を扱っても、クライアントサイドのみで扱えるのでエラーがでません。
これでで実装していたページ遷移で発火するToken確認処理も、クライアントサイドのみで処理を行うようにできました。
ストア情報はClientOnlyで囲む
こうすることでサーバー側のレンダリングをスキップすることができ、ハイドレーションエラーを防げます。
ただこれは限定的で、多用はだめ。
対処療法的な使い方です。 本来はレンダリングのタイミングを意識して、根本的に差異をなくしてエラーをなくすのが吉。
まとめ
SSR使いこなすの難しいです。。
ですが、SPAアプリケーションのユーザー体験はSSRを理解して使いこなすと、品質はかなり上がるなと感じました!!
コメント