Unplugin Vue Routerで小規模開発でもファイルベースルーティング

##  Unplugin Vue Routerとは
https://uvr.esm.is/

Unplugin Vue Routerとは、一言で言うとVue.jsプロジェクトにおいて型安全性の高いファイルベースルーティングを実現するためのプラグインです。

また、今回は取り上げませんがその他の機能として、実験的な[データローダー機能](https://uvr.esm.is/data-loaders/)をサポートしています。

## なぜUnplugin Vue Routerを調べてみたのか
Vue.jsのフレームワークであるNuxt.jsの主要なメリットとして、レンダリング方式の柔軟な切り替えなどと並んで、ファイルベースルーティングによる開発効率の向上がよく挙げられます。
私自身試してみたこともあり、直感的で分かりやすいと感じておりました。

しかし、私が携わるプロジェクトは比較的小規模なものが多く、Nuxt.jsの豊富な機能セットはいささか過剰であると判断し、その採用を見送ってきました。

そんな中、Nuxt.jsと同様のファイルベースルーティングを単独の機能として追加できるUnplugin Vue Routerを知り、これならば小規模プロジェクトにも適しているのでは考え、まずは試してみることにしました。

## 実際に試してみた
### セットアップ手順
実際に[リファレンス](https://uvr.esm.is/introduction.html)に沿って設定してみます。

#### プロジェクト作成
まずはcreate-viteでテンプレートを使ってさくっとプロジェクトを作成します。
筆者はVue.js+TypeScript+Viteでの作業が主なので今回はそのまま使ってます

```terminal:terminal
$ create-vite
✔ Project name: … vite-project
✔ Select a framework: › Vue
✔ Select a variant: › TypeScript

Scaffolding project in /xxx/vite-project...

Done. Now run:

  cd vite-project
  npm install
  npm run dev
```

作成されたプロジェクトで任意のパッケージマネージャを使用して、node_modulesをインストール、開発サーバーを起動してみると以下の画面が表示されるはずです。

![localhost_5173_(FHD).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1654532/b2bfd71c-757e-5f3d-6afb-c63d78e87207.png)

#### ライブラリインストール
ここで今回の主役である`vue-router`と`unplugin-vue-router`をインストールします。
```terminal:terminal
$ yarn add vue-router
$ yarn add -D unplugin-vue-router
```

#### プラグインの追加
バンドルに今回のプラグインを含めるよう設定を変更します。

```diff_typescript:vite.config.ts
import { defineConfig } from 'vite'
+import VueRouter from 'unplugin-vue-router/vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({

  plugins: [
+    VueRouter(),
    vue()
  ],
})
```

設定が変更できたらここでもう一度開発サーバーを起動してみましょう。
rootディレクトリに`typed-router.d.ts`が作成されていれば成功です。

ここまでできたら`tsconfig.json`を編集し、unplugin-vue-routerの型を追加しておきましょう。
```diff_json:tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,

+    "types": ["unplugin-vue-router/auto-routes"]
  },
- "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "./typed-router.d.ts"],
 
  "references": [{ "path": "./tsconfig.node.json" }]
}
```
ここまででセットアップは完了です。

### 基本的な使用方法
#### アプリケーションへの反映
main.tsでVue Routerの取り込みを行い、アプリケーションへ反映します。
これはいつものVue Routerと同じですね
```diff_typescript:main.ts
import { createApp } from 'vue'
import './style.css'
+import { createRouter, createWebHistory } from 'vue-router'
+import { routes } from 'vue-router/auto-routes'
import App from './App.vue'

+const router = createRouter({
+    history: createWebHistory(),
+    routes,
+  })

-createApp(App).mount('#app')
+createApp(App).use(router).mount('#app')
```
App.vueでいつものようにRouterViewを表示するように設定します。
```vue:App.vue
<script setup lang="ts">
import { RouterView } from 'vue-router'
</script>

<template>
  <RouterView />
</template>
```
これで反映の準備が整いました。

#### ファイルの配置＝ルーティング
次に`/src`配下に`pages`ディレクトリを作成し、その中に`index.vue`を作成します。
```vue:pages/index.vue
<script setup lang="ts">
import { RouterLink } from 'vue-router'
</script>
<template>
  <h1>Home</h1>
  <p>URL: {{ $route.path }}</p>
  <RouterLink to="/Hello">to Hello</RouterLink>
</template>
```
このファイルが`https://www.hogehoge.com/`の際にルーティングされるファイルです

遷移先のHello.vueも同じディレクトリに作成しておきます。
```vue:pages/Hello.vue
<script setup lang="ts">
import { RouterLink } from 'vue-router'
</script>
<template>
  <h1>Hello</h1>
  <p>URL: {{ $route.path }}</p>
  <RouterLink to="/">to Home</RouterLink>
</template>
```
作成後の配置はこのような状態です。
![スクリーンショット 2024-12-24 23.57.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1654532/cb025af8-2b69-bff2-24cd-955f47756831.png)

作成できたらこの状態でまた開発サーバーを起動してみましょう。

![Dec-25-2024 00-55-44.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1654532/eebeb1b4-d3e1-00b3-c8fc-f2174103f912.gif)


Vue Routerの設定ファイルを作成していないのにしっかり画面遷移が行われています。
これはかなり楽ですね。

#### さらに階層をネストさせたい場合
例えば先ほどのhelloの下にworldをつけて"/hello/world"で別の画面を表示したい場合は、
`pages`ディレクトリの中に`hello`ディレクトリを作成、その中に`hello.vue`を移動させ、名前を`index.vue`にすることで現在と同様の挙動を得られます。
そして、同階層に`World.vue`を先ほどまでと同じ手順で作成します。

![スクリーンショット 2024-12-25 0.43.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1654532/00acb68f-2c20-b45d-89eb-630f564a026a.png)


![Dec-25-2024 00-58-25.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1654532/2da91215-33dc-cf2f-9f55-06afd742b63b.gif)


<details>
<summary>発展：動的ルーティング</summary>

[]でファイル名を囲むことでパスのパラメータを動的に変更することができます。
pagesディレクトリ配下に[id].vueを作成してみます。
```vuejs:[id].vue
<script setup lang="ts">
import { RouterLink } from 'vue-router'
import { useRoute } from 'vue-router'

const id = useRoute().params.id
</script>
<template>
  <h1>{{ id }}</h1>
  <p>URL: {{ $route.path }}</p>
  <RouterLink to="/">to Home</RouterLink>
</template>
```

この時点ではまだ、[id].vueのパスは決まっておらず、idとして渡された値に応じてパスが動的に作成されます。
pages/index.vueに自由入力欄を設けて動的にパスを作成してみます。

```diff_vue
<script setup lang="ts">
import { RouterLink } from 'vue-router'
+import { ref } from 'vue'

+const id = ref('')
</script>
<template>
  <h1>Home</h1>
  <p>URL: {{ $route.path }}</p>
  <RouterLink to="/Hello">to Hello</RouterLink><br>
+  <input type="text" v-model="id" /><br>
+  <RouterLink :to="`/${id}`">to {{ id }}</RouterLink>
</template>
```
![スクリーンショット 2024-12-25 1.17.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1654532/0ee6dc3c-72f7-125d-be66-efeb838d792b.png)


![Dec-25-2024 01-10-25.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1654532/40c9e7e3-41b3-05ef-c025-9ecfca486e5a.gif)

</details>

## 実際に試してみて分かったUnplugin Vue Router

### 開発効率の向上
やはりファイルベースルーティングは直感的で分かりやすく、型補完が効くこともあり認知負荷が軽減されミスを最小限に抑えることができそうです。

従来のルーティング方法だとページを追加するたびに下記のようなルーティングの設定ファイルを更新していましたが、そこがなくなるのはストレスフリーですね。
```typescript:router.ts
// Components
import SignIn from "@/views/SignIn.vue";
import DashBoard from "@/views/DashBoard.vue";
import DeviceList from "@/views/DeviceList.vue";
import Schedule from "@/views/Schedule.vue";
import Alerts from "@/views/Alerts.vue";
import AlertsLog from "@/views/AlertsLog.vue";
import UserList from "@/views/UserList.vue";


// Router Rules
const routes: RouteRecordRaw[] = [
  {
    path: "/signin",
    name: "signin",
    component: SignIn,
    meta: { requiredAuth: false },
  },
  {
    path: "/dashboard",
    name: "dashboard",
    component: DashBoard,
    meta: { requiredAuth: true },
    children: [
      {
        path: "home",
        name: "home",
        component: DeviceList,
        meta: { requiredAuth: true },
      },
      {
        path: "Schedule",
        name: "Schedule",
        component: Schedule,
        meta: { requiredAuth: true },
      },
      {
        path: "Alerts",
        name: "Alerts",
        component: Alerts,
        meta: { requiredAuth: true },
      },
      {
        path: "AlertsLog",
        name: "AlertsLog",
        component: AlertsLog,
        meta: { requiredAuth: true },
      },
      {
        path: "UserList",
        name: "UserList",
        component: UserList,
        meta: { requiredAuth: true },
      },
    ],
  },
  {
    path: "/:pathMatch(.*)*",
    redirect: {
      name: "signin",
    },
  },
];
```

私のようにNuxt.jsを採用するほどでもないが、ファイルベースルーティングでケアレスミスを減らしたい方は検討してみても良いかもしれません。