主旨
vuetify3 と vue-router を同時に使うときのメモです。Vue3 を使います。
問題
yarn create vuetify
で vue-router を使用する設定 (base) を使ってプロジェクトの初期化を行うと、src/layouts
というディレクトリが作成されます。具体的には、下記のようにしてプロジェクトの初期化をした場合です。
$ yarn create vuetify
....
✔ Project name: … vue-router-vuetify3
✔ Which preset would you like to install? › Base (Vuetify, VueRouter)
✔ Use TypeScript? … No / Yes
✔ Would you like to install dependencies with yarn, npm, pnpm, or bun? › yarn
src/layouts
以下に追加されるコードは、Nuxt.js の layouts
っぽい機能を実現できるようにするためのものです(多分)。src/layouts
以下のファイルを使うと、これまでの書き方に比べてページごとに異なるレイアウトを適用しやすくなります。
とはいえ、vuetify2 時代の書き方と変わる部分もあるので、これまでの書き方を踏襲したいということもあるかと思います。
そこで、以下では「従来に近い書き方をする方法」と「layouts
を使う方法」の両方についてメモしておきます。
自力で後から vue-router を入れて vue2 時代っぽく書く
Vuetify2/Vue2 の時代に自力で yarn add vuetify vue-router
などとしていた場合は、この方法が一番近い感じになります(多分)。
$ yarn create vuetify
...
✔ Project name: … legacy-vuerouter-vuetify3
✔ Which preset would you like to install? › Default (Vuetify)
✔ Use TypeScript? … No / Yes
✔ Would you like to install dependencies with yarn, npm, pnpm, or bun? › yarn
上記のように vue-router を含めないようにプロジェクトを作成します。次に、プロジェクトのディレクトリに入って yarn add vue-router
します。
$ cd legacy-vuerouter-vuetify3
$ yarn add vue-router
この方法で vue-router
を追加すると、vue-router
のモジュール自体はプロジェクトに追加はされますが、vue-router
を使うためのコードは自力で追加する必要があります。
具体的には、下記のファイルの変数および作成をします。
-
App.vue
: 編集 -
main.js
: 変数 -
src/router/index.js
: 作成 -
src/view/Home.vue
: 作成
最終的なファイル構成は下記のようになります。
$ tree src
src
├── App.vue
├── main.js
├── plugins
│ ├── index.js
│ └── vuetify.js
├── router
│ └── index.js
└── views
└── Home.vue
src/App.vue
繊維先のページを展開する <router-vue/>
要素をページ内に追加します。
<template>
<v-app>
<v-main>
<router-view />
</v-main>
</v-app>
</template>
<script setup>
</script>
src/main.js
// Plugins
import { registerPlugins } from '@/plugins'
import router from './router' // 追加
// Components
import App from './App.vue'
// Composables
import { createApp } from 'vue'
const app = createApp(App).use(router) // 追加
registerPlugins(app)
app.mount('#app')
vue-router
を読み込むコードと、アプリ内で使用するためのコードを追加します。上記のコードは、Vuetify2 時代のコードにやファイル構成近づけるようにわざと 'router/index.jsを読むようにしていますが、後述するように
plugin以下においた
vue-router` の初期化用のコードを読み出すようにすることもできます(その方法が今後のスタンダードになりそうです)。
src/router/index.js
vue-router
モジュールの初期化を行うコードを書きます。以下では、/
に遷移したときに、../views/Home.vue
を読み込んで <router-view/>
の要素の位置に展開するように設定しています。
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/Home.vue'
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
src/views/Home.vue
/
に遷移したときに表示されるページの内容を記述します。下のコードは、Hello! というテキストが書かれたボタンがひとつあるページになっています。
<template>
<div>
<v-btn color="primary">
Hello!
</v-btn>
</div>
</template>
テスト
$ yarn dev
ブラウザで http://localhost:3000
にアクセスして、下記のような画面が出たら成功です。
/about
のページを追加
繊維先のいページを夜やしたい時は、src/router/index.js
に繊維先のページの情報を追加し、実際に遷移したときに表示するページ内容を記述した .vue
ファイルを追加します。
$ tree src
src
├── App.vue
├── main.js
├── plugins
│ ├── index.js
│ └── vuetify.js
├── router
│ └── index.js
└── views
├── About.js
└── Home.vue
ファイル構成は上記のようになります。
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/Home.vue'
import AboutView from '../views/About.vue'
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
component: AboutView
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
/about
の繊維先のページには、ABOUT! と書かれたボタンをひとつ配置してみます。
<template>
<div>
<v-btn color="secondary">
About!
</v-btn>
</div>
</template>
上記のようにファイルの書き換え、追加をしてから yarn dev
とすると、http://localhost:3000/about` をブラウザで開いた時に下記の画面が表示されます。
ボタンを押したらページ遷移する
/
と /about
のページの間で、相互にページ遷移させる例も書いておきます。遷移の方法自体は、Vuetify とは関係なく vue-router
の一般的な方法をそのまま使います。
<template>
<v-container>
<p class="text-h5 my-2">ここは / です</p>
<div>
<v-btn color="primary" @click="$router.push('/about')">
GO TO ABOUT
</v-btn>
</div>
<router-link class="text-h6" to="/about">To About</router-link> |
</v-container>
</template>
<router-link>
を使うと、ページ遷移のためのアンカーを設置できます。ただしアンカー扱いにあるので、スタイルはアンカー(またはテキスト)のものが適用されます。
ボタンなどのコンポーネントを操作したときに遷移させる場合は、$router.push()
を使います。Vuetify のコンポーネントを使って遷移させたい場合は、こちらの方法のほうが適しています。
<template>
<v-container>
<p class="text-h5 my-2">ここは /about です</p>
<div>
<v-btn color="secondary" @click="$router.push('/')">
GO TO HOME
</v-btn>
</div>
<router-link class="text-h6" to="/">To Home</router-link> |
</v-container>
</template>
上記のようにコードを変更すると、/
と /about
のページとの間で、相互に遷移できるようになります。
ページ遷移しても変化しない固定レイアウト(Application Barなど)部分の記述
Application Bar や Nativation Drawer などの固定レイアウト部分は、src/App.vue
の中に記述します。
<template>
<v-app>
<v-app-bar color="primary">
<v-app-bar-nav-icon
variant="text"
@click.stop="drawer = !drawer">
</v-app-bar-nav-icon>
<v-app-bar-title>
Application
</v-app-bar-title>
</v-app-bar>
<v-navigation-drawer
v-model="drawer"
>
<v-list>
<v-list-item title="Home" value="home" @click="$router.push('/')"></v-list-item>
<v-list-item title="About" value="about" @click="$router.push('/about')"></v-list-item>
</v-list>
</v-navigation-drawer>
<v-main>
<router-view />
</v-main>
<v-footer app color="primary">
超Lチカ団 (c) 2023
</v-footer>
</v-app>
</template>
<script setup>
import {ref} from 'vue';
const drawer = ref(false);
</script>
ページ遷移したときに表示を変更したい部分に <router-view />
を設置します。
layouts
を使わない場合は、このように App.vue
の中に固定レイアウトの部分を直接書き込みます。この方法は、遷移によって変更される部分が <router-view />
の位置と一致しているので、ページの構成が直感的に把握しやすいです。
反面、遷移するページによって固定レイアウトの部分に変更を加えたいときは多少の工夫が必要になります。
src/plugins
に vue-router の初期化コードを書く
yarn create vuetify
としてプロジェクトを初期化すると、vuetify 関連の初期化コードは src/plugins
内に配置されます。この作法に従って vue-router 関連のコードも src/plugins
内に配置するには src/plugins/router.js
に従来の src/router/index.js
に相当するコードを書き、src/plugins/index.js
で app.use(router)
とします。
このようにすると、プラグイン関連のコードは src/plugins
内のコードを編集するだけでよくなり、src/main.js
を変更しなくてよくなるため、全体のコードの見通しが良くなります(バグが減る)。
src
├── App.vue
├── main.js
├── plugins
│ ├── index.js
│ ├── router.js
│ └── vuetify.js
└── views
├── About.vue
└── Home.vue
ファイル構成は上記のようになります。
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/Home.vue'
import AboutView from '../views/About.vue'
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
component: AboutView
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
src/plugins/router.js
に書くコードは、従来の router/index.js
の中身と全く同じです。
import vuetify from './vuetify'
import router from './router'
export function registerPlugins (app) {
app.use(vuetify)
app.use(router)
}
src/plugins/index.js
の中で router.js
を読み込んで app.use(router)
として vue-router を初期化するようにコードを書き換えます。このようにした場合、src/main.js
は yarn create vuetify
したときのままでOKです(編集の必要はありません)。
// Plugins
import { registerPlugins } from '@/plugins'
// Components
import App from './App.vue'
// Composables
import { createApp } from 'vue'
const app = createApp(App)
registerPlugins(app)
app.mount('#app')
同様にして webfontloader
のような初期化が必要なプログインを追加するときも、src/plugins
に初期化のコードを追加するようにすることで、プラグイン関連のコードが src/plugins
にまとまり、コード全体の見通しが良くなります。
src/layouts
に Application Bar などの固定レイアウト部分を記述する方法
yarn create vuetify
としたときに、VueRouter を使う設定 (Base (Vuetify, VueRouter)) を使って初期化すると、src/layouts
というディレクトリが作成されます。具体的には、下記のようにして初期化した場合です。
$ yarn create vuetify
....
✔ Project name: … vue-router-vuetify3
✔ Which preset would you like to install? › Base (Vuetify, VueRouter)
✔ Use TypeScript? … No / Yes
✔ Would you like to install dependencies with yarn, npm, pnpm, or bun? › yarn
以下は、この作法に従って、繊維先のページと固定レウアウトの部分を記述する方法についてのメモです。ルートのページ /
を表示したときの見かけが、下記のようになるようにファイルを変数、追加していきます。
このページで "GO TO ABOUT" と書かれたボタンをクリックするか、"To About" のアンカーをクリックすることで、/about
に遷移するようにします。
/about
のページの見かけは下記のようにします。"GO TO HOME" と書かれたボタンをクリックするか、"To Home" のアンカーをクリックすることで、/
に遷移するようにします。
また、Navigation Drawer を使って各ページへ遷移できるようにもします。
サイトの基本的なページ構成は「自力で後から vue-router を入れて vue2 時代っぽく書く」の項目で書いたものと同一になるようにしています。
ファイル構成
ファイル構成は下記のようになります。プロジェクト初期化時に存在していないファイルには # 追加
というコメントを入れています。
$ tree bash
src
├── App.vue
├── layouts
│ └── default
│ ├── AppBar.vue # 編集
│ ├── Default.vue # 編集
│ ├── Footer.vue # 追加
│ └── View.vue # 編集
├── main.js
├── plugins
│ ├── index.js
│ └── vuetify.js
├── router
│ └── index.js # 編集
└── views
├── About.js # 追加
└── Home.vue # 追加
ざっくりした説明
src/App.vue
に固定レウアウト部分を記述するのではなく、ページ全体のレイアウトを src/layouts
以下にまとめて書きます。ページのレイアウトを layouts
以下に作ったディレクトリ毎にまとめて書いておき、表示する側のページでレイアウトを読み込んで適用するというイメージです。
layouts
以下のファイルの読み込みは、vue-router の設定ファイル router/index.js
の中に書きます。
src/router/index.js
component: () => import('@/layouts/default/Default.vue')
というコードで、layouts/default/Default.vue
をコンポーネントとして読み込んでいます。読み込んだコンポーネントは、path: '/'
以下の全てのページ(子ページ、孫ページ)で、最上位のコンポーネントとして使われます。
// Composables
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
component: () => import('@/layouts/default/Default.vue'),
children: [
{
path: '',
name: 'Home',
component: () => import('@/views/Home.vue'),
},
{
path: 'about',
name: 'About',
component: () => import('@/views/About.vue'),
},
],
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
})
export default router
src/App.vue
<template>
<router-view />
</template>
<script setup>
</script>
src/App.vue
の中身は非常にシンプルです。<router-view/>
の部分が、遷移先のページの内容に置き換えて表示されます。ただし、前記のsrc/router/index.js
では /
以下の全てのページで src/layouts/default/Default.vue
を読むように指定されているため、<router-view/>
の部分は常にsrc/layouts/default/Default.vue
の内容で置き換えられることになります。
src/layouts/default/Default.vue
このコンポーネントが、すべてのページの最上位コンポーネントとして使われます。このコンポーネントの中で、Application Bar と Navigation Drawer を配置している AppBar.vue
と、遷移先のページを表示するための View.vue
、Footer を配置している Footer.vue
をそれぞれ読み込み、ページ内に配置しています。
<template>
<v-app>
<default-bar />
<default-view />
<default-footer />
</v-app>
</template>
<script setup>
import DefaultBar from './AppBar.vue'
import DefaultView from './View.vue'
import DefaultFooter from './Footer.vue'
</script>
src/layouts/default/View.vue
このファイルは、'/' 以下の個々のページの内容を表示させるためのものです。
<template>
<v-main>
<router-view />
</v-main>
</template>
<script setup>
//
</script>
<router-view />
の部分には、router/index.js
で /
のパスの children
で定義されている各パスに対応する components が読み込まれて置き換えられます。
const routes = [
{
path: '/',
component: () => import('@/layouts/default/Default.vue'),
children: [
{
path: '',
component: () => import('@/views/Home.vue'),
},
{
path: 'about',
component: () => import('@/views/About.vue'),
},
]
}
]
上記のように src/router/index.js
が定義されていると、vue-router は以下のような処理を行います。
-
App.vue
内にある<router-view/>
はconst routes
の最上位の component で置き換えられます。上記のコードの場合、最上位にはpath: '/'
だけが定義されており、その中で@/layouts/default/Default.vue
を読み込んでいるので、 常にDefault.vue
の内容がAue.vue
の<router-view/>
の部分に読み込まれることになります。 -
Default.vue
内にある<router-view/>
は、コンポーネントが読み込まれたパスの子として定義されているパス内の component に置き換えられます。上記のコードの場合、""
とabout
という二つのパスが定義されており、これらは実質的に/
と/about
というパス名として機能します。/
に遷移したときは<router-view/>
はviews/Home.vue
に置き換えられ、/about
に遷移したときは<router-view/>
はviews/About.vue
に置き換えられるという動作になります。
このような構造を利用することで、パスごとにレイアウトを統一したり、上位のページのレイアウトを下位のページで継承するような使い方ができます。
src/layouts/default/AppBar.vue
固定レイアウトの Application Bar と Navigation Drawers を配置するためのファイルです。Default.vue
から読み込まれます。
Navigation Drawer は /
と /about
に遷移できるメニュー(リスト)を含んでいます。
<template>
<v-app-bar color="primary">
<v-app-bar-nav-icon
variant="text"
@click.stop="drawer = !drawer">
</v-app-bar-nav-icon>
<v-app-bar-title>
Application
</v-app-bar-title>
</v-app-bar>
<v-navigation-drawer
v-model="drawer"
>
<v-list>
<v-list-item title="Home" value="home" @click="$router.push('/')"></v-list-item>
<v-list-item title="About" value="about" @click="$router.push('/about')"></v-list-item>
</v-list>
</v-navigation-drawer>
</template>
<script setup>
import {ref} from 'vue';
const drawer = ref(false);
</script>
src/layouts/default/Footer.vue
固定レイアウトの Footer を配置するためのファイルです。Default.vue
から読み込まれます。
<template>
<v-footer app color="primary">
超Lチカ団 (c) 2023
</v-footer>
</template>
<script setup>
//
</script>
src/views/Home.vue
/
に遷移したときに表示するページの内容です。/about
へ遷移するためのボタンとアンカーを設置しています。
<template>
<v-container>
<p class="text-h5 my-2">ここは / です</p>
<div>
<v-btn color="primary" @click="$router.push('/about')">
GO TO ABOUT
</v-btn>
</div>
<router-link class="text-h6" to="/about">To About</router-link> |
</v-container>
</template>
src/views/About.vue
/about
に遷移したときに表示するページの内容です。/
へ遷移するためのボタンとアンカーを設置しています。
<template>
<v-container>
<p class="text-h5 my-2">ここは / です</p>
<div>
<v-btn color="primary" @click="$router.push('/about')">
GO TO ABOUT
</v-btn>
</div>
<router-link class="text-h6" to="/about">To About</router-link> |
</v-container>
</template>
動作テスト
以上のようにファイルの編集、追加を行なってから yarn dev
します。http://localhost:3000
をブラウザで開くと、下記のようなページが表示されるはずです。
ページ遷移したときのステータスの保持について
App.vue
に Application Bar などを配置して、<router-view/>
で遷移先のページを表示するという構造の場合は、ページ遷移がおきても App.vue
内に Application Bar が記述されています。このため、Application Bar で保持したいステータスがある場合でも、ページ遷移にかかわらずそのステータス保持されるだろうという期待ができます。
実際、App.vue
を下記のように書いておくと、どのページに遷移しても counter
の値は保持されます(layouts
を使わない場合)。
<template>
<v-app>
<v-app-bar color="primary">
<v-app-bar-title>
Application
</v-app-bar-title>
<template v-slot:append>
<v-btn @click="counter++">
Increse
</v-btn>
<v-btn variant="text">
{{counter}}
</v-btn>
</template>
</v-app-bar>
<v-main>
<router-view />
</v-main>
</v-app>
</template>
<script setup>
import {ref} from 'vue';
const counter = ref(0);
</script>
一方、layouts
を使う場合は、ページ遷移するたびに <router-view/>
の置き換えが行われ、そのたびに AppBar.vue
が読み込まれる構造になっているので、AppBar.vue
内で保持したいステータスがあったときに、それが保持されるかについては若干不安があります。
<template>
<v-app-bar color="primary">
<v-app-bar-title>
Application
</v-app-bar-title>
<template v-slot:append>
<v-btn @click="counter++">
Increse
</v-btn>
<v-btn variant="text">
{{counter}}
</v-btn>
</template>
</v-app-bar>
</template>
<script setup>
import {ref} from 'vue';
const counter = ref(0);
</script>
結論から言えば、ステータスは保持されます。しかし、これは src/router/index.js
が下記のように '/' 以下のパスへの遷移するときに、共通のコンポーネントとして layouts/default/Default.vue
を読み込む構造になっているから、Default.vue の中で読み込まれている AppBar.vue
のステータスが保持されているでけなのでは?という疑問もわきます。
const routes = [
{
path: '/',
component: () => import('@/layouts/default/Default.vue'),
children: [
{
path: '',
component: () => import('@/views/Home.vue'),
},
{
path: 'about',
component: () => import('@/views/About.vue'),
},
]
}
]
そこで、src/router/index.js
を書き換えて、'/' と '/about' で別々に layouts/default/Default.vue
を読み込むようにしてみます。このようにすると、'/' と '/about' で読み込まれる Default.vue
は別のモジュール扱いになって、'/' と '/about' との間で相互遷移させるとカウンターの値は保持されなくなりそうです。
const routes = [
{
path: '/',
component: () => import('@/layouts/default/Default.vue'),
children: [
{
path: '',
name: 'Home',
component: () => import('@/views/Home.vue'),
},
]
},
{
path: '/about',
component: () => import('@/layouts/default/Default.vue'),
children: [
{
path: '',
name: 'about',
component: () => import('@/views/About.vue'),
}
]
}
]
で、結論としては「AppBar.vue
内のステータスはやっぱり保持される」となりました。
ならば、AppBar.vue
を読み込まないページを作って、そこへ遷移させたあとに再び AppBar.vue
を読み込むページへ戻ったらどうなるでしょうか。
const routes = [
{
path: '/',
component: () => import('@/layouts/default/Default.vue'),
children: [
{
path: '',
name: 'Home',
component: () => import('@/views/Home.vue'),
},
]
},
{
path: '/about',
component: () => import('@/layouts/default/Default.vue'),
children: [
{
path: '',
name: 'about',
component: () => import('@/views/About.vue'),
}
]
},
{
path: '/test',
children: [
{
path: '',
name: 'Test',
component: () => import('@/views/Test.vue'),
},
]
},
]
<template>
test
</template>
これで '/', '/test', '/about' と遷移すると、さすがにステータス(カウンターの値)は保持されませんでした。まあ、これはそうなるだろうという感じですが、ページ遷移の順序や組み合わせの違いによって、ステータスが保持されたりされなかったりするのはやはり問題です。
以上のことから、layouts
を使う場合で固定レイアウトのモジュール内で保持したいステータス(値)があるときは、横着せずに App.vue
で保持するか、vuex
などを使った方がよさそうです。
結論
Nuxt.js
で layouts
を使いましょう。