【Vue】路由管理(Vue Router)
个人主页:Guiat
归属专栏:Vue
文章目录
- 1. Vue Router 简介
- 1.1 单页应用与路由
- 1.2 Vue Router 的核心功能
- 2. Vue Router 基础配置
- 2.1 安装与基本设置
- 2.2 路由模式
- 2.3 路由组件与视图
- 3. 路由导航
- 3.1 声明式导航
- 3.2 编程式导航
- 3.3 路由参数获取
- 4. 高级路由配置
- 4.1 嵌套路由
- 4.2 命名视图
- 4.3 路由懒加载
- 5. 路由守卫
- 5.1 全局守卫
- 5.2 路由独享守卫
- 5.3 组件内守卫
- 6. 路由元信息与数据获取
- 6.1 路由元信息
- 6.2 路由数据获取
- 7. 动态路由与进阶技巧
- 7.1 动态添加和删除路由
- 7.2 路由过渡效果
- 7.3 滚动行为
- 8. 实际应用与最佳实践
- 8.1 路由组织与模块化
- 8.2 权限控制与认证
- 8.3 性能优化策略
- 9. Vue Router 与状态管理的结合
- 9.1 Vue Router 与 Vuex/Pinia 协作
- 9.2 路由参数同步到状态
- 9.3 基于状态的导航守卫
- 10. 测试与调试
- 10.1 路由单元测试
- 10.2 路由调试技巧
- 10.3 常见问题与解决方案
正文
1. Vue Router 简介
Vue Router 是 Vue.js 官方的路由管理器,它与 Vue.js 核心深度集成,使构建单页面应用变得轻而易举。它提供了声明式的导航控制和组件系统,让开发者能够通过配置路由映射、嵌套路由等方式构建复杂的应用。
1.1 单页应用与路由
单页应用(SPA)是一种网页应用程序或网站的模型,它通过动态重写当前页面来与用户交互,而非传统的从服务器加载整个新页面。这种方法避免了页面之间切换打断用户体验。
// 传统多页应用
// 每次导航都会向服务器请求新页面
location.href = '/about.html'// 单页应用
// 只改变 URL,不重新加载页面
history.pushState(null, '', '/about')
1.2 Vue Router 的核心功能
- 嵌套的路由/视图表
- 模块化的、基于组件的路由配置
- 路由参数、查询、通配符
- 基于 Vue.js 过渡系统的视图过渡效果
- 细粒度的导航控制
- 带有自动激活的 CSS class 的链接
- HTML5 历史模式或 hash 模式
- 自定义的滚动行为
2. Vue Router 基础配置
2.1 安装与基本设置
# Vue 2
npm install vue-router@3# Vue 3
npm install vue-router@4
基本配置(Vue 3):
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'const routes = [{path: '/',name: 'Home',component: Home},{path: '/about',name: 'About',component: About}
]const router = createRouter({history: createWebHistory(),routes
})export default router
在 Vue 应用中使用:
// 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')
2.2 路由模式
Vue Router 提供两种路由模式:
- Hash 模式:使用 URL 的 hash (#) 来模拟完整的 URL
- History 模式:利用 HTML5 History API 实现无 hash 的 URL
// Hash 模式 (默认)
const router = createRouter({history: createWebHashHistory(),routes
})// History 模式
const router = createRouter({history: createWebHistory(),routes
})
2.3 路由组件与视图
在 Vue 组件中使用路由视图:
<template><div id="app"><nav><router-link to="/">Home</router-link> |<router-link to="/about">About</router-link></nav><!-- 路由匹配的组件将渲染在这里 --><router-view /></div>
</template>
3. 路由导航
3.1 声明式导航
使用 <router-link>
组件进行声明式导航:
<!-- 基本用法 -->
<router-link to="/">Home</router-link><!-- 命名路由 -->
<router-link :to="{ name: 'About' }">About</router-link><!-- 带参数的路由 -->
<router-link :to="{ name: 'User', params: { id: 123 }}">User</router-link><!-- 带查询参数 -->
<router-link :to="{ path: '/search', query: { q: 'vue' }}">Search</router-link><!-- 自定义激活类 -->
<router-link to="/about" active-class="active">About</router-link><!-- 精确匹配 (Vue 3) -->
<router-link to="/" :exact="true">Home</router-link>
3.2 编程式导航
通过 JavaScript 代码控制路由:
// 基本导航
this.$router.push('/')
this.$router.push('/about')// 对象形式
this.$router.push({ path: '/about' })// 命名路由
this.$router.push({ name: 'User', params: { id: 123 }})// 带查询参数
this.$router.push({ path: '/search', query: { q: 'vue' }})// 替换当前历史记录
this.$router.replace('/about')// 前进/后退
this.$router.go(1) // 前进一步
this.$router.go(-1) // 后退一步
this.$router.back() // 后退一步
this.$router.forward() // 前进一步
在 Vue 3 Composition API 中:
import { useRouter } from 'vue-router'export default {setup() {const router = useRouter()function navigateToAbout() {router.push('/about')}return { navigateToAbout }}
}
3.3 路由参数获取
通过 $route
对象访问路由参数:
// 路由配置
const routes = [{path: '/user/:id',name: 'User',component: User}
]// 在组件中获取参数 (Vue 2)
export default {created() {// 路径参数console.log(this.$route.params.id)// 查询参数console.log(this.$route.query.search)}
}// 在组件中获取参数 (Vue 3 Composition API)
import { useRoute } from 'vue-router'export default {setup() {const route = useRoute()console.log(route.params.id)console.log(route.query.search)}
}
4. 高级路由配置
4.1 嵌套路由
路由可以嵌套,实现复杂的应用布局:
const routes = [{path: '/user',component: User,children: [// 当 /user 匹配成功// UserHome 会被渲染在 User 的 <router-view> 中{ path: '', component: UserHome },// 当 /user/profile 匹配成功// UserProfile 会被渲染在 User 的 <router-view> 中{ path: 'profile', component: UserProfile },// 当 /user/posts 匹配成功// UserPosts 会被渲染在 User 的 <router-view> 中{ path: 'posts', component: UserPosts }]}
]
对应的组件结构:
<!-- User.vue -->
<template><div><h2>User Section</h2><nav><router-link to="/user">Home</router-link><router-link to="/user/profile">Profile</router-link><router-link to="/user/posts">Posts</router-link></nav><router-view></router-view></div>
</template>
4.2 命名视图
同一个路由可以显示多个视图:
const routes = [{path: '/',components: {default: Home,sidebar: Sidebar,footer: Footer}}
]
对应的模板:
<router-view></router-view>
<router-view name="sidebar"></router-view>
<router-view name="footer"></router-view>
4.3 路由懒加载
为了提高应用性能,可以使用动态导入实现路由懒加载:
// 不使用懒加载
import UserDetails from './views/UserDetails.vue'const routes = [{ path: '/user/:id', component: UserDetails }
]// 使用懒加载
const routes = [{ path: '/user/:id', component: () => import('./views/UserDetails.vue') }
]// 分组懒加载
const routes = [{path: '/user',component: () => import(/* webpackChunkName: "user" */ './views/User.vue'),children: [{path: 'profile',component: () => import(/* webpackChunkName: "user" */ './views/UserProfile.vue')},{path: 'posts',component: () => import(/* webpackChunkName: "user" */ './views/UserPosts.vue')}]}
]
5. 路由守卫
5.1 全局守卫
全局前置守卫:
router.beforeEach((to, from, next) => {// to: 即将进入的目标路由对象// from: 当前导航正要离开的路由对象// next: 函数,必须调用该方法来 resolve 这个钩子// 检查用户是否已登录if (to.meta.requiresAuth && !isAuthenticated) {// 未登录,重定向到登录页next({ name: 'Login' })} else {// 继续导航next()}
})
全局解析守卫:
router.beforeResolve((to, from, next) => {// 在所有组件内守卫和异步路由组件被解析之后调用next()
})
全局后置钩子:
router.afterEach((to, from) => {// 不会影响导航本身,常用于分析、更改页面标题等document.title = to.meta.title || 'My App'
})
5.2 路由独享守卫
直接在路由配置上定义:
const routes = [{path: '/admin',component: Admin,beforeEnter: (to, from, next) => {// 检查用户权限if (hasAdminRights()) {next()} else {next('/403')}}}
]
5.3 组件内守卫
在组件内定义路由导航守卫:
export default {// 在渲染该组件的对应路由被验证前调用beforeRouteEnter(to, from, next) {// 不能获取组件实例 `this`// 因为当守卫执行时,组件实例还没被创建next(vm => {// 通过 `vm` 访问组件实例})},// 在当前路由改变,但是该组件被复用时调用beforeRouteUpdate(to, from, next) {// 可以访问组件实例 `this`this.name = to.params.namenext()},// 导航离开该组件的对应路由时调用beforeRouteLeave(to, from, next) {// 可以访问组件实例 `this`const answer = window.confirm('Do you really want to leave?')if (answer) {next()} else {next(false)}}
}
在 Vue 3 Composition API 中:
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'export default {setup() {// 与 beforeRouteLeave 相同onBeforeRouteLeave((to, from) => {const answer = window.confirm('Do you really want to leave?')if (!answer) return false})// 与 beforeRouteUpdate 相同onBeforeRouteUpdate((to, from) => {// 处理路由变化...})}
}
6. 路由元信息与数据获取
6.1 路由元信息
可以在路由配置中附加自定义数据:
const routes = [{path: '/admin',component: Admin,meta: { requiresAuth: true,title: 'Admin Panel',permissions: ['admin', 'editor']}}
]
使用元信息:
// 全局守卫中使用
router.beforeEach((to, from, next) => {document.title = to.meta.title || 'Default Title'if (to.meta.requiresAuth && !isAuthenticated()) {next('/login')} else {next()}
})// 组件中使用
export default {created() {console.log(this.$route.meta.permissions)}
}
6.2 路由数据获取
在导航完成前获取数据:
export default {data() {return {post: null,loading: false,error: null}},beforeRouteEnter(to, from, next) {next(vm => {vm.fetchData()})},beforeRouteUpdate(to, from, next) {this.post = nullthis.fetchData()next()},methods: {fetchData() {this.loading = truethis.error = null// 假设 getPost 返回一个 PromisegetPost(this.$route.params.id).then(post => {this.post = postthis.loading = false}).catch(err => {this.error = err.toString()this.loading = false})}}
}
在导航完成后获取数据:
export default {data() {return {post: null,loading: false,error: null}},created() {// 组件创建完后获取数据this.fetchData()},watch: {// 路由变化时重新获取数据'$route': 'fetchData'},methods: {fetchData() {this.loading = truethis.error = nullgetPost(this.$route.params.id).then(post => {this.post = postthis.loading = false}).catch(err => {this.error = err.toString()this.loading = false})}}
}
在 Vue 3 Composition API 中:
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'export default {setup() {const route = useRoute()const post = ref(null)const loading = ref(false)const error = ref(null)const fetchData = async () => {loading.value = trueerror.value = nulltry {post.value = await getPost(route.params.id)} catch (err) {error.value = err.toString()} finally {loading.value = false}}// 首次加载fetchData()// 路由参数变化时重新获取watch(() => route.params.id, fetchData)return { post, loading, error }}
}
7. 动态路由与进阶技巧
7.1 动态添加和删除路由
Vue Router 允许动态修改路由配置:
// 添加路由
router.addRoute({path: '/dynamic',name: 'Dynamic',component: () => import('./views/Dynamic.vue')
})// 添加嵌套路由
router.addRoute('User', {path: 'settings',component: UserSettings
})// 删除路由
// 方法 1:添加一个名字相同的路由
router.addRoute({ path: '/about', name: 'About', component: About })// 方法 2:通过返回的删除函数
const removeRoute = router.addRoute(route)
removeRoute() // 删除路由// 方法 3:通过名称删除
router.removeRoute('About')// 检查路由是否存在
router.hasRoute('About')// 获取所有路由记录
router.getRoutes()
7.2 路由过渡效果
结合 Vue 的过渡系统实现路由切换动画:
<template><router-view v-slot="{ Component }"><transition name="fade" mode="out-in"><component :is="Component" /></transition></router-view>
</template><style>
.fade-enter-active,
.fade-leave-active {transition: opacity 0.3s ease;
}.fade-enter-from,
.fade-leave-to {opacity: 0;
}
</style>
基于路由的动态过渡:
<template><router-view v-slot="{ Component, route }"><transition :name="route.meta.transition || 'fade'"><component :is="Component" /></transition></router-view>
</template><script>
export default {// 路由配置// {// path: '/slide-page',// component: SlidePage,// meta: { transition: 'slide' }// }
}
</script><style>
.fade-enter-active,
.fade-leave-active {transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {opacity: 0;
}.slide-enter-active,
.slide-leave-active {transition: transform 0.3s;
}
.slide-enter-from,
.slide-leave-to {transform: translateX(100%);
}
</style>
7.3 滚动行为
控制页面切换时的滚动位置:
const router = createRouter({history: createWebHistory(),routes: [...],scrollBehavior(to, from, savedPosition) {// 返回 savedPosition,在按下 后退/前进 按钮时有效if (savedPosition) {return savedPosition}// 滚动到锚点if (to.hash) {return {el: to.hash,behavior: 'smooth',}}// 滚动到顶部return { top: 0 }}
})
8. 实际应用与最佳实践
8.1 路由组织与模块化
大型应用中的路由组织:
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import homeRoutes from './modules/home'
import userRoutes from './modules/user'
import shopRoutes from './modules/shop'const routes = [...homeRoutes,...userRoutes,...shopRoutes,{ path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('../views/NotFound.vue') }
]const router = createRouter({history: createWebHistory(),routes
})export default router// router/modules/user.js
export default [{path: '/user',component: () => import('@/layouts/UserLayout.vue'),children: [{path: '',name: 'UserHome',component: () => import('@/views/user/Home.vue')},{path: 'profile',name: 'UserProfile',component: () => import('@/views/user/Profile.vue')}]}
]
8.2 权限控制与认证
基于角色的路由访问控制:
// 路由配置
const routes = [{path: '/admin',component: Admin,meta: { requiresAuth: true, roles: ['admin'] }},{path: '/editor',component: Editor,meta: { requiresAuth: true, roles: ['editor', 'admin'] }}
]// 全局前置守卫
router.beforeEach((to, from, next) => {const { requiresAuth, roles } = to.metaconst currentUser = getCurrentUser()if (!requiresAuth) {return next()}if (!currentUser) {return next({path: '/login',query: { redirect: to.fullPath }})}if (roles && !roles.includes(currentUser.role)) {return next('/403')}next()
})
8.3 性能优化策略
- 路由懒加载:减小初始加载体积
const routes = [{path: '/dashboard',component: () => import('./views/Dashboard.vue')}
]
- 预加载关键路由:提高用户体验
// 在应用空闲时预加载
const PreloadedView = () => ({component: import('./views/PreloadedView.vue'),loading: LoadingComponent,delay: 200
})
- 保持路由组件状态:使用
<keep-alive>
<router-view v-slot="{ Component }"><keep-alive><component :is="Component" /></keep-alive>
</router-view>
- 路由元信息缓存控制:
const routes = [{path: '/user/:id',component: UserDetails,meta: { keepAlive: true }}
]// 在模板中
<router-view v-slot="{ Component }"><keep-alive><component v-if="$route.meta.keepAlive" :is="Component" /></keep-alive><component v-if="!$route.meta.keepAlive" :is="Component" />
</router-view>
9. Vue Router 与状态管理的结合
9.1 Vue Router 与 Vuex/Pinia 协作
// store/index.js (Vuex)
export default createStore({state: {user: null,isAuthenticated: false},mutations: {setUser(state, user) {state.user = userstate.isAuthenticated = !!user}},actions: {async login({ commit }, credentials) {const user = await authService.login(credentials)commit('setUser', user)return user},async logout({ commit }) {await authService.logout()commit('setUser', null)}}
})// 路由守卫中使用
router.beforeEach((to, from, next) => {const requiresAuth = to.matched.some(record => record.meta.requiresAuth)const { isAuthenticated } = store.stateif (requiresAuth && !isAuthenticated) {next('/login')} else {next()}
})
9.2 路由参数同步到状态
// store/index.js
export default createStore({state: {searchQuery: '',currentProductId: null},mutations: {setSearchQuery(state, query) {state.searchQuery = query},setCurrentProductId(state, id) {state.currentProductId = id}}
})// 组件中
export default {created() {// 从路由同步到状态this.$store.commit('setSearchQuery', this.$route.query.q || '')this.$store.commit('setCurrentProductId', this.$route.params.id)},watch: {'$route'(to) {this.$store.commit('setSearchQuery', to.query.q || '')this.$store.commit('setCurrentProductId', to.params.id)}}
}
9.3 基于状态的导航守卫
router.beforeEach(async (to, from, next) => {// 检查是否需要获取用户信息if (to.matched.some(record => record.meta.requiresAuth) && store.state.user === null) {try {// 尝试获取用户信息await store.dispatch('fetchCurrentUser')next()} catch (error) {next('/login')}} else {next()}
})
10. 测试与调试
10.1 路由单元测试
使用 Vue Test Utils 测试路由组件:
import { mount } from '@vue/test-utils'
import { createRouter, createWebHistory } from 'vue-router'
import App from '@/App.vue'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'// 创建路由实例
const routes = [{ path: '/', component: Home },{ path: '/about', component: About }
]const router = createRouter({history: createWebHistory(),routes
})test('routing', async () => {// 挂载应用const wrapper = mount(App, {global: {plugins: [router]}})// 等待路由准备就绪await router.isReady()// 验证初始路由expect(wrapper.html()).toContain('Welcome to Home')// 导航到 About 页面await router.push('/about')// 等待路由变化await wrapper.vm.$nextTick()// 验证新路由内容expect(wrapper.html()).toContain('About Page')
})
10.2 路由调试技巧
-
使用 Vue Devtools 检查路由状态
-
路由日志记录:
router.beforeEach((to, from, next) => {console.log(`Navigating from ${from.fullPath} to ${to.fullPath}`)next()
})
- 路由错误处理:
router.onError((error) => {console.error('Router error:', error)// 可以上报到错误监控系统
})
10.3 常见问题与解决方案
- 页面刷新 404 问题:
在使用 history 模式时,需要服务器配置将所有请求重定向到 index.html
# Nginx 配置
location / {try_files $uri $uri/ /index.html;
}
- 路由参数变化但组件不更新:
使用 watch 监听路由变化或使用 key 强制更新
<router-view :key="$route.fullPath"></router-view>
- 路由守卫中的异步操作:
确保在异步操作完成后调用 next()
router.beforeEach(async (to, from, next) => {try {await someAsyncOperation()next()} catch (error) {next(false)}
})
- 导航重复错误:
处理导航到当前位置的情况
// Vue Router 4.x
router.push('/current-path').catch(err => {if (err.name !== 'NavigationDuplicated') {throw err}
})// 或全局处理
const originalPush = router.push
router.push = function push(location) {return originalPush.call(this, location).catch(err => {if (err.name !== 'NavigationDuplicated') throw err})
}
结语
感谢您的阅读!期待您的一键三连!欢迎指正!