3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

VueAdvent Calendar 2023

Day 18

Vue3 + Vuetify3 + vue-router の組み合わせでプロジェクトを作成するときのメモ

Last updated at Posted at 2023-12-08

主旨

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/> 要素をページ内に追加します。

src/App.vue
<template>
  <v-app>
    <v-main>
      <router-view />
    </v-main>
  </v-app>
</template>

<script setup>

</script>

src/main.js

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/> の要素の位置に展開するように設定しています。

src/router/index.js
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! というテキストが書かれたボタンがひとつあるページになっています。

src/views/Home.vue
<template>
  <div>
    <v-btn color="primary">
      Hello!
    </v-btn>
  </div>
</template>

テスト

$ yarn dev

ブラウザで http://localhost:3000 にアクセスして、下記のような画面が出たら成功です。

image.png

/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

ファイル構成は上記のようになります。

src/router/index.js
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! と書かれたボタンをひとつ配置してみます。

src/views/About.vue
<template>
  <div>
    <v-btn color="secondary">
      About!
    </v-btn>
  </div>
</template>

上記のようにファイルの書き換え、追加をしてから yarn dev とすると、http://localhost:3000/about` をブラウザで開いた時に下記の画面が表示されます。

image.png

ボタンを押したらページ遷移する

//about のページの間で、相互にページ遷移させる例も書いておきます。遷移の方法自体は、Vuetify とは関係なく vue-router の一般的な方法をそのまま使います。

src/views/Home.vue
<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 のコンポーネントを使って遷移させたい場合は、こちらの方法のほうが適しています。

src/views/About.vue
<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 のページとの間で、相互に遷移できるようになります。

image.png

ページ遷移しても変化しない固定レイアウト(Application Barなど)部分の記述

Application Bar や Nativation Drawer などの固定レイアウト部分は、src/App.vue の中に記述します。

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 /> を設置します。

image.png

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.jsapp.use(router) とします。

このようにすると、プラグイン関連のコードは src/plugins 内のコードを編集するだけでよくなり、src/main.js を変更しなくてよくなるため、全体のコードの見通しが良くなります(バグが減る)。

src
├── App.vue
├── main.js
├── plugins
│   ├── index.js
│   ├── router.js
│   └── vuetify.js
└── views
    ├── About.vue
    └── Home.vue

ファイル構成は上記のようになります。

src/plugins/router.js
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 の中身と全く同じです。

src/plugins/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.jsyarn create vuetify したときのままでOKです(編集の必要はありません)。

src/main.js
// 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

以下は、この作法に従って、繊維先のページと固定レウアウトの部分を記述する方法についてのメモです。ルートのページ / を表示したときの見かけが、下記のようになるようにファイルを変数、追加していきます。

image.png

このページで "GO TO ABOUT" と書かれたボタンをクリックするか、"To About" のアンカーをクリックすることで、/about に遷移するようにします。

/about のページの見かけは下記のようにします。"GO TO HOME" と書かれたボタンをクリックするか、"To Home" のアンカーをクリックすることで、/ に遷移するようにします。

image.png

また、Navigation Drawer を使って各ページへ遷移できるようにもします。

image.png

サイトの基本的なページ構成は「自力で後から 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: '/' 以下の全てのページ(子ページ、孫ページ)で、最上位のコンポーネントとして使われます。

router/index.js
// 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

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 をそれぞれ読み込み、ページ内に配置しています。

src/layouts/default/Default.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

このファイルは、'/' 以下の個々のページの内容を表示させるためのものです。

src/layouts/default/View.vue
<template>
  <v-main>
    <router-view />
  </v-main>
</template>

<script setup>
  //
</script>

<router-view /> の部分には、router/index.js/ のパスの children で定義されている各パスに対応する components が読み込まれて置き換えられます。

src/router/index.js
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 に遷移できるメニュー(リスト)を含んでいます。

src/layouts/default/AppBar.vue
<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 から読み込まれます。

src/layouts/default/Footer.vue
<template>
  <v-footer app color="primary">
    超Lチカ団 (c) 2023
  </v-footer>
</template>

<script setup>
  //
</script>

src/views/Home.vue

/ に遷移したときに表示するページの内容です。/about へ遷移するためのボタンとアンカーを設置しています。

src/views/Home.vue
<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 に遷移したときに表示するページの内容です。/ へ遷移するためのボタンとアンカーを設置しています。

src/views/About.vue
<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 をブラウザで開くと、下記のようなページが表示されるはずです。

image.png

ページ遷移したときのステータスの保持について

App.vue に Application Bar などを配置して、<router-view/> で遷移先のページを表示するという構造の場合は、ページ遷移がおきても App.vue 内に Application Bar が記述されています。このため、Application Bar で保持したいステータスがある場合でも、ページ遷移にかかわらずそのステータス保持されるだろうという期待ができます。

実際、App.vue を下記のように書いておくと、どのページに遷移しても counter の値は保持されます(layoutsを使わない場合)。

src/App.vue
<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 内で保持したいステータスがあったときに、それが保持されるかについては若干不安があります。

src/layouts/default/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 のステータスが保持されているでけなのでは?という疑問もわきます。

src/router/index.js
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' との間で相互遷移させるとカウンターの値は保持されなくなりそうです。

src/router/index.js
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 を読み込むページへ戻ったらどうなるでしょうか。

router/index.js
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'),
      },
    ]
  },
]
src/view/Test.vue
<template>
  test
</template>

これで '/', '/test', '/about' と遷移すると、さすがにステータス(カウンターの値)は保持されませんでした。まあ、これはそうなるだろうという感じですが、ページ遷移の順序や組み合わせの違いによって、ステータスが保持されたりされなかったりするのはやはり問題です。

以上のことから、layouts を使う場合で固定レイアウトのモジュール内で保持したいステータス(値)があるときは、横着せずに App.vue で保持するか、vuex などを使った方がよさそうです。

結論

Nuxt.jslayouts を使いましょう。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?