[キャッチアップ] Nuxt 3

Nuxt 3 でアプリケーション開発をする過程で公式ドキュメントを読んだ際のメモを残す場所

Introduction
Nuxt の主な説明。こう見るとだいぶ SSR 推しなんだね。
あと専用 CLI とかサーバーツールが新しくなってるんだ。

Installation
簡単なセットアップ。 nuxi
っていう CLI から諸々出来るようになってる。
ローカルで動かして試してく。

Key Concepts
Auto importss
- ヘルパー関数、コンポーザブル、Vue API をアプリケーション全体で自動的にインポートしてくれる仕組み
- 自動インポートはディレクトリ構造に基づいて行われる
- 型付けや IDE での補完は、実際にプロダクションでもインポートされて使用されるコードに対してのみ行われる
自動インポートされるモジュールの例
- データフェッチ系
- useAsyncData
- $fetch
- Vue コンポーネント系
- ref
- computed
- components/ 以下にあるコンポーネント
- composables/ 以下にあるコンポーザブル
- utils/ 以下にあるヘルパー関数

ビルトインだけじゃなくて自作のコンポーネントについても自動インポートが出来るんだ。
すごいけど vscode/volar にロックインしちゃったりするのかな。

Vue.js Development
Nuxt 3 は以下の理由で Vue を使用している
- データの変更がインタフェースに反映されるリアクティブモデルがあること
- HTML をベースとしたテンプレートによる直感的なコンポーネントが使えること
- 小規模開発から大規模開発までスケールできること
Vue with Nuxt
- SFC の使用を基本とする
- components/ ディレクトリ内のコンポーネントは自動でインポートされ、使用できる
- Vue Router を使用したルーティングをディレクトリベースで自動で決定する
Differences with Nuxt 2 / Vue 2
Vue 3 で変わったこと
- パフォーマンス向上
- Composition API の導入
- TypeScript サポート
Faster Rendering
仮想DOMの生成プロセスが書き直されて最適化されてる
Smaller Bundle
Vue のほとんどの機能がツリーシェイキング可能になったため、バンドルサイズを削減しやすくなった
Composition API
- 再利用可能なロジックをコンポーネントから分離できる仕組み
- <script setup> を用いてコンポーネント内で使用可能
- Nuxt 3 は Composition API を使った開発体験の向上を目指している
TypeScript Support
- Vue 3 / Nuxt 3 ともに TypeScript で書き直されたため、完全な型情報の提供がされている

Rendering Modes
Nuxt はサーバーサイド、ブラウザそれぞれで JavaScript を実行した Vue のレンダリングができる。両者にはメリット・デメリットが存在する。
Client-side Only Rendering
ブラウザは空のHTMLとJavaScript ファイルをダウンロードし、ブラウザ側で UI を構築する
Pros
- ブラウザだけで動くので、
window
などのブラウザ専用のAPIを気にせずどこでも使うことができる - サーバーを動かす必要がないので安く済む
- サーバーを必要としないのでオフライン対応しやすい
Cons
- JavaScript のダウンロードと実行が、端末のスペックと回線の品質に依存するためパフォーマンスの低下を引き起こしやすい
- SEOに弱い
Examples
Client-Side Only Rendering は、インタラクティブ性が高く、検索エンジンへのインデックスの必要性がないサービスに向いている。例えば SaaS やバックオフィスアプリケーション、ゲームなど。
Universal Rendering
Nuxt はサーバーサイドで Vue.js のコードを実行し、構築された HTML をクライアントに返すことができる。
クライアントは HTML を受け取りつつ、JavaScript の実行も行うことで、サーバサイドでレンダリングしつつ、そのままクライアントサイドレンダリングに引き継ぐことができる (ハイドレーション)
Props
- ユーザーは完成されたHTMLを受け取るだけですぐにコンテンツにアクセスできる
- SEOに強い
Cons
- サーバーとブラウザ、どちらで動くコードなのかを意識して開発する必要がある
- サーバーを稼働するためのコストがかかる
Exampels
Universal rendering は汎用性が高く、Client-Side Only Rendering に向いているサイト以外にはだいたい適しているl。特にコンテンツをベースとしている、ブログ、マーケティングサイト、ポートフォリオ、ECサイトなどに向いている。
Summary
- 要は使い分けが大事
- Nuxt はデフォルトで Universal Rendering モードになってるが、容易に変更可能
New Rendering Patterns in Nuxt 3
Nuxt 2 の Universal Rendering はほとんどのユースケースを満たせていたが、Nuxt 3 ではさらに Hybrid Rendering / Edge-side rendering が追加された。
Hybrid Rendering
ルートごとにルールを変更する仕組み(?)
Rendering on CDN Edge Workers
Nuxt 2 ではサーバーサイドが Node.js を使用することを前提としていたけど、Nuxr 3 では Nitro という新たなサーバーエンジンが使用され、これは Node.js 以外の Deno や各種 Edge Workers でも動くようになった。
Route Rules
ルートごとに色々指定できる。
export default defineNuxtConfig({
routeRules: {
// Static page generated on-demand, revalidates in background
'/blog/**': { swr: true },
// Static page generated on-demand once
'/articles/**': { static: true },
// Set custom headers matching paths
'/_nuxt/**': { headers: { 'cache-control': 's-maxage=0' } },
// Render these routes with SPA
'/admin/**': { ssr: false },
// Add cors headers
'/api/v1/**': { cors: true },
// Add redirect headers
'/old-page': { redirect: '/new-page' },
'/old-page2': { redirect: { to: '/new-page', statusCode: 302 } }
}
})

ルート単位でレンダリングモードを切り替えられるように出来て、かつ Node 以外の Edge Worker なんかでも動かせるようになったから、公開コンテンツを配信するルートだけ CDN で SSR とか出来るんだ。

なんとなく配信の最適化は Next が飛び抜けてて、Nuxt は開発者体験の向上を重視してるってイメージあったけど、Nuxt 3 からはだいぶ変わってきてるみたい?

Server Engine
サーバーエンジンとして Nitro を使用してる
- クロスプラットフォーム
- サーバーレスサポート
- APIルート
- コード分割、チャンク化の自動化
- Static + serverless のハイブリット
- HMR
API Layer
API レイヤー自体は h3 を使用している。
h3 の特徴
- ハンドラは JavaScript のオブジェクト、配列をそのまま返せ、自動で JSON にシリアライズされる
- ハンドラは Promise を返せる
- body のパース、クッキーの操作、リダイレクトなどのヘルパー関数を持つ
Nuxt では /server
ディレクトリ以下が h3 による API レイヤーになる
Direct API Calls
ビルトインの $fetch
メソッドで API を直接呼び出せる。内部的なフェッチライブラリは ofetch
- レスポンスの JSON は自動でパースする
- リクエストボディは Content-Type に基づいて自動で生成される
Typed API Routes
API ルートが値を返す場合、そのレスポンスの型が推論できるようになる
Standalone Server
Nuxt 2 自体はサーバーは Nuxt にロックインしていたが Nuxt 3 では Nitro のサーバーをスタンドアローンで動かすことができるため、ビルド時の成果物を使って任意の環境でサーバーを動かすことができる。

Nitro が思ったよりも Nuxt の特徴のほとんどを占めてるように感じる。
Nuxt - Nitro で残るものは何だ…? 自動インポートとか、Vue 関係の最適化、ビルトインコンポーネントにコンポーザブル、VueRouter の自動化あたりか。

h3 のハンドラが Promise を返せるってのがイマイチイメージできない。普通に await してから返してくれるって認識で良いのかな。
Handlers can return promises, which will be awaited (res.end() and next() are also supported)
だからそうっぽい。

Modules
Nuxt はフレームワークを拡張するためのモジュールシステムを提供する。
モジュールは npm で配布され、next.config.ts
に使用するモジュールを宣言することで使える。

ESM
Background
CommonJS Modules
- CJS は Node によって導入されたモジュール構文 (require / exports)
- Webpack や Rollup といったバンドラはこの構文をサポートして、ブラウザで動くようにしてくれる
ESM Syntax
- ESM は ECMAScript で定義されたモジュールシステム (import / export)
- ESM が普及する前から、Webpack や TypeScript といったツールは ESM 記法をサポートし、トランスパイルしたいた
What is Native ESM?
- ESM は現代ではブラウザがサポートしているため、Nuxt 2 ではブラウザ向けには ESM を、サーバー向けには CJS のコードを生成してる
- ライブラリの場合は、CJS と ESM 両方のバージョンを提供していることがあり、必要な方を使用することができる。
{
"name": "sample-library",
"main": "dist/sample-library.cjs.js",
"module": "dist/sample-library.esm.js"
}
- Nuxt 2 ではバンドラに webpack を使用しており、サーバー用には CJS を、ブラウザ用には ESM を使用するようになっている
- しかし、最新の Node.js は ESM を使用することができる (Native ESM)
- Node.js は、以下のいずれかの場合に ESM を使用する
-
package.json
にtype: 'module'
が設定されており、ファイルの拡張子が.js
である - ファイルの拡張子が
.mjs
である (推奨)
-
- よって Nuxt はサーバーコードを生成する際に
.mjs
ファイルを生成する
What Are Valid Imports in a Node.js Context?
Node.js が ESM (import
) を用いてモジュール解決をする場合、 package.json
の exports
または module
フィールドを参考にする
What Kinds of Problems Can There Be?
- かつてモジュール開発者は ESM フォーマットを
.ems.js
.es.js
といった慣習で配布してきた- webpack でバンドルする際は拡張子の制限がなかったので問題なかった
- Node.js による Native ESM では、
.esm.js
.es.js
を ESM として読み込むことが出来ない- 対象パッケージが
type: module
となってれば良いが、過去のパッケージはそうはなっていない
- 対象パッケージが
Troubleshooting ESM Issues
- 前述の問題はライブラリ側が仕様に準拠するべきである
- Nuxt ではライブラリごとの解決方法を調整する機能がある
Library Author Guide
ライブラリを実装する上でやることは2つ
- ESM として扱うファイルの拡張子を
.mjs
にする - ESM のみを提供する

この辺、歴史とか互換性もあいまって本当に難しいよなぁ。
あとどれぐらいしたら ESM が当たり前で、CJS の存在をまったく意識せずに開発できる世界になるんだろ。

TypeScript
Type-checking
- Nuxt はデフォルトでは型チェックは行わない
- 開発、デプロイのパフォーマンスの都合
- vue-tsc を用いた型チェックを有効化することも可能
-
nuxi typecheck
で手動実行も可能
Auto-generated Types
-
nuxi dev
nuxi build
実行時に、型定義(.nuxt/nuxt.d.ts
) と設定ファイル(.nuxt.tsconfig.json
) が自動生成される
Stricter Checks
より強力で安全な型を生成するためには、 nuxt.config を調整する必要がある。
export default defineNuxtConfig({
typescript: {
strict: true
}
})
これは tsconfig.json における strict とは違うらしい。

Configuration
Nuxt Configuration
デフォルトで、最も一般的なユースケースに沿った設定が適用されてるけど、 nuxt.config.ts
でその設定をいくらでも変えることができる。
デフォルトはこんなん
export default defineNuxtConfig({
// My Nuxt config
})
Environment Variables and Private Tokens
設定ファイルから環境変数を注入できる。サーバーサイドでのみ使うもの、クライアントサイドにも使うものを分けて定義できる。
export default defineNuxtConfig({
runtimeConfig: {
// The private keys which are only available server-side
apiSecret: '123',
// Keys within public are also exposed client-side
public: {
apiBase: '/api'
}
}
})
ここで定義した値は、実行環境の環境変数によって上書きされる。
環境変数は専用のコンポーザブルで利用可能。
const runtimeConfig = useRuntimeConfig()
App Configuration
app.config.ts
というファイルも別に存在し、こちらでもアプリケーションの設定を定義できる。
export default defineAppConfig({
title: 'Hello Nuxt',
theme: {
dark: true,
colors: {
primary: '#ff0000'
}
}
})
こちらは runtimeConfig とは異なり、環境変数でのオーバーライドはされない。
useAppConfig
で使用可能
const appConfig = useAppConfig()
runtimeConfig vs app.config
基本的にはビルドタイムで環境に応じて差し替えたい値は runtimeConfig
で、それ以外のアプリケーションの設定は app.config
で良い。
前者は実行環境の環境変数によって上書きされるため、ビルド時点では不定だが、後者はビルド時点で確定するのでバンドルに含まれるという違いがある。
隠したい情報も runtimeConfig
と考えて良さそう。
External Configuration Files
nuxt.config.js
はいくつかの他のツールの設定をまとめて記述できる
- nitro
- PostCSS
- Vite
- webpack
それ以外は個別の設定ファイルが必要
- TypeScript
- ESLint
- Prettier
- Stylelint
- TailwindCSS
- Vitest
- ...

Directory Structure
先にざっと見ておこ
.nuxt
- 開発環境でビルドした際の成果物ディレクトリ
- 直接触ることはない
.output
- プロダクション用にビルドした際の成果物ディレクトリ
- 直接触ることはない
assets
Web サイトで使用し、webpack/vite がビルドする静的ファイルの置き場所
- Stylesheet
- Fonts
- Images
ただしアセットをバンドルに含めるんじゃなくサーバーから配信したい場合は /public を使用する
public
サーバーから直接配信するファイルの置き場 (Nuxt 2 では static
)
components
- すべてのコンポーネント(pages 以外) を置く場所
- ここに置いておけば、自動インポートの対象になる
- 自動インポートのディレクトリ、拡張子はカスタムできるので、このディレクトリに必ずしも置く必要はない
自動インポート関連の Tips
- コンポーネント名はディレクトリを元に自動でプレフィックスが付けられる
-
components/base/foo/Button.vue
なら<BaseFooButton>
となる - ファイル名が
BaseFooButton.vue
ならそのまま使われるしそれが推奨されてる (?) - プレフィックスの自動付与はオプションで無効化出来る
-
-
Lazy
プレフィックスを付けた場合、コンポーネントはダイナミックインポートされる
ビルトインコンポーネント関連の Tips
-
<ClientOnly>
コンポーネント以下のコードはクライアントサイドでのみレンダリングされる -
<ClientOnly>
コンポーネントを使用しなくても、.client.vue
のファイルはクライアントサイドでのみレンダリングされる - Standalone server components を有効化することで、サーバー側でのみレンダリングするコンポーネントを作成できる(いわゆるサーバーコンポーネント)
-
<DevOnly>
コンポーネントは開発環境の場合だけスロットを描画してくれる -
<NuxtClientFallback>
コンポーネントは、SSR 時にエラーが発生した場合にのみ描画される
composables
- Composition API を用いたコンポーザブル置き場
- デフォルトで自動インポートの対象
- 自動インポート対象はデフォルトではディレクトリ直下のみ
- 他のディレクトリも対象にするように設定するか、直下に他のディレクトリをインポートするモジュールを用意する
- 自動インポートされるコンポーザブルの型は、開発環境を起動しているときのみ生成されるので注意
- コンポーザブルから Nuxt プラグインへのアクセスも可能
content
- CMS の一種である Nuxt Content Module にて使用するコンテンツ置き場 (.md, .yml, .csv, .json)
- Nuxt Content Module で出来ること
- コンテンツをコンポーネントのように扱う
- コンテンツを MongoDB のように検索する
- Markdown 内で Vue コンポーネントを使用する
- ナビゲーションの自動生成
layouts
- アプリケーション全体で使用するレイアウト用コンポーネント置き場
- 自動インポートの対象
- <NuxtLayout> と組み合わせて使用する
- レイアウトが一種類しかない場合は、このディレクトリでなく App.vue に直接記述することを推奨
- 複数のレイアウトコンポーネントがある場合、
<NuxtLayout>
コンポーネントで動的に使用するコンポーネントを切り替えられる
middleware
ルートミドルウェア(VueRouter におけるナビゲーションガード) 置き場
ルートミドルウェアは3種類ある
- 各 pages に直接定義されている匿名ミドルウェア
- middleware/ ディレクトリに配置され自動で読み込まれる名前付きミドルウェア
- middleware/ ディレクトリに配置され自動で読み込まれるグローバルミドルウェア (
.global
)
modules
- Nuxt モジュール置き場
- 自作のモジュールをここにおいておけば起動時に自動で読み込まれる
node_modules
いわずもがな
pages
VueRouter による自動ルーティングのためのディレクトリ
Nuxt 2 と一緒だった気がするものはスキップ
-
<NuxtPage>
コンポーネントで、現在のルートに対応するコンポーネントを描画する - ページは必ず一つのルート要素が必要
- ページトランジションの都合
- フラグメントは不可)
- HTML コメントも要素扱いなので注意
-
useRoute
で Composition API でルーターアクセス可能 -
[...slug].vue
でURL内の全てのルートパラメータを補足可能-
/hello/world
ならslug
は["hello", 'world"]
に - URLに日付が入る場合に使えるとかなのかな
-
- ディレクトリ名と同じ名前のファイルがある場合、 Nested Route として使用できる
-
definePageMeta
マクロでページごとのメタ情報を定義可能- マクロであることからコンパイル時に挿入されるため、コンポーネントの情報にはアクセスできない
- メタデータは任意のキーバリューを設定できるが、デフォルトで意味のあるキーもある
- alias: ページにアクセスするURLのエイリアス
- keepalive:
true
の場合、ページ全体を自動で<KeepAlive>
でラップし、ページの状態をキャッシュする - key: 再レンダリングの条件となるキーを指定する
- layout: 使用するレイアウトコンポーネント
- layoutTransition/pageTransition: ページ遷移時の
<transition>
の使用可否 - middleware: ページ読み込み前にフックするルーターミドルウェア
- name: ページ名
- path: ページに対応するパス名 (ディレクトリベース以外の任意の指定ができる)
- アプリケーション内のリンクには
<NuxtLink>
を使用する -
navigateTo()
メソッドで動的にページ遷移も可能
plugins
- プラグイン置き場で、自動で読み込まれる
-
.server
.client
をサフィックスにつけることで、環境を制限できる - 自動で読み込まれるのはディレクトリ直下のみ
- プラグインは
defineNuxtPlugin
で定義し、Nuxt App の各ライフサイクルにフックできる - Nuxt プラグインから各種 Vue プラグインを注入することも可能
server
- サーバーサイドで実行されるコードの集合
- server/api
- server/routes
- server/middleware
utils
- ヘルパー関数置き場
- 自動インポート対象
- クライアントサイドのみで、サーバーサイドは
server/utils
を使う
.env
-
dotenv
用- Nuxt はビルトインでサポートしている
-
build
dev
generate
時に自動で読み込まれる
.gitignore
いわずもがな
.nuxtignore
- ビルド時に無視するファイルを指定する
- .gitignore と使い方は一緒
app.config.ts
- リアクティブに使用可能な設定ファイル
- バンドルに含まれる設定のため、秘匿性の高いデータは使用すべきでない
- config の入力、出力それぞれに手動で型をつけることが可能
app.vue
- エントリーになる Vue コンポーネント
- これさえあれば pages/ さえ不要
- pages/ を使用する場合は
<NuxtPage>
を含める
nuxt.config.ts
Nuxt 自体の設定ファイル
package.json
言わずもがな
tsconfig.json
- 言わずもがな
- デフォルトで
./.nuxt/tsconfig.json
を継承しており、そっちはプロジェクトの構成に応じて自動生成される

Data Fetching
Nuxt では データフェッチのためのコンポーザブルが多数提供されている
useFetch
- URL を指定するだけでデータフェッチが可能なコンポーザブル
- サーバールートAPI の URL とレスポンスの型が推論される
useLazyFetch
- useFetch + lazy
useAsyncData
- 非同期で取得するデータの定義する
- (useFetch の実態は、これと $fetch のラッパー)
useLazyAsyncData
- useAsyncData + lazy
Transforming Data
transform
オプションを指定することで、レスポンスの内容を書き換えることができる
Refreshing Data
useFetch
などの返り値に含まれる refresh
関数を使用することで、フェッチを再実行できる
refreshNuxtData
-
useFetch
などは、キーに基づいてレスポンスをキャッシュすることで、呼び出し回数を削減できる -
refreshNuxtData
を使用することで、キャッシュを使用せずに再フェッチすることを強制できる
clearNuxtData
- すべてのキャッシュを破棄する
Options API support
-
defineNuxtComponent
でコンポーネントをラップし、asyncData
フィールドを定義することで、useFetch
同等の機能を Options API でも利用できる
$fetch
directly
Using -
$fetch
で、unjs/ofetch
をビルトインで使用可能
Isomorphic
- fetch をクライアントサイドで使用する場合と、サーバーサイドで使用する場合で異なる点がある
- ブラウザクッキーを送信するのは当然クライアントサイドのみ
Best Practices
- API レスポンスが小さくなるように努めよう
- データはページごとにキャッシュされるため
-
pick
オプションを使用すると、レスポンスをサーバーサイドの時点で絞り込んでくれる
- サーバーとクライアントでの二重呼び出しを避けよう
- サーバークライアント両方で実行されるコード中で
$fetch
を使うと、API呼び出しが二回発生してしまう -
$fetch
を直接使うのでなく、useFetch
などを使用することで回避できる
- サーバークライアント両方で実行されるコード中で

State Management
-
useState
というコンポーザブルがビルトインで提供される -
useState
はref
とだいたい一緒だが、サーバサイド+クライアントサイド、及びコンポーネント間で状態を共有することが出来る -
useState
による状態はシリアライズされるため、シリアライズ可能なデータのみ使用可能
Best Practices
-
const useX = () => useState('x')
の形式で使用する -
useState
はsetup()
内でしか使用してはならない- (他で使用すると、ユーザーをまたいで共有されたりメモリリークにつながる)
Examples
コンポーザブルとしてエクスポート
export const useCounter = () => useState<number>('counter', () => 0)
export const useColor = () => useState<string>('color', () => 'pink')
各コンポーネントにバインド
<script setup>
const color = useColor() // Same as useState('color')
</script>
<template>
<p>Current color: {{ color }}</p>
</template>
Using third-party libraries
Nuxt 2 はグローバルステートのために Vuex にロックインしてたけど、Nuxt 3 はそんなことないので好きなのを使おう。
- Pinia
- Harlem
- XState

Nuxt 3 の useState, かなりラフにグローバルステート作れるけど、結局は ref のラッパーだし、ちょっと込み入ったことやドメイン知識をもたせたりする場合は素直に pinia 使うのが良いんだろうな。

Harlem, XState もちょっとは気になるけど、 pinia の時点で小規模プロダクトでも必要十分な設計だから pinia があれば充分な気がする。