Vue Router 4 路由实战:动态路由、导航守卫与懒加载

Vue Router 4 路由实战:动态路由、导航守卫与懒加载

admin
2026-07-12 / 0 评论 / 5 阅读

任何多页面 SPA 都绕不开 Vue Router。但很多项目对它的使用停留在"能跳转"——路由懒加载没配、守卫写得一团乱麻、动态路由权限方案稀里糊涂。本文以 Vue Router 4(对应 Vue 3)为准,把这几个中级必考点一次讲透。

基础配置与懒加载

路由懒加载是必选项

打包工具默认会把所有页面组件打进一个 bundle,首屏加载动辄几 MB。动态 import() 让每个页面按需加载:

// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      component: () => import('@/views/Home.vue'),
      children: [
        {
          path: 'orders',
          component: () => import('@/views/order/List.vue')
        },
        {
          path: 'orders/:id',
          component: () => import('@/views/order/Detail.vue'),
          props: true // 把路由参数作为 props 传入组件
        }
      ]
    }
  ]
})

配合 Vite 的手动分包,可以进一步把公共依赖抽出来:

// vite.config.ts
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vendor-vue': ['vue', 'vue-router', 'pinia'],
          'vendor-ui': ['element-plus']
        }
      }
    }
  }
}

props 解耦:别在组件里读 $route

<!-- ❌ 与路由强耦合,组件无法复用 -->
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
const id = route.params.id
</script>

<!-- ✅ props: true 后,组件像普通组件一样接收参数 -->
<script setup>
defineProps({ id: String })
</script>

函数模式更灵活:

{
  path: 'orders/:id',
  component: OrderDetail,
  props: route => ({ id: Number(route.params.id), tab: route.query.tab })
}

动态路由与权限系统

后台管理系统的经典需求:不同角色看到不同菜单。标准方案是前置白名单 + 动态添加路由

第一步:定义静态与动态路由

// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

// 静态路由:任何人都能访问
export const constantRoutes = [
  { path: '/login', component: () => import('@/views/Login.vue') },
  { path: '/404', component: () => import('@/views/404.vue') }
]

// 动态路由:按角色分配,meta.roles 记录可访问角色
export const asyncRoutes = [
  {
    path: '/',
    component: () => import('@/views/Layout.vue'),
    children: [
      {
        path: 'dashboard',
        component: () => import('@/views/Dashboard.vue'),
        meta: { title: '工作台', roles: ['admin', 'operator'] }
      },
      {
        path: 'system',
        component: () => import('@/views/system/Index.vue'),
        meta: { title: '系统管理', roles: ['admin'] }
      }
    ]
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes: constantRoutes
})

第二步:登录后过滤并挂载

// stores/permission.ts
import { defineStore } from 'pinia'
import { asyncRoutes, constantRoutes } from '@/router'

function hasPermission(roles, route) {
  return route.meta?.roles
    ? roles.some(role => route.meta.roles.includes(role))
    : true // 没声明 roles 默认放行
}

export function filterRoutes(routes, roles) {
  return routes.reduce((acc, route) => {
    const tmp = { ...route }
    if (hasPermission(roles, tmp)) {
      if (tmp.children) {
        tmp.children = filterRoutes(tmp.children, roles)
      }
      acc.push(tmp)
    }
    return acc
  }, [])
}

export const usePermissionStore = defineStore('permission', {
  state: () => ({
    accessibleRoutes: []
  }),
  actions: {
    generateRoutes(roles) {
      this.accessibleRoutes = [...constantRoutes, ...filterRoutes(asyncRoutes, roles)]
      return this.accessibleRoutes
    }
  }
})

第三步:addRoute 动态注册

// 登录成功后
const permissionStore = usePermissionStore()
const routes = permissionStore.generateRoutes(user.roles)

routes.forEach(route => router.addRoute(route))

注意addRoute 后必须用 router.replace 或返回新 location 触发一次重新导航,否则匹配不到刚添加的路由。

导航守卫体系

Vue Router 4 的守卫分三类,执行顺序必须烂熟于心:

导航触发
  → beforeEach(全局前置)
  → beforeEnter(路由独享)
  → beforeRouteUpdate / beforeRouteEnter(组件内)
  → afterEach(全局后置)

全局前置守卫:登录鉴权标准模板

const WHITE_LIST = ['/login', '/404']

router.beforeEach(async (to, from) => {
  const userStore = useUserStore()

  // 1. 白名单直接放行
  if (WHITE_LIST.includes(to.path)) return true

  // 2. 未登录跳登录页,带上 redirect 参数
  if (!userStore.token) {
    return { path: '/login', query: { redirect: to.fullPath } }
  }

  // 3. 已登录但还没拉取用户信息/动态路由
  if (!userStore.userInfo) {
    try {
      await userStore.fetchUserInfo()
      const permissionStore = usePermissionStore()
      const routes = permissionStore.generateRoutes(userStore.roles)
      routes.forEach(r => router.addRoute(r))

      // 关键:addRoute 后重新进入当前路由
      return { ...to, replace: true }
    } catch {
      userStore.logout()
      return { path: '/login', query: { redirect: to.fullPath } }
    }
  }

  // 4. 正常放行
  return true
})

守卫返回值语义

Vue Router 4 的守卫返回值规则:

返回值效果
undefined / true放行
false取消导航
路由地址对象 / 字符串重定向
Promiseresolve 上述值,reject 则取消并报错

afterEach:动态修改页面标题

router.afterEach((to) => {
  document.title = to.meta?.title
    ? `${to.meta.title} - 管理系统`
    : '管理系统'
})

组件内守卫:离开确认

表单页防止用户误关,用 onBeforeRouteLeave

<script setup>
import { ref } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'

const dirty = ref(false)

onBeforeRouteLeave(() => {
  if (dirty.value) {
    return window.confirm('表单未保存,确定离开?')
  }
})
</script>

滚动行为与过渡动画

const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) return savedPosition // 前进后退恢复位置
    if (to.hash) return { el: to.hash, behavior: 'smooth' } // 锚点
    return { top: 0 } // 默认回到顶部
  }
})

路由切换配合 <transition> 做页面动画:

<template>
  <router-view v-slot="{ Component, route }">
    <transition name="fade" mode="out-in">
      <component :is="Component" :key="route.path" />
    </transition>
  </router-view>
</template>

<style>
.fade-enter-active,
.fade-leave-active { transition: opacity 0.2s ease; }
.fade-enter-from,
.fade-leave-to { opacity: 0; }
</style>

useRoute 与 useRouter

<script setup> 中通过组合式 API 获取路由:

import { useRoute, useRouter } from 'vue-router'

const route = useRoute()  // 当前路由信息(响应式):params、query、meta
const router = useRouter() // 路由实例:push、replace、go

// 带参数跳转
router.push({ name: 'order-detail', params: { id: 123 }, query: { tab: 'logs' } })

// query 变化时重新拉数据(同一路由复用时 watch)
watch(() => route.query.tab, (tab) => {
  fetchList(tab)
})

易错点route.params 不是深度响应式的可靠来源,路径参数变化而组件复用时(如 /orders/1/orders/2),要 watch route.params.id 或给 router-view 加 :key

常见坑速查

  1. 刷新 404:动态路由方案里,刷新后路由表被重置,必须在守卫里重新 addRoute(见上文模板第 3 步)
  2. 通配符路由位置{ path: '/:pathMatch(.*)*' } 必须放在动态路由 addRoute 之后,否则先匹配到 404
  3. history 模式 404:服务器需配置所有路径回落到 index.html(Nginx 的 try_files $uri $uri/ /index.html;
  4. 循环重定向:守卫里 next() 与返回值混用会导致逻辑混乱,Vue Router 4 统一用返回值风格
  5. keep-alive 失效:配合动态路由时 include 需要组件 name,确保组件显式声明了 namedefineOptions({ name: 'OrderList' })

总结

  • 懒加载是标配,props: true 让组件与路由解耦
  • 权限路由三步走:静态/动态路由分离 → 登录后按角色过滤 → addRoute + 重新导航
  • 守卫统一用返回值风格,beforeEach 模板可以直接抄走
  • onBeforeRouteLeave 处理离开确认,scrollBehavior 处理滚动恢复
  • 刷新 404 和通配符路由顺序是动态路由方案最常踩的两个坑

路由系统是后台管理项目的骨架,把这套方案吃透,遇到再复杂的权限场景都能从容拆解。

0

评论 (0)

取消
0:00