vueRouter
Vue Router
-
前端路由的本质【原理】
传统多页应用时代,路由属于后端,服务器根据不同的URL返回不同的HTML页面。每次页面切换都需要重新请求服务器,导致白屏问题。单页应用(SPA)时代,只有一个HTML页面,视图切换变成了页面上某个模块的切换(即页面级组件的切换)。前端路由维护的是URL与页面级组件的映射关系,当URL变化时,动态渲染对应的组件,而不重新请求页面。
-
路由的两种实现模式【原理】
Vue Router支持两种浏览器环境下的模式:Hash模式
- 利用URL中的
#符号(hash) - 特点:hash变化不会发送HTTP请求,完全由前端处理;兼容性好
- 原理:通过
window.location.hash赋值改变路由,监听hashchange事件响应变化 - 适用:无需服务器配置,适合简单应用或静态托管
History模式
- 利用HTML5的History API(
pushState、replaceState) - 特点:URL美观,没有
#;需要服务器配合,否则刷新会404 - 原理:通过
history.pushState修改URL而不刷新页面,监听popstate事件响应变化 - 适用:生产环境,需要服务器配置回退到index.html
const router = createRouter({ history: createWebHistory(), // History模式 // history: createWebHashHistory() // Hash模式 routes }) - 利用URL中的
-
核心原理:路由与视图的联动【原理】
Vue Router的核心工作机制可以概括为三部分:① 路由注册(install)
- 通过
Vue.use(Router)调用插件的install方法 - 全局混入
beforeCreate钩子,为所有组件注入_router、_route属性 - 通过
Vue.util.defineReactive将_route变为响应式数据 - 全局注册
RouterView和RouterLink组件
② 路由匹配(matcher)
- 将开发者配置的
routes数组(树形结构)转换为扁平的路由记录表(pathMap、nameMap) - 每条路由记录包含:
path、regex、components、parent、meta等信息 - 路由匹配时,通过
path或name快速查找到对应的路由记录,生成$route对象
③ 视图更新
- 当路由变化时,调用
History.transitionTo进行导航转换 - 更新
history.current(当前路由对象) - 触发根组件
_route的响应式更新 RouterView组件依赖_route数据,重新渲染匹配的组件
伪代码示例:
// 简化版RouterView实现 const RouterView = { setup() { const route = inject('route') const depth = inject('depth', 0) const matched = computed(() => route.matched[depth]) return () => { const component = matched.value?.components.default return component ? h(component) : null } } } - 通过
-
基础使用【入门】
// router/index.js import { createRouter, createWebHistory } from 'vue-router' import Home from '@/views/Home.vue' const routes = [ { path: '/', name: 'Home', component: Home }, { path: '/about', name: 'About', component: () => import('@/views/About.vue') // 路由懒加载 } ] const router = createRouter({ history: createWebHistory(), routes }) export default router// main.js import { createApp } from 'vue' import App from './App.vue' import router from './router' createApp(App).use(router).mount('#app')<!-- App.vue --> <template> <nav> <router-link to="/">首页</router-link> <router-link to="/about">关于</router-link> </nav> <router-view /> <!-- 路由组件渲染位置 --> </template> -
动态路由匹配【进阶】
const routes = [ { path: '/user/:id', // 动态路径参数 component: User, props: true // 将params作为props传给组件 } ]<!-- User.vue --> <template> <div>用户ID:{{ id }}</div> </template> <script setup> import { useRoute } from 'vue-router' const route = useRoute() console.log(route.params.id) // 访问动态参数 // 或者通过props接收 defineProps(['id']) </script> -
嵌套路由【进阶】
const routes = [ { path: '/user/:id', component: User, children: [ { path: 'profile', component: UserProfile }, // /user/123/profile { path: 'posts', component: UserPosts } // /user/123/posts ] } ]<!-- User.vue --> <template> <div> <h1>用户信息</h1> <router-view /> <!-- 子路由出口 --> </div> </template> -
导航守卫【进阶】
导航守卫用于在路由跳转前后执行逻辑,如权限验证、数据预取等。① 全局守卫
// 全局前置守卫 router.beforeEach((to, from) => { // 返回false取消导航,返回路径重定向,返回true或undefined放行 if (to.meta.requiresAuth && !isLoggedIn()) { return '/login' } }) // 全局解析守卫(组件内守卫和异步组件解析后触发) router.beforeResolve((to) => { if (to.meta.requiresCamera) { return checkCameraPermission() } }) // 全局后置钩子(不能改变导航) router.afterEach((to, from, failure) => { if (!failure) sendAnalytics(to.fullPath) })② 路由独享守卫
const routes = [ { path: '/admin', component: Admin, beforeEnter: (to, from) => { if (!isAdmin()) return '/unauthorized' } } ]③ 组件内守卫
<script> export default { beforeRouteEnter(to, from, next) { // 进入组件前,不能访问this next(vm => { /* 可以访问组件实例 */ }) }, beforeRouteUpdate(to, from) { // 同一组件不同参数时调用(如/user/1 → /user/2) this.loadData(to.params.id) }, beforeRouteLeave(to, from) { // 离开组件前 const answer = confirm('确定离开吗?') if (!answer) return false } } </script> -
路由元信息【最佳实践】
通过meta字段附加自定义数据,如权限、标题等。const routes = [ { path: '/admin', component: Admin, meta: { requiresAuth: true, roles: ['admin'], title: '管理后台' } } ] // 在导航守卫中访问 router.beforeEach((to) => { if (to.meta.requiresAuth && !hasRole(to.meta.roles)) { return '/unauthorized' } }) // 在后置钩子中设置标题 router.afterEach((to) => { document.title = to.meta.title || '默认标题' }) -
路由懒加载【优化】
使用动态导入,按需加载组件,减少首屏加载时间。const routes = [ { path: '/heavy', component: () => import(/* webpackChunkName: "heavy" */ '@/views/HeavyComponent.vue') } ] -
编程式导航【技巧】
import { useRouter } from 'vue-router' const router = useRouter() // 字符串路径 router.push('/user/123') // 对象形式 router.push({ path: '/user/123' }) router.push({ name: 'User', params: { id: 123 } }) router.push({ path: '/search', query: { q: 'vue' } }) // 替换当前历史记录 router.replace('/login') // 前进/后退 router.go(1) // 前进 router.back() // 后退 -
完整导航解析流程【原理】
从触发导航到视图更新的完整流程:- 导航被触发
- 失活的组件里调用
beforeRouteLeave守卫 - 调用全局
beforeEach守卫 - 复用组件里调用
beforeRouteUpdate守卫 - 路由配置里调用
beforeEnter守卫 - 解析异步路由组件
- 激活组件里调用
beforeRouteEnter守卫 - 调用全局
beforeResolve守卫 - 导航确认
- 调用全局
afterEach钩子 - 触发DOM更新
- 调用
beforeRouteEnter中传给next的回调函数
-
权限控制实战【最佳实践】
权限控制的核心需求:不同角色的用户只能访问其权限范围内的页面,未登录用户跳转到登录页,无权限用户跳转到403。① 基本原理
- 前端维护一份公共路由(如登录页、注册页、404页)
- 登录成功后,后端返回当前用户的权限标识(如角色列表、可访问的路由名称或路径)
- 前端根据权限动态生成该用户特有的路由表,通过
router.addRoute动态添加 - 在全局前置守卫中校验访问权限
② 为什么需要动态路由?
静态路由配置所有可能的路由,但未授权用户虽然看不到入口,却可能通过URL直接访问。动态路由确保未授权的路由根本就不存在,从根源上防止越权。③ 代码实现示例
// 公共路由(所有人可访问) const constantRoutes = [ { path: '/login', component: Login }, { path: '/404', component: NotFound }, { path: '/', redirect: '/dashboard' } ] // 异步路由(需要权限的页面) const asyncRoutes = [ { path: '/admin', component: Layout, meta: { roles: ['admin'] }, children: [ { path: 'users', component: UserManage, meta: { roles: ['admin'] } }, { path: 'settings', component: SystemSetting, meta: { roles: ['admin'] } } ] }, { path: '/editor', component: Layout, meta: { roles: ['editor', 'admin'] }, children: [ { path: 'articles', component: ArticleList, meta: { roles: ['editor', 'admin'] } } ] } ] // 路由守卫 router.beforeEach(async (to, from) => { const token = localStorage.getItem('token') if (to.path === '/login') { if (token) return '/' // 已登录,跳转首页 return true } if (!token) return '/login' // 已登录,尝试获取用户权限 const userStore = useUserStore() if (!userStore.roles.length) { try { // 请求后端获取用户角色和权限路由 const { roles, dynamicRoutes } = await fetchUserInfo() userStore.setRoles(roles) // 根据角色过滤出可访问的路由表 const accessibleRoutes = filterAsyncRoutes(asyncRoutes, roles) // 动态添加路由 accessibleRoutes.forEach(route => router.addRoute(route)) // 添加404通配路由(必须最后添加) router.addRoute({ path: '/:pathMatch(.*)*', redirect: '/404' }) // 重新触发当前导航 return { ...to, replace: true } } catch (error) { // token无效,跳转登录 return '/login' } } // 检查当前路由是否有权限访问 if (to.meta.roles && !to.meta.roles.some(r => userStore.roles.includes(r))) { return '/403' // 无权限跳转403页面 } return true }) // 辅助函数:根据角色过滤路由 function filterAsyncRoutes(routes, roles) { const res = [] routes.forEach(route => { const r = { ...route } if (hasPermission(roles, r.meta)) { if (r.children) { r.children = filterAsyncRoutes(r.children, roles) } res.push(r) } }) return res }④ 动态路由的持久化
刷新页面后动态添加的路由会丢失,因为router.addRoute是在内存中操作,刷新后需要重新执行权限逻辑。所以需要在刷新前将用户角色等信息存储在store(或缓存)中,再次进入守卫时重新生成。 -
根据后端接口配置路由【实战】
当权限粒度很细,或者菜单项完全由后端动态配置时,前端需要将后端返回的JSON格式路由数据转换成Vue Router可识别的路由配置。① 为什么需要?
- 大型CMS系统中,菜单和权限经常变化,前端重新发版成本高
- 后端统一管理权限,前端只负责渲染
- 实现零代码修改即可调整菜单结构
② 后端返回数据结构示例
[ { "path": "/dashboard", "name": "Dashboard", "component": "dashboard/index", // 组件路径,需要映射 "meta": { "title": "仪表盘", "icon": "dashboard" }, "children": [] }, { "path": "/system", "name": "System", "component": "Layout", // 布局组件 "meta": { "title": "系统管理", "icon": "setting" }, "children": [ { "path": "user", "name": "User", "component": "system/user/index", "meta": { "title": "用户管理" } } ] } ]③ 前端动态映射组件
// 组件映射表(需要提前导入所有可能用到的组件) const componentMap = { 'Layout': () => import('@/layouts/Layout.vue'), 'dashboard/index': () => import('@/views/dashboard/index.vue'), 'system/user/index': () => import('@/views/system/user/index.vue') // 更多组件... } function generateRoutes(backendRoutes) { const routes = [] backendRoutes.forEach(route => { const r = { path: route.path, name: route.name, component: componentMap[route.component], meta: route.meta, children: route.children ? generateRoutes(route.children) : [] } routes.push(r) }) return routes } // 获取后端路由后调用 const asyncRoutes = generateRoutes(backendData) asyncRoutes.forEach(route => router.addRoute(route)) -
路由优化技巧【优化】
① 路由懒加载与代码分割
使用动态导入import(),webpack会自动进行代码分割,生成单独的chunk。可以为每个路由指定chunk名称,便于缓存分析。const routes = [ { path: '/large', component: () => import(/* webpackChunkName: "large" */ './views/Large.vue') } ]② 预加载(Prefetch)
Vue Router支持router.beforeResolve结合webpackPrefetch预加载即将用到的组件。或者在用户悬停链接时预加载。router.beforeResolve(async (to) => { // 预加载目标路由组件及其异步组件 const components = to.matched.map(record => record.components.default) await Promise.all(components.map(comp => comp())) })③ 路由级别数据预取
在路由守卫中预先获取数据,避免进入页面后出现空白或加载状态。可以在beforeRouteEnter或全局beforeResolve中派发store action,获取数据后再进入页面。 -
总结【速记版】
- 本质:URL与组件的映射关系,实现SPA无刷新视图切换
- 模式:Hash(兼容好)和History(URL美观,需服务器支持)
- 原理:路由配置 → 扁平化路由表 → 响应式
_route→ RouterView重新渲染 - 动态路由:
:id参数,通过route.params访问 - 嵌套路由:
children配置,多级router-view - 导航守卫:全局、路由独享、组件内三级防护,用于权限控制
- 元信息:
meta字段附加路由元数据 - 懒加载:动态导入组件,优化性能
- 编程式导航:
router.push()、router.replace()等 - 权限控制:动态添加路由(
addRoute)+ 路由守卫校验 - 动态配置:根据后端JSON动态生成路由,实现灵活权限管理
- 优化:预加载、数据预取、代码分割