前回はコンポーネントを活用してページを作成していくことを試していきました。
今回はVue Routerを使って追加モードと編集モード
ゴール
- Vueのコンポーネントを整理しわかりやすいディレクトリにする
- Vue Routerによる画面遷移の実装ができるようになる
-
<router-link>とJavaScriptによる遷移の2パターンを知る
Vue Routerのセットアップ
Vue Routerのインストールとpackage.json
Vue Routerをインストールして実際に使っていってみましょう。
こういったライブラリのインストールはnpm installコマンドで実施します。
今回はvue-routerかつバージョンを4系を指定したいので以下のようになります。
$ npm install vue-router@4
インストールが完了したメッセージが流れたら、正しくインストールできたかを
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により規定となるフォルダの位置をエイリアス~により表現できます。
// これまで(`/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が便利です。
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はこれで良いので、続いて以下のようにファイル構造を修正します。
|-- 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版で修正してみましょう。
// 修正前
import MyButton from './components/basics/MyButton.vue'
// 修正後
import MyButton from '@/components/basics/MyButton.vue'
あわせてボタンのプラス画像の場所もこのままだと異なっているので修正します。
<!-- 修正前 -->
<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に切り出しインポートします。
<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は以下のように修正します。
<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もテンプレートだけ用意します。
<template>
<p>編集モード</p>
<input type="text" name="" id="" />
</template>
これでVueRouterの機能を使う準備が整いました。
router.js作成
/src/router.jsを作成します。
Vue RouterのcreateRouterを利用します。
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でインポートすることでルーティングが動作します。
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を表示する処理は消してしまいましょう。
<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によるシングルページアプリケーションでは使わないようにする必要があります。
<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メソッドを呼び出して、パスを渡してみましょう。
<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の設定も最初から設定したほうが便利です。
もし新たなプロジェクトを始める機会があればぜひ最初から導入してみてください。