Vue3 チュートリアル 5 VueRouterの導入

https://qiita.com/TakahiRoyte/private/9033b61a0e528324da4c

前回はコンポーネントを活用してページを作成していくことを試していきました。

今回はVue Routerを使って追加モードと編集モード

### ゴール

- Vueのコンポーネントを整理しわかりやすいディレクトリにする
- Vue Routerによる画面遷移の実装ができるようになる
- `<router-link>`とJavaScriptによる遷移の2パターンを知る

## Vue Routerのセットアップ

### Vue Routerのインストールと`package.json`

Vue Routerをインストールして実際に使っていってみましょう。
こういったライブラリのインストールは`npm install`コマンドで実施します。
今回は`vue-router`かつバージョンを4系を指定したいので以下のようになります。

```bash
$ npm install vue-router@4
```

インストールが完了したメッセージが流れたら、正しくインストールできたかを
`package.json`を見て確認しましょう。

```json:package.json
{
  "name": "vite-vue-project",
  "private": true,
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.2.25",
    "vue-router": "^4.0.15"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^2.3.1",
    "vite": "^2.9.7"
  }
}
```

ここではじめて`package.json`の説明をします。
このファイルは任意のnpmコマンドが`scripts`、このプロジェクトが依存しているライブラリが
`dependencies`と`devDependencies`に記載されています。
いつも`npm run dev`で開発環境を起動しているのはここに記載されている`dev`を元に
npmが`vite`コマンドを実行してくれてるからなのです。

今のように`npm install`をするとインストールしたパッケージは`dependencies`に残ります。
バージョンはコマンド実行してインストールされたものが記載されています。
`devDependencies`は開発環境でのみ必要なパッケージで、
`vite`などのビルドツールやテスティングツールなどが該当します。

この`package.json`はGitリポジトリにコミットし管理されるものなので、
開発者は依存するライブラリとそのバージョンまでも共有することができるのです。
他の開発者は個別にパッケージ名を指定しなくても、`npm install`コマンドだけで
この`dependencies`と`devDependencies`を一挙インストールできます。

### src配下のパスのalias対応

今回のチュートリアルの中でコンポーネントを色々整理して追加＆移動していくにあたり、
`./components/`や`../`など相対パスで指定したものがそのままだと動かなくなります。
一つ一つ修正していくのも手なのですが、毎回パスの内容を確認したり、
修正のたびに修正が必要になりメンテナンス性が低いです。
これを解消するためにVite側でalias（エイリアス）の設定をすることができます。
aliasにより規定となるフォルダの位置をエイリアス`~`により表現できます。

```js:MyComponent.vue
// これまで（`/src/components/sample/`からの場合）
import YourComponent from '../component/basics/YourComponent.vue'

// これから（aliasの既定パスを`src`に設定した場合）
import YourComponent from '~/components/basics/YourComponent.vue`
```

上のケースだと`~`が`src`フォルダ直下を指しているので、
この`YourComponent`をインポートしている`MyComponent`が例え`basics`の中にあろうと、
`sample`の中にあろうと移動しようと問題がなくなるわけです。
逆に`MyComponent`をインポートしているファイルが他にある場合は、
インポート先は修正が必要になりますが、それでも一括置換などで対応可能になります。
この設定はプロジェクトルートにある`vite.config.js`に追記することで実現できます。
ちなみにファイル名を指定して特定のファイルを開きたいときにはCtrl+Pが便利です。

```js:vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  resolve: {
    alias: {
      '@': resolve(__dirname, './src'),
    },
  },
  plugins: [vue()],
})
```

`vite.config.js`はこれで良いので、続いて以下のようにファイル構造を修正します。

```text
|-- assets
|   |-- icon-plus.svg
|   `-- logo.png
|-- components
|   |-- basics（色々なところで使い回せる汎用性の高いコンポーネント）
|   |   `-- MyButton.vue
|   |-- components（ページコンポーネントの中で使用するコンポーネント）
|   |   |-- TodoAdd.vue
|   |   `-- TodoList.vue
|   `-- pages（Vue Routerで切り替えるページコンポーネント）
|       |-- Edit.vue（新しい空のファイル）
|       `-- Home.vue（新しい空のファイル）
|-- App.vue
|-- main.js
`-- router.js
```

そしてすでに実装済みの`TodoAdd`でのインポート文をalias版で修正してみましょう。

```javascript
// 修正前
import MyButton from './components/basics/MyButton.vue'

// 修正後
import MyButton from '@/components/basics/MyButton.vue'
```

あわせてボタンのプラス画像の場所もこのままだと異なっているので修正します。

```html
<!-- 修正前 -->
<MyButton @click="todoAdd">追加<img src="../assets/icon-plus.svg" height="10" /></MyButton>

<!-- 修正後 -->
<MyButton @click="todoAdd">追加<img src="@/assets/icon-plus.svg" height="10" /></MyButton>
```

これだけだとイマイチ効果がわからないと思うので、続いて`App.vue`の内容を`Home.vue`に移していきます。
`App.vue`はヘッダーだけ残してあとは`Home.vue`に切り出しインポートします。

```vue:App.vue
<script>
import Home from '@/components/pages/Home.vue'
export default {
  components: { Home },
}
</script>

<template>
  <h1>My ToDo App</h1>
  <Home />
</template>

<style>
body {
  background-color: #eee;
}
</style>
```

`Home.vue`は以下のように修正します。

```vue:Home.vue
<script>
import TodoAdd from '@/components/components/TodoAdd.vue'
import TodoList from '@/components/components/TodoList.vue'

export default {
  components: {
    TodoAdd,
    TodoList,
  },
  data() {
    return {
      todos: [
        //{ isDone: false, text: 'ToDoの文字列' }
      ],
    }
  },
  methods: {
    addTodo(newTodoText) {
      if (!newTodoText) return alert('文字を入力してください')
      this.todos.push({
        isDone: false,
        text: newTodoText,
      })
    },
    clearDoneTodos() {
      this.todos = this.todos.filter((todo) => !todo.isDone)
    },
  },
}
</script>

<template>
  <TodoAdd @delete-done="clearDoneTodos" @add-todo="addTodo" />
  <p v-if="todos.length === 0">ToDoがまだありません！</p>
  <TodoList v-else :todos="todos" />
</template>

<style>
.todo-done {
  text-decoration: line-through;
}
</style>
```

Todoに関する処理は完全に切り出していることがわかるかと思います。
続いて切り替え先の`Edit.vue`もテンプレートだけ用意します。

```vue:Edit.vue
<template>
  <p>編集モード</p>
  <input type="text" name="" id="" />
</template>
```

これでVueRouterの機能を使う準備が整いました。

### `router.js`作成

`/src/router.js`を作成します。
Vue Routerの`createRouter`を利用します。

```js:router.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from './components/pages/Home.vue'
import Edit from './components/pages/Edit.vue'

const routes = [
  { path: '/', name: 'Home', component: Home },
  { path: '/edit', name: 'Edit', component: Edit },
]

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

export default router
```

このコードで何をしているかというと、`routes`でインポートしたコンポーネントに対して、
名前とパスの設定をしており、その設定を元に`createRouter`でルーティング設定を作っています。
このエクスポートしている`router`を`main.js`でインポートすることでルーティングが動作します。

```js:main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)
app.use(router)
app.mount('#app')
```

これでVue Routerは効きますが、どこでページの切り替えを行うかをまだ明記していません。
今回のような作りの場合は`App.vue`に実装することが多いです。
`App.vue`にルーティング切り替えのビューを定義する`<router-view />`を定義します。
ついでにデフォルトで`Home.vue`を表示する処理は消してしまいましょう。

```vue:App.vue
<template>
  <h1>My ToDo App</h1>
  <router-view />
</template>

<style>
body {
  background-color: #eee;
}
</style>
```

これでエラーも出ず表示が元と変わらなければ、うまくいっている証拠です。
ただ、画面がなにも変わらないので、リンクを押すと切り替わる`<router-link>`を使ってみます。

### `router-link`による遷移

`router-link`はVue Router利用時における`a`リンクに変わるカスタムコンポーネントです。
`router-link`を用いることで実際に画面をリロードすることなく内部の処理による画面遷移を実現します。
つまり`a`によるリンクはVueによるシングルページアプリケーションでは使わないようにする必要があります。

```vue:App.vue
<template>
  <router-link to="/">Home</router-link> ・
  <router-link to="/edit">Edit</router-link>
  <h1>My ToDo App</h1>
  <router-view />
</template>

<style>
body {
  background-color: #eee;
}
</style>
```

これでヘッダーテキストの上に遷移リンクが表示されました。
`to=""`の向き先は`router.js`で定義した`routes`とマッチしています。
実際のアプリでも`router-view`の前後にこのような形で画面遷移のメニューをヘッダーや、
ナビゲーションバーに用意することでどの画面でも共通で遷移メニューを表示することをすることがあります。

### `router.push`による遷移

プログラマブルに遷移の処理を任意のタイミングで実施することも出来ます。
追加したToDoに編集ボタンをつけてそこをクリックしたら編集モードに移動するように実装してみましょう。
Vueコンポーネント内部で`this.$router`によりVue Routerのインスタンスにアクセスできます。
そこから`router.push`メソッドを呼び出して、パスを渡してみましょう。

```vue:TodoList.vue
<script>
import MyButton from '../basics/MyButton.vue'

export default {
  components: {
    MyButton,
  },
  props: {
    todos: Array,
  },
  methods: {
    openEditMode(todo) {
      this.$router.push('/edit')
    },
  },
}
</script>

<template>
  <ul>
    <li v-for="todo in todos">
      <input type="checkbox" v-model="todo.isDone" /><span
        :class="{ 'todo-done': todo.isDone }"
        >{{ todo.text }}</span
      >
      <MyButton @click="openEditMode(todo)">編集</MyButton>
    </li>
  </ul>
</template>
```

この状態でHomeでTodoを追加し、新たに表示された編集ボタンを押すとEdit画面に移れるはずです。
ですがこの状態だとEdit画面には遷移できますが、Homeに戻るとデータが消えてしまいます。
このように、VueのコンポーネントはVue Routerにより切り替えられるごとにインスタンスが生成されるため、
中のデータは揮発してしまうことに注意する必要があります。

この問題を回避するためには、サーバー側でデータを保持し、Vueのインスタンス生成時に
そのデータを取得して表示するか、あるいはVueのアプリ内ですぐに揮発しないストア領域を持つ必要があります。
次回のチュートリアルでは揮発しないストア領域を提供するライブラリ`Pinia`を使って
ページ遷移してもデータが引き継がれる実装をしていきます。
また、Vue Routerの機能もより掘り下げて見ていきます。

## まとめ

Vue Routerの基本を学び、`router.js`の作り方や基本的な遷移方法を見てきました。

今回はいったんアプリを作ってから途中でVueRouterを導入しているのでファイルの移動やらが多く、
混乱しやすいかもしれませんが、一般的にはVueRouterが必要そうであれば最初からいれますし、
Viteのaliasの設定も最初から設定したほうが便利です。
もし新たなプロジェクトを始める機会があればぜひ最初から導入してみてください。