Closed22

[キャッチアップ] Nuxt 3

shingo.sasakishingo.sasaki

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

shingo.sasakishingo.sasaki

Key Concepts

https://nuxt.com/docs/guide/concepts/auto-imports

Auto importss

  • ヘルパー関数、コンポーザブル、Vue API をアプリケーション全体で自動的にインポートしてくれる仕組み
  • 自動インポートはディレクトリ構造に基づいて行われる
  • 型付けや IDE での補完は、実際にプロダクションでもインポートされて使用されるコードに対してのみ行われる

自動インポートされるモジュールの例

  • データフェッチ系
    • useAsyncData
    • $fetch
  • Vue コンポーネント系
    • ref
    • computed
  • components/ 以下にあるコンポーネント
  • composables/ 以下にあるコンポーザブル
  • utils/ 以下にあるヘルパー関数
shingo.sasakishingo.sasaki

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

shingo.sasakishingo.sasaki

Vue.js Development

https://nuxt.com/docs/guide/concepts/vuejs-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 で書き直されたため、完全な型情報の提供がされている
shingo.sasakishingo.sasaki

Rendering Modes

https://nuxt.com/docs/guide/concepts/rendering

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 } }
  }
})
shingo.sasakishingo.sasaki

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

shingo.sasakishingo.sasaki

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

shingo.sasakishingo.sasaki

Server Engine

https://nuxt.com/docs/guide/concepts/server-engine

サーバーエンジンとして Nitro を使用してる

  • クロスプラットフォーム
  • サーバーレスサポート
  • APIルート
  • コード分割、チャンク化の自動化
  • Static + serverless のハイブリット
  • HMR

https://github.com/unjs/nitro

API Layer

API レイヤー自体は h3 を使用している。
https://github.com/unjs/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 のサーバーをスタンドアローンで動かすことができるため、ビルド時の成果物を使って任意の環境でサーバーを動かすことができる。

shingo.sasakishingo.sasaki

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

shingo.sasakishingo.sasaki

h3 のハンドラが Promise を返せるってのがイマイチイメージできない。普通に await してから返してくれるって認識で良いのかな。

Handlers can return promises, which will be awaited (res.end() and next() are also supported)

だからそうっぽい。

shingo.sasakishingo.sasaki

ESM

https://nuxt.com/docs/guide/concepts/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.jsontype: 'module' が設定されており、ファイルの拡張子が .js である
    • ファイルの拡張子が .mjs である (推奨)
  • よって Nuxt はサーバーコードを生成する際に .mjs ファイルを生成する

What Are Valid Imports in a Node.js Context?

Node.js が ESM (import) を用いてモジュール解決をする場合、 package.jsonexports または 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 のみを提供する
shingo.sasakishingo.sasaki

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

shingo.sasakishingo.sasaki

TypeScript

https://nuxt.com/docs/guide/concepts/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 とは違うらしい。

shingo.sasakishingo.sasaki

Configuration

https://nuxt.com/docs/getting-started/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
  • ...
shingo.sasakishingo.sasaki

Directory Structure

先にざっと見ておこ

https://nuxt.com/docs/guide/directory-structure/nuxt

.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 を継承しており、そっちはプロジェクトの構成に応じて自動生成される
shingo.sasakishingo.sasaki

Data Fetching

Nuxt では データフェッチのためのコンポーザブルが多数提供されている

https://nuxt.com/docs/getting-started/data-fetching

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 でも利用できる

Using $fetch directly

  • $fetch で、unjs/ofetch をビルトインで使用可能

Isomorphic

  • fetch をクライアントサイドで使用する場合と、サーバーサイドで使用する場合で異なる点がある
  • ブラウザクッキーを送信するのは当然クライアントサイドのみ

Best Practices

  • API レスポンスが小さくなるように努めよう
    • データはページごとにキャッシュされるため
    • pick オプションを使用すると、レスポンスをサーバーサイドの時点で絞り込んでくれる
  • サーバーとクライアントでの二重呼び出しを避けよう
    • サーバークライアント両方で実行されるコード中で $fetch を使うと、API呼び出しが二回発生してしまう
    • $fetch を直接使うのでなく、 useFetch などを使用することで回避できる
shingo.sasakishingo.sasaki

State Management

  • useState というコンポーザブルがビルトインで提供される
  • useStateref とだいたい一緒だが、サーバサイド+クライアントサイド、及びコンポーネント間で状態を共有することが出来る
  • useState による状態はシリアライズされるため、シリアライズ可能なデータのみ使用可能

https://nuxt.com/docs/getting-started/state-management

Best Practices

  • const useX = () => useState('x') の形式で使用する
  • useStatesetup() 内でしか使用してはならない
    • (他で使用すると、ユーザーをまたいで共有されたりメモリリークにつながる)

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
shingo.sasakishingo.sasaki

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

shingo.sasakishingo.sasaki

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

このスクラップは2023/06/19にクローズされました