Skip to content

Vue Router 3.x ​

大白话解释: Vue Router 就像"网站的导航地图"。用户点击不同的链接,Router 负责把对应的页面内容展示出来,同时管理浏览器的前进/后退按钮。

为什么需要路由?

  • 单页面应用(SPA)的核心:页面不刷新,只替换内容区域
  • URL 与页面对应:用户可以直接访问某个 URL 看到对应页面
  • 权限控制:通过路由守卫判断用户是否有权限访问某个页面

Vue Router 3 vs 4:

  • Vue Router 3:适配 Vue 2,使用 new VueRouter() 创建实例,守卫中必须调用 next(),通配路由用 path: '*'
  • Vue Router 4:适配 Vue 3,使用 createRouter() 创建实例,守卫中可不调用 next()(直接 return),通配路由改用 path: '/:pathMatch(.*)*' 或 /:catchAll(.*),mode: 'history' 改为 history: createWebHistory()

⚠️ Vue Router 3 已停止维护(EOL),新项目请使用 Vue Router 4(适配 Vue 3)。

Vue Router 3.x 是适配 Vue 2 的路由管理器。API 与 Vue Router 4(Vue 3 版)有部分差异,最明显的是构造方式和守卫中 next() 的用法。


基本配置 ​

安装与初始化 ​

js
// main.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import App from './App.vue'
import router from './router'

Vue.use(VueRouter)

new Vue({
  router,
  render: (h) => h(App),
}).$mount('#app')

路由表定义 ​

js
// router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'

// 路由懒加载 —— 按需加载,优化首屏
const Home = () => import('@/views/Home.vue')
const Login = () => import('@/views/Login.vue')
const Dashboard = () => import('@/views/Dashboard.vue')
const UserList = () => import('@/views/user/UserList.vue')
const UserDetail = () => import('@/views/user/UserDetail.vue')
const NotFound = () => import('@/views/404.vue')

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/login',
    name: 'Login',
    component: Login,
    meta: { title: '登录' },
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: Dashboard,
    meta: { requiresAuth: true, title: '仪表盘' },
  },
  {
    path: '/user',
    name: 'User',
    component: () => import('@/views/user/Layout.vue'),
    redirect: '/user/list',
    children: [
      {
        path: 'list',
        name: 'UserList',
        component: UserList,
        meta: { title: '用户列表' },
      },
      {
        path: 'detail/:id',  // 动态路由参数
        name: 'UserDetail',
        component: UserDetail,
        meta: { title: '用户详情' },
        props: true, // 将 params 作为 props 传入组件
      },
    ],
  },
  {
    // 通配路由 —— 必须放在最后
    // ⚠️ Vue Router 4 不再支持 path: '*',需改为 path: '/:pathMatch(.*)*'
    path: '*',
    name: 'NotFound',
    component: NotFound,
    meta: { title: '404' },
  },
]

const router = new VueRouter({
  // ⚠️ Vue Router 4 改为 createRouter({ history: createWebHistory(), routes })
  // mode: 'history' 对应 Vue 4 的 createWebHistory()
  // mode: 'hash' 对应 Vue 4 的 createWebHashHistory()
  mode: 'history',  // 'hash' | 'history' | 'abstract'
  routes,
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) return savedPosition
    if (to.hash) return { selector: to.hash }
    return { x: 0, y: 0 }
  },
})

export default router

路由模式 ​

hash 模式(默认) ​

js
new VueRouter({ mode: 'hash', routes })
// URL: http://example.com/#/user/list
  • 兼容性好,支持 IE9+
  • 不需要后端配置
  • URL 带 # 号

history 模式 ​

js
new VueRouter({ mode: 'history', routes })
// URL: http://example.com/user/list
  • URL 干净,没有 #
  • 需要后端配置:所有路由都返回 index.html
  • IE10+ 支持
nginx
# Nginx 配置
location / {
  try_files $uri $uri/ /index.html;
}
apache
# Apache 配置
<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.html [L]
</IfModule>

路由守卫 ​

全局前置守卫 ​

js
// Vue 3 中守卫可以不调用 next(),直接 return 即可
router.beforeEach((to, from, next) => {
  // to: 即将进入的路由
  // from: 当前正要离开的路由
  // next: Vue Router 3 必须调用;Vue 4 中可省略,直接 return

  const token = localStorage.getItem('token')
  const isLoggedIn = !!token

  // 设置页面标题
  document.title = to.meta.title || '默认标题'

  // 需要登录但未登录 → 跳转登录页
  if (to.meta.requiresAuth && !isLoggedIn) {
    next({
      name: 'Login',
      query: { redirect: to.fullPath }, // 记录目标页,登录后跳回
    })
    return
  }

  // 已登录还访问登录页 → 跳转首页
  if (to.name === 'Login' && isLoggedIn) {
    next({ name: 'Dashboard' })
    return
  }

  next() // 必须调用 next(),否则路由会挂起
})

⚠️ Vue Router 3 必须调用 next()。忘记调用会导致路由挂起,页面空白。Vue Router 4 中守卫函数可以直接 return 或返回路由地址,next() 仅为可选的兼容写法。

全局后置钩子 ​

js
router.afterEach((to, from) => {
  // 不需要 next
  // 常用于:页面埋点、NProgress 结束
  NProgress.done()
  // 页面访问统计
  trackPageView(to.fullPath)
})

全局解析守卫 ​

js
router.beforeResolve((to, from, next) => {
  // 在所有组件内守卫和异步路由组件被解析之后调用
  // 确保所有数据都已加载
  next()
})

路由独享守卫 ​

js
{
  path: '/admin',
  component: AdminLayout,
  meta: { requiresAdmin: true },
  beforeEnter: (to, from, next) => {
    const userRole = store.getters.userRole
    if (userRole !== 'admin') {
      next({ name: 'Forbidden' })
    } else {
      next()
    }
  },
}

组件内守卫 ​

vue
<script>
export default {
  // 进入路由前(组件实例未创建,不能访问 this)
  beforeRouteEnter(to, from, next) {
    // 通过回调访问组件实例
    next((vm) => {
      // vm 就是组件实例
      console.log(vm.someData)
    })
  },

  // 路由参数变化(组件复用时触发)
  beforeRouteUpdate(to, from, next) {
    // this 可用
    this.fetchData(to.params.id)
    next()
  },

  // 离开路由前
  beforeRouteLeave(to, from, next) {
    if (this.hasUnsavedChanges) {
      this.$confirm('有未保存的更改,确定离开吗?')
        .then(() => next())
        .catch(() => next(false))
    } else {
      next()
    }
  },
}
</script>

动态路由 ​

路由参数 ​

js
// 路由定义
{ path: '/user/:id', name: 'User', component: User, props: true }
vue
<script>
export default {
  // 方式一:通过 $route 获取
  computed: {
    userId() {
      return this.$route.params.id
    },
  },

  // 方式二:通过 props 获取(推荐,解耦)
  props: ['id'],

  // 监听参数变化(组件复用时)
  watch: {
    '$route.params.id'(newId) {
      this.fetchUser(newId)
    },
  },

  // 或用 beforeRouteUpdate
  beforeRouteUpdate(to, from, next) {
    this.fetchUser(to.params.id)
    next()
  },

  methods: {
    async fetchUser(id) {
      const res = await api.getUser(id)
      this.user = res.data
    },
  },
}
</script>

嵌套路由 ​

js
{
  path: '/user/:id',
  component: UserLayout,
  children: [
    { path: '', name: 'UserProfile', component: UserProfile }, // 默认子路由
    { path: 'posts', name: 'UserPosts', component: UserPosts },
    { path: 'settings', name: 'UserSettings', component: UserSettings },
  ],
}
vue
<!-- UserLayout.vue -->
<template>
  <div class="user-layout">
    <nav>
      <router-link :to="{ name: 'UserProfile', params: { id } }">资料</router-link>
      <router-link :to="{ name: 'UserPosts', params: { id } }">文章</router-link>
    </nav>
    <!-- 子路由出口 -->
    <router-view />
  </div>
</template>

编程式导航 ​

js
// 字符串
this.$router.push('/home')

// 对象 —— path
this.$router.push({ path: '/home' })

// 对象 —— name + params(推荐,不暴露路径)
this.$router.push({ name: 'User', params: { id: 123 } })

// 对象 —— path + query
this.$router.push({ path: '/search', query: { keyword: 'vue', page: 1 } })

// 带 hash
this.$router.push({ path: '/about', hash: '#team' })

// 替换(不产生历史记录)
this.$router.replace({ name: 'Login' })

// 前进后退
this.$router.go(-1)  // 后退一步
this.$router.go(1)   // 前进一步
this.$router.back()  // 等同于 go(-1)

params vs query 区别 ​

js
// params —— 路径的一部分
this.$router.push({ name: 'User', params: { id: 123 } })
// URL: /user/123

// query —— 查询参数
this.$router.push({ path: '/search', query: { keyword: 'vue' } })
// URL: /search?keyword=vue

⚠️ params 不能和 path 一起使用,必须用 name。


路由元信息 (meta) ​

js
{
  path: '/dashboard',
  component: Dashboard,
  meta: {
    requiresAuth: true,   // 需要登录
    roles: ['admin', 'editor'], // 允许的角色
    title: '仪表盘',       // 页面标题
    breadcrumb: true,      // 是否显示面包屑
    keepAlive: true,       // 是否缓存
  },
}
js
// 在守卫中使用 meta
router.beforeEach((to, from, next) => {
  const title = to.matched
    .slice()
    .reverse()
    .find((r) => r.meta?.title)?.meta.title
  document.title = title || '默认标题'

  const token = localStorage.getItem('token')
  if (to.meta.requiresAuth && !token) {
    next({ name: 'Login' })
  } else {
    next()
  }
})

导航守卫执行顺序 ​

1. 导航被触发
2. 失活组件 beforeRouteLeave
3. 全局 beforeEach
4. 复用组件 beforeRouteUpdate
5. 路由配置 beforeEnter
6. 解析异步路由组件
7. 激活组件 beforeRouteEnter
8. 全局 beforeResolve
9. 导航确认
10. 全局 afterEach
11. DOM 更新触发

命名视图 ​

什么是命名视图?什么时候用?

一个页面通常只有一个 <router-view>,但有时候需要同时展示多个视图区域。比如后台管理系统:

  • 顶部是 header(导航栏)
  • 左侧是 sidebar(菜单栏)
  • 右侧是 main(主内容区)

这三个区域需要根据路由变化而变化,这时候就需要命名视图。

典型场景:

  • 后台管理系统的 header/sidebar/main 布局
  • 多面板编辑器(左侧文件树 + 右侧编辑区 + 底部终端)
  • 响应式布局(移动端和桌面端显示不同的导航)
js
{
  path: '/dashboard',
  components: {
    default: DashboardMain,    // 默认视图 <router-view>
    sidebar: DashboardSidebar, // 命名视图 <router-view name="sidebar">
    header: DashboardHeader,   // 命名视图 <router-view name="header">
  },
}
vue
<template>
  <div class="layout">
    <router-view name="header" />
    <div class="content">
      <router-view name="sidebar" />
      <router-view />
    </div>
  </div>
</template>

路由懒加载(按组分块) ​

什么是路由懒加载?为什么要分块?

默认情况下,所有页面的代码会打包成一个 JS 文件。用户访问首页时,需要下载整个文件(包含所有页面代码),导致首屏加载很慢。

路由懒加载把每个页面的代码分成独立的文件,只有访问该页面时才下载。这样:

  • 首屏只下载首页的代码,加载更快
  • 其他页面的代码按需加载,不浪费带宽
  • 浏览器会缓存已下载的页面,下次访问更快

什么是"分块"?

把相关的页面打包成一个文件。比如用户相关的页面(用户列表、用户详情、用户设置)放在同一个 chunk 中,访问其中任何一个页面时,会把整个用户模块一起下载,这样切换页面时不需要再请求。

js
// 同一组件放在同一个 chunk 中
const UserList = () => import(/* webpackChunkName: "user" */ '@/views/user/UserList.vue')
const UserDetail = () => import(/* webpackChunkName: "user" */ '@/views/user/UserDetail.vue')
const UserSettings = () => import(/* webpackChunkName: "user" */ '@/views/user/UserSettings.vue')

常见坑点 ​

1. 重复导航报错 ​

js
// ❌ NavigationDuplicated: Avoided redundant navigation
this.$router.push({ name: 'Home' })

// ✅ 方案一:捕获错误
this.$router.push({ name: 'Home' }).catch((err) => {
  if (err.name !== 'NavigationDuplicated') throw err
})

// ✅ 方案二:全局处理(推荐)
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
  return originalPush.call(this, location).catch((err) => {
    if (err.name !== 'NavigationDuplicated') throw err
  })
}

2. 404 路由必须放最后 ​

js
// ❌ 通配路由放在前面,所有路由都匹配到 404
{ path: '*', component: NotFound },
{ path: '/home', component: Home },

// ✅ 放在最后
{ path: '/home', component: Home },
{ path: '*', component: NotFound },

3. history 模式刷新 404 ​

bash
# 直接访问 /user/list 会 404,因为服务器没有这个文件
# 必须配置服务器将所有路由返回 index.html

4. params 传参刷新丢失 ​

js
// ❌ params 用 path 配置,刷新后参数丢失
this.$router.push({ path: '/user', params: { id: 123 } })

// ✅ 用 name 配置
this.$router.push({ name: 'User', params: { id: 123 } })

参考 ​

个人学习笔记,部分内容借助 AI 辅助整理,仅供查阅参考,请以官方文档为准