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.html4. params 传参刷新丢失
js
// ❌ params 用 path 配置,刷新后参数丢失
this.$router.push({ path: '/user', params: { id: 123 } })
// ✅ 用 name 配置
this.$router.push({ name: 'User', params: { id: 123 } })