ルートメタ情報の概要
Vue Router では、各ルート定義に任意のデータを付与する「メタ情報」機能を提供しています。この機能を利用することで、権限管理、ページタイトルの動的設定、レイアウトの切り替えなど、ルート固有の情報をコンポーネント外から制御可能になります。メタ情報は meta プロパティを通じて定義され、ナビゲーションガードやコンポーネント内から参照することが可能です。
TypeScript による型定義
TypeScript を使用している場合、メタ情報の型安全性を確保するためにモジュール拡張を行います。これにより、定義されていないプロパティへのアクセスを防ぎ、開発体験を向上させます。
// router/config.ts
import { createRouter, createWebHistory } from 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
pageTitle?: string
requiresAuth?: boolean
}
}
export const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
component: () => import('@/pages/Auth/LoginPage.vue'),
meta: {
pageTitle: "ログイン",
requiresAuth: false
}
},
{
path: '/dashboard',
component: () => import('@/pages/Dashboard.vue'),
meta: {
pageTitle: "ダッシュボード",
requiresAuth: true
}
},
],
})
ローディングコンポーネントの実装
ルート遷移時のユーザー体験を向上させるため、プログレスバー形式的のローディングコンポーネントを作成します。ここでは requestAnimationFrame を利用して滑らかなアニメーションを実現します。
<!-- components/PageLoader.vue -->
<template>
<div class="loader-container">
<div ref="progressBar" class="progress-bar"></div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const progressValue = ref<number>(0)
const progressBar = ref<HTMLElement | null>(null)
let animationFrameId = 0
const startLoading = () => {
progressValue.value = 0
const element = progressBar.value
if (!element) return
const animate = () => {
if (progressValue.value < 85) {
progressValue.value += Math.random() * 5
element.style.width = `${progressValue.value}%`
animationFrameId = requestAnimationFrame(animate)
}
}
animationFrameId = requestAnimationFrame(animate)
}
const finishLoading = () => {
const element = progressBar.value
if (!element) return
setTimeout(() => {
progressValue.value = 100
element.style.width = '100%'
setTimeout(() => {
progressValue.value = 0
element.style.width = '0%'
}, 300)
}, 400)
}
defineExpose({ startLoading, finishLoading })
</script>
<style scoped lang="less">
.loader-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 4px;
z-index: 9999;
.progress-bar {
height: 100%;
background: #1890ff;
width: 0%;
transition: width 0.2s ease;
}
}
</style>
ページコンポーネントの構成
ダッシュボードページとログインページを用意します。ログインページではフォーム検証を行い、成功時にトークンを保存して遷移します。
<!-- pages/LoginPage.vue -->
<template>
<div class="auth-wrapper">
<el-card class="auth-card">
<el-form :model="credentials" :rules="validationRules" ref="loginFormRef">
<el-form-item prop="username" label="ユーザー名">
<el-input v-model="credentials.username" />
</el-form-item>
<el-form-item prop="pass" label="パスワード">
<el-input v-model="credentials.pass" type="password" />
</el-form-item>
<el-button type="primary" @click="handleLogin">ログイン</el-button>
</el-form>
</el-card>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
const router = useRouter()
const loginFormRef = ref<FormInstance>()
const credentials = reactive({
username: '',
pass: ''
})
const validationRules: FormRules = {
username: [{ required: true, message: 'ユーザー名を入力してください', trigger: 'blur' }],
pass: [{ required: true, message: 'パスワードを入力してください', trigger: 'blur' }]
}
const handleLogin = async () => {
if (!loginFormRef.value) return
await loginFormRef.value.validate((valid) => {
if (valid) {
localStorage.setItem('auth_token', 'dummy_token_value')
router.push('/dashboard')
} else {
ElMessage.error('入力内容を確認してください')
}
})
}
</script>
ナビゲーションガードによる制御
アプリケーションのエントリーポイントである main.ts において、グローバルガードを設定します。ここではページタイトルの更新、ローディングの制御、および認証状態に基づいたルート保護を行います。
// main.ts
import { createApp, createVNode, render } from 'vue'
import App from './App.vue'
import { router } from './router/config'
import ElementPlus from 'element-plus'
import PageLoader from './components/PageLoader.vue'
import 'element-plus/dist/index.css'
const loaderVNode = createVNode(PageLoader)
render(loaderVNode, document.body)
const app = createApp(App)
app.use(router)
app.use(ElementPlus)
const publicRoutes = ['/', '/login']
router.beforeEach((to, from, next) => {
// タイトル更新
if (to.meta.pageTitle) {
document.title = `${to.meta.pageTitle} - アプリ`
}
// ローディング開始
loaderVNode.component?.exposed?.startLoading()
const hasToken = localStorage.getItem('auth_token')
const isPublic = publicRoutes.includes(to.path)
if (isPublic || hasToken) {
next()
} else {
next('/')
}
})
router.afterEach(() => {
// ローディング終了
loaderVNode.component?.exposed?.finishLoading()
})
app.mount('#app')