メタバースイベントサイト開発、Webアプリケーション開発、個人開発で得た、Nuxt3の「わがった!」と「わがんない」
はじめに
本稿は、QiitaにおけるVR法人HIKKY Advent Calendar 2023の2日目の記事であり、記載内容は投稿日時時点の情報となります。
手前味噌で恐縮ですが、本日10時より、バーチャルマーケット2023 Winterが開幕しました。以下がイベントWebサイトのURLです。
なお、本稿ではバーチャルマーケットそのものについての解説は行いません。ぜひ、我々が開発したイベントWebサイトのAboutページをご確認ください。
そして、このイベントWebサイトは、フロントエンド領域はNuxt3, TypeScript, Zod, Sass(Scss)を用いて構成しています。バックエンドはRuby on Rails等を、インフラにはGAE等を利用しているのですが、フロントエンドのチーフである私がそれらを詳細に書き記すのは畑違いであるため、別のアドベントカレンダーの記事に譲ることとします。
なお、私は他にも昨年の12月に公開した同社のMyVketや、個人で操業しているエストニア法人OmusBridge OÜの公式サイトでも、Nuxt3を扱ってきました。本稿では、それらの開発のなかで得たNuxt3に関する「わがった(分かった)!」と「わがんない(分かんない)」[1]を挙げていき、2023年のNuxt3知識の棚卸しをしていきます。
Nuxt3のリアクティビティとステートに関する「わがった!」と「わがんない」
Nuxt3は2022年11月16日にリリースされた、比較的新しいフロントエンドのフルスタックフレームワークです。Nuxt3は、同じくフロントエンドのJavaScriptフレームワークであるVue3を拡張し、フロントエンドからバックエンドまで、Web開発を一気通貫で行えるようにしてくれます。静的サイト、SPA、SSRのアプリケーションのbuildに加え、エッジサイドレンダリングにも対応し、ISGやISRができたり、複数のレンダリング方式を混在させたハイブリッドレンダリングができるWebアプリケーションを構築することができます。
本章の章題であるリアクティビティやステート管理には、基本的にVue3の機能を用いることになります。リアクティビティーを扱う具体的なAPIとしては、ref()
やreactive()
、computed()
などがよく知られていますね。詳細はVue3の公式ドキュメントに、分かりやすく説明されています。
ただ、上記ドキュメントに記載されている通り、いかんせん、リアクティビティーAPIは種類が多いです。且つ、Nuxt3で登場するuseState()
ではリアクティビティとは別にステート管理も必要になってくると、それらがこんがらがってしまいます。リアクティビティとステート、なんも「わがんない」。
しかし、使っていくうちに「わがった」ことは、 「だいたいはref()
とcomputed()
で話がつく」「どうしてもグローバルにステートを維持したければuseState()
を使う」 という2点です。「こういう場合はref()
で、こういう場合にはreactive()
で...」とやる必要はなく、今使っている規模とドメインのWebアプリケーションであれば、上記のルールだけで十分に事足ります。これはVue2からも言えたことですが、watch()
やwatchEffect()
を使いたくなった場合にまずやることというのは、それらの使い方の調査ではなく、そもそものロジックの見直しです。ロジックが複雑になりつつある兆候なので、綺麗にシンプルにするように努めるべきです。複雑なステート管理は愚かな人類には早すぎます。AIが全部やってくれる時代になるまでは、極力シンプルに作っていきましょう。
Nuxt3のFetchにおける「わがった!」と「わがんない」
Nuxt2からNuxt3になって大きく変わったことのひとつは、fetcherの変更です。useFetch()
、useLazyFetch()
、useAsyncData()
、useLazyAsyncData()
、$fetch()
が提供されており、どれを使えばいいのか「わがんない」になりがちです。実際、私自身も最適な使い分けというのは「わがんない」です。各々の定義自体は以下のNuxt3公式ドキュメントをみれば「わがった!」となるのですが。
さらに、使い分けのみならず、戻り値やオプションも多くあります。軽く使う分には問題ないのですが、深淵に踏み込もうとすると「わがんない」になります。Nuxt2ではおそらくaxiosを使うのが一般的だったのですが、それと比べて、非常に考えるべき項目が多いことがわかるはずです。
// ref: https://nuxt.com/docs/api/composables/use-fetch#type
function useFetch<DataT, ErrorT>(
url: string | Request | Ref<string | Request> | () => string | Request,
options?: UseFetchOptions<DataT>
): Promise<AsyncData<DataT, ErrorT>>
type UseFetchOptions<DataT> = {
key?: string
method?: string
query?: SearchParams
params?: SearchParams
body?: RequestInit['body'] | Record<string, any>
headers?: Record<string, string> | [key: string, value: string][] | Headers
baseURL?: string
server?: boolean
lazy?: boolean
immediate?: boolean
getCachedData?: (key: string) => DataT
deep?: boolean
default?: () => DataT
transform?: (input: DataT) => DataT
pick?: string[]
watch?: WatchSource[] | false
}
type AsyncData<DataT, ErrorT> = {
data: Ref<DataT | null>
pending: Ref<boolean>
refresh: (opts?: AsyncDataExecuteOptions) => Promise<void>
execute: (opts?: AsyncDataExecuteOptions) => Promise<void>
error: Ref<ErrorT | null>
status: Ref<AsyncDataRequestStatus>
}
interface AsyncDataExecuteOptions {
dedupe?: boolean
}
type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'
弊チームではもとより、将来的なfetcherの移行を見据えて、そのマイグレーションを楽にしようと、以下のようなfetcherのラッパーを用意していたのですが、悉く修正が必要になりました。
// ex: api.ts(一部簡略化して記載)
/** 前略 */
const apiFetchFunction = (
method: string,
_path: string,
_options?: Omit<FetchOptions, 'method'>
) => {
const _DEPENDED_API = fetcher
const _DEFAULT_FETCH_API = pluginFetchApi().fetchApi || _ohMyFetchApi
const FETCH_API = _DEPENDED_API || _DEFAULT_FETCH_API
return (path = _path, opts = _options) => {
const options = { ...opts, method }
return FETCH_API(path, options)
}
}
export const api = {
get: (path: string, fetchOptions: FetchOptions) => apiFetchFunction('GET', path, options)
post: (path: string, fetchOptions: FetchOptions) => apiFetchFunction('POST', path, options)
put: (path: string, fetchOptions: FetchOptions) => apiFetchFunction('PUT', path, options)
delete: (path: string, fetchOptions: FetchOptions) => apiFetchFunction('DELETE', path, options)
patch: (path: string, fetchOptions: FetchOptions) => apiFetchFunction('PATCH', path, options)
}
Nuxt2と比較して破壊的な変更が行なわれたfetcherの移行はとても大変でしたが、関係者の皆様のおかげで、どうにかNuxt2からNuxt3に移行できました。TypeScript有識者が作ってくれたZodの型関数にもずいぶん助けられていますね。
// ex: return.ts(一部簡略化して記載)
/** 前略 */
export function ensureAsyncDataOf<T>(
responseSchema: ZodType<T, ZodTypeDef>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
y: any
): asserts y is AsyncDataResponse<T> {
responseSchema.nullable().parse(y.data.value)
fetchErrorSchema.nullable().parse(y.error.value)
}
export function requireAsyncDataOf<T>(
x: ZodType<T, ZodTypeDef>,
y: unknown
): AsyncDataResponse<T> {
ensureAsyncDataOf(x, y)
return y
}
こういった仕組のもとでという前提ですが、実質的には 「軒並みuseFetch()
を使うだけでOKや」 ということが「わがった!」なのですが、最適化を考えると、もう少し詳しく調べないといけないと感じています。Nuxt3の破壊的変更ラッシュも近頃は少し落ち着いてきたので、ぼちぼち調べていきたいところですね。一寸先は闇で、深淵を知ろうとすると「わがんない」になるのは、フロントエンド領域の難しいところだと感じる毎日です。管理職になっても日々勉強ですね。
Nuxt3のその他の「わがった!」
このペースで書き続けると薄い本くらいの分量になりそうなので、その他の「わがった!」を覚えている範囲で箇条書きであげていきます!
- Nuxtの3.2系以降で、Nuxt Modules以外の目的で
modules
フォルダを使用すると、nuxt.config.tsの設定を変更しない限りデフォルトで実行時エラーになるのが「わがった!」。それまで汎用的な関数をmodules
フォルダに格納していたが、utils
フォルダに移すことで対応した。 - SSR(ESR含む)なプロダクトであれば、簡単なAPIを作ったり、あるいはクライアントから直接叩きたくない(例えばキー情報などを隠匿したい)APIを叩く場合には、nitroを使ってnuxtと同一リポジトリでやっちゃうほうがお気軽なことが「わがった!」。supabase等を使えば型情報をフロントとバックエンドで共用できるのもとても素晴らしいですね。
- ドメインのロジックは
composables
にまとめると便利で見通しが良くなることも「わがった!」。 - layoutの上位にある
app.vue
に共通処理を仕込んで、provide()
、inject()
することで、composablesを容易に引き摺り回せることが「わがった!」。
Nuxt3のその他の「わがんない」
一方で、「わがんない」なことも結構あります。ここらはおいおい調べつつ対応していきたいですね。
- 古めのNuxt3だとSSRとCSRの間でリアクティビティが途切れることがあるのが「わがんない」... CSRでre-fetchかければ問題ないのですが...
- i18nの最適な設定が「わがんない」。なんとなくでi18nを使っている...(懺悔)
- ESRでHybrid Rendering(on AWS Lambda-edge)しようとすると、うまくできないのがわがんない... ここらの設定は検証中です。
- routerがpagesで動かないことがある。「わがんない」...
おわりに
Nuxt3は新しいFWであるため、日々壁にぶちあたりながら、トライアンドエラーをしながら使用しています。また、Nuxt3はここ最近までは破壊的な変更がちょくちょく入ってきていたので、アプデするたびに辛みがあることが多々ありました。VitestやStorybookを使うなかでエラーが発生したり、YarnをV4にあげたりするとGAEがうまく動かなかったり... 時代に追従するのに苦戦しているのは、どこの現場でもあるあるなことと存じます。
それでも、Nuxt3は便利なDev Toolsが使えたり、ユーザーにとっても益のあるHybrid Renderingが使えたり、Banにも対応していたりと、夢のあるフレームワークです。次世代フレームワークが台頭するまでは、仲良く付き合っていきたいと、個人的には思ってます。もちろん、チームでどうするかはチームで話しながら決めていきたいですね。2023年は、実務でも使えうるFWに進化したNuxt3、2024年にはどうなるのか、とても楽しみです。
最後に、宣伝ですが、冒頭にも紹介した通り、本日からバーチャルマーケット2023 Winterが開幕しました。世界最大級のメタバースイベントで、VRChatでも参加できますし、イベントWebサイトのトップページから、直接参加することもできます!また、12/16(Sat)-17(Sun)には、渋谷と原宿でリアルイベントも開催されています。一部ではオワコン扱いされているVR・メタバースですが、作っていても参加しても楽しいコンテンツです。ぜひ、足を運んでいただけますと幸甚です!
末筆ながら、私自身、いろいろなイベントに登壇したり、記事を残していくムーブをしていく所存です。界隈の皆様、対戦よろしくお願いします。
以上
-
ホロライブのさくらみこ様の語録より引用。「わがった!」→「わがんない」のお家芸、好きです。 ↩︎
Discussion