🎢

Nuxt 3 × Vue Router の主要な機能をまとめてみた

2024/12/08に公開

この記事は ANDPAD Advent Calendar 2024 8 日目の記事です。

内容としては、今年10月に行われた「Vue Fes Japan 2024」にて登壇した際の発表内容を、記事向けにまとめ直したものとなります。

元々、どちらかというと喋るより書く方が得意ですし、記事として残っていた方が良い内容でもあると思っていたのですが、

登壇を終えてスライドを上げたあたりで燃え尽きていたところ、アドベントカレンダーが会社テックブログでなくてもOKということだったので、参加させて頂きました。

登壇資料も上がっていますので、読みやすいスタイルで見て頂けたらありがたいです!

はじめに

Nuxt 3 のリリースから2年ほど経ち、利用者も増えてきていると思いますが、ルーティング周りの機能は Vue Router という別のライブラリの機能となっているため、Nuxt の公式サイトでも詳しく掘り下げられていません。

しかも Vue Router デフォルトの機能と、Nuxt 側で拡張している機能が混在しているので、「何ができるのか」をまとめている公式の資料が意外と少なかったりします。

例えば、React 界隈では一般的になってきた「Nested Layout」が Nuxt でも使えることや、「Typed Page」がデフォルトで使えることなど、私自身が公式ドキュメントを読んでいる中で「こんなこともできるのか」と驚いた機能を、存在だけでも知ってもらうための記事となります。

この記事では、Nuxt と Vue Router がデフォルトでサポートしている強力な機能をさわりだけでも一通り紹介することで、Nuxt と Vue Router を最大限活用するための助けになればと思います。

説明としてはやや駆け足ですので、気になった項目についてはぜひ各項でリンクしている公式ドキュメントの該当ページもご参照ください!

Dynamic Routes & Catch-All Route

まずは基本となる、pages 内のファイル名やディレクトリ名を [] で囲むことで、動的なルートを作成できる機能です。

https://nuxt.com/docs/guide/directory-structure/pages#dynamic-routes

/users/[user_id]/posts/[post_id].vue のように書くことで、ブラケット部分が任意の文字列にマッチする、というのが基本的な使い方です。

これは Nuxt 2 からある、ルーティングの基本とも言える機能なので、使ったことがない方はほとんどいないでしょうが、Nuxt 3 で _id.vue から [id].vue にファイル名のルールが変わったことで、さらに柔軟な設定を行えるようになりました。

例えば、 /pages/user-[id].vue と、ファイル名の一部のみをブラケットで囲むことで、動的ルート部分の前後にスラッシュ以外の文字が使えるようになっています。

また、/user/[[id]].vue のように二重に囲むと、そのパスが省略可能になり、この例では /user//user/1 の両方にマッチするようになります。

さらに、[...id].vue というように三点ドットを付けると、スラッシュで区切られた配下のパスに全てマッチする、 Catch-All Route という機能もあります。

この場合、図のように route.paramsstring[] 型の配列で返却されます。

これらの仕様は Next.js とほぼ同じなので、Next から入る方にとってもわかりやすくなっています。

Route Groups (v3.13~)

今年8月にリリースされた v3.13 以降では、 Route Groups という機能も追加されました。

https://nuxt.com/docs/guide/directory-structure/pages#route-groups

こちらは、ディレクトリ名を () で囲むことで、URLに含まれないセグメントを作成できるようになります。


-| pages/
---| (backstage)/
-----| dashboard.vue
---| (portal)/
-----| login.vue

例えば、上記のようなファイル構造にした場合に、 /pages/(backstage)/dashboard.vue/dashboard/ にマッチして、丸括弧内はURL上は無視されるようになります。

サイトの仕様には影響を与えない、pages ディレクトリを綺麗に整理するための機能です。

逆に言うと、同じルートに一致するページコンポーネントを複数作れてしまう点は注意が必要です。

Nested Routes

Nested Routes は、ディレクトリと同じ名前の Vue ファイルを作成することで、URL構造に合わせて入れ子構造でコンポーネントをネストするようになる機能です。

一般的な Nuxt アプリケーションでは、共通部分を layouts (または app.vue)、ページごとのコンポーネントを pages に置く、という組み合わせで管理しますが、 Nested Routes を使うことで、この間のレイヤーを増やせる、というイメージです。

https://router.vuejs.org/guide/essentials/nested-routes.html

ここでは Vue Router のドキュメントに掲載されている図を引用します。

/user/johnny/profile                     /user/johnny/posts
+------------------+                  +-----------------+
| User             |                  | User            |
| +--------------+ |                  | +-------------+ |
| | Profile      | |  +------------>  | | Posts       | |
| |              | |                  | |             | |
| +--------------+ |                  | +-------------+ |
+------------------+                  +-----------------+

「サイト全体で共通のヘッダーと、user_id 配下のみに共通するサイドバーを表示して、ユーザー詳細と記事一覧でコンテンツは別にする」というように、ページURLの構造と画面レイアウトの構造を一致して管理できます。

Nuxt 3 での Nested Routes

Nuxt 3 での Nested Routes の設定方法については、pages ディレクトリの解説ページにも使い方が軽く触れられています。

https://nuxt.com/docs/guide/directory-structure/pages#nested-routes

使い方はとてもシンプルで、ディレクトリパスと同じ名前の Vue ファイルを作成して同じ階層に配置することです。(TSXやJSXでも同じです)

-| pages/
---| index.vue
---| red.vue
---| red/
-----| blue.vue
-----| blue/
-------| green.vue
-----| [color]/
-------| [color].vue

この例だと、red ディレクトリと red.vue 、blue ディレクトリと blue.vue がそれぞれ対応していて、 /red/blue のようなURLにアクセスすると各ページコンポーネントが入れ子状に描画されます。

この状態で /red/ ディレクトリのルートページを別にしたい場合は、 /red/index.vue と、ディレクトリ配下に index.vue を作ることで実現できます。

動的ルートでも同じ考え方で組み合わせることができます。

一見ややこしそうに見えますが、実際には「ディレクトリ名とファイル名が重複していたら Nested Layout用のページファイル」というだけなので、慣れれば割と直感的です。

Nested Routes の先行事例

余談ですが、Nested Routes は、どちらかというと React のフレームワークである Next.js や Remix で聞いたことがある人の方が多いのではないかと思います。

そのため、Nested Routes をどのように活用すれば良いのか、という考え方を理解する場合、Next や Remix の先行事例の方が充実していたりします。

例えば、Next.js 公式ドキュメントには以下のような Nested Routes 概念のわかりやすい図が掲載されています。

アプリケーションでの具体的な活用例についても、Nuxt に限定せずに検索した方が参考になるかもしれません。

Typed Pages (v3.5~)

Typed Pages は v3.5 で追加された、Vue Router のページ遷移や動的パラメータで型サポートを受ける機能です。

https://nuxt.com/blog/v3-5#fully-typed-pages

こちらは Nuxt Config で有効化する必要があります。

export default defineNuxtConfig({
  experimental: {
    typedPages: true
  }
})

<NuxtLink to="path" />useRouter().push('path') などで遷移先を指定する際に、存在するパスを自動で補完し、存在しないページを指定するとタイプエラーを出してくれます。

useRoute() の場合は、引数にパスを指定することで、動的パラメータなどの型情報を取得できるようになります。

こちらは現在 experimental となっていますが、既に導入から時間が経ってある程度安定してきている印象です。

詳しくは別の記事に書いていますので興味のある方はぜひそちらをお読みください!

https://zenn.dev/ytr0903/articles/8444a0a2d1bf22

Routes' Matching Syntax

最後に紹介する Routes' Matching Syntax は、ここまで説明してきた機能だけだとどうしても難しい、ちょっと入り組んだルーティングを実現するための高度な機能です。

例えば、/users/:user_id(\\d+) のように書くと、正規表現で user_id が数字の場合のみマッチする、というように、動的ルートを特定の文字列のみに制限したり、複数のページパスを同じルートにマッチさせたり、ということができます。

Nuxt ではなく素の Vue Router の指定を自分で書くことになるので、動的パスはブラケットではなくコロンを使うなど、少し書き方が異なります。

詳しくはVue Router の公式ドキュメントをご参照ください。

https://router.vuejs.org/guide/essentials/route-matching-syntax.html

使い方①ページコンポーネントに definePageMeta() を記述する

Nuxt での使い方は大きく分けて2種類あり、1つは definePageMeta を使う方法です。

https://nuxt.com/docs/api/utils/define-page-meta#using-a-custom-regular-expression

// pages/[postId]-[postSlug].vue
definePageMeta({
  path: '/:postId(\\d+)-:postSlug' 
})

このように書くと、postId が数字である場合のみマッチする、つまり /123-new/ にはマッチするが、 /abc-new/ にはマッチしなくなります。

path を上書きするので、ファイル名に基づくルーティングは完全に無視されますが、Nested Routes の影響は受けることに注意が必要です。

とはいえ、影響を受けたくなければページ自体を別の場所に移動すれば良いだけなので、便利な場面の方が多いと思います。

使い方② nuxt.config.tspages:extend hook を使う

もう1つは、Nuxt Config の hooks を利用して上書きする方法です。こちらはさらに柔軟性が高く、pages に置いていないコンポーネントにもマッチさせることができます。

export default defineNuxtConfig({
  hooks: {
    'pages:extend'(routes) {
      routes.push({
        path: '/:postId(\\d+)-:postSlug' 
        file: '~/pages/red/blue/[postId]-[postSlug].vue',
      })
    }
  }
})

また、こちらは上書きではなくルートの追加なので、元のファイルベースのルーティングも有効のままです。

上記の例でいえば、このフックで指定している postId-postslug のパスと、元々のページコンポーネントに基づいた red-blue-postId-postSlug パスの両方にマッチします。

Typed Pages と組み合わせる

①②どちらの使い方でも、Typed Pages とも問題なく組み合わせることができ、型推論や動的パス名の補完も受けられます。ただし、正規表現でパスの文字列を限定している場合も string 型になります。

この時、 useRoute() の引数などに使うルートの名前は、 definePageMeta() ではファイル名、 pages:extend では path 文字列がデフォルトで使われますが、name プロパティを指定して、好きな名前に上書きすることもできます。

Path Ranker (Vue Router Path Parser)

さて、ここまでの機能、特に Routes Matching Syntax を見ていて、実際に期待する動作になっているかを確認するのが大変そうだなと思った方もいるかもしれません。

そんな方にオススメなのが Path Ranker というツールです。

https://paths.esm.dev/

指定した正規表現と実際のURLを渡すことで、どのページにマッチするかを検証して、優先度も確認できるようになっています。

パスの優先度をスコアで表示してくれるので、どのパスにマッチするかが一目でわかります。

pages/ ディレクトリ内に配置している、自分で正規表現を書いていないパスの場合はどうかというと、こちらも内部的には Nuxt が正規表現に変換しているため、その中身を Path Ranker に渡すことでチェックができます。

どう変換されているかは typedPages をONにしていれば /.nuxt/types/typed-router.d.ts を開いて確認できるので、それを活用しましょう。

  /**
   * Route name map generated by unplugin-vue-router
   */
  export interface RouteNamedMap {
    'index': RouteRecordInfo<'index', '/:page_route(page)?/:page(d+)?/', { page_route?: ParamValueZeroOrOne<true>, page?: ParamValueZeroOrOne<true> }, { page_route?: ParamValueZeroOrOne<false>, page?: ParamValueZeroOrOne<false> }>,
    'category-slug': RouteRecordInfo<'category-slug', '/category/:slug/page/:page(\d+)', { slug: ParamValue<true>, page: ParamValue<true> }, { slug: ParamValue<false>, page: ParamValue<false> }>,
    'date-dates': RouteRecordInfo<'date-dates', '/date/:dates(.*)*', { dates?: ParamValueZeroOrMore<true> }, { dates?: ParamValueZeroOrMore<false> }>,
    'red': RouteRecordInfo<'red', '/red', Record<never, never>, Record<never, never>>,
    'red-blue': RouteRecordInfo<'red-blue', '/red/blue', Record<never, never>, Record<never, never>>,
    'my-favorite-name': RouteRecordInfo<'my-favorite-name', '/:postId(\d+)-:postSlug()?', { postId: ParamValue<true>, postSlug?: ParamValueZeroOrOne<true> }, { postId: ParamValue<false>, postSlug?: ParamValueZeroOrOne<false> }>,
    'red-blue-green': RouteRecordInfo<'red-blue-green', '/red/blue/green', Record<never, never>, Record<never, never>>,
  }

おわりに

今回紹介したのはあくまでさわりの部分であり、今後のアップデートで変更されることもあると思いますので、ぜひ公式ドキュメントを読んでみて、実際に触って試してみるのがオススメです。

Vue Router と Nuxt のルーティング周りは、かなり細かいところまでカバーしているツールなので、まずは標準機能だけで結構いろいろできるということ自体を覚えておくと、何かルーティング周りの困りごとが発生した際に「まず公式ドキュメントを読んでみよう」という気になるかもしれません。

この記事が Vue Router と Nuxt のルーティングの理解の助けになれば幸いです!

Discussion