Vue Router 深度详解
Vue Router 深度详解
Vue Router 是 Vue.js 官方的路由管理器,与 Vue.js 深度集成,让构建单页面应用变得简单。本文详细讲解 Vue Router 的核心概念、底层原理、高级用法和最佳实践,覆盖从入门到工程化落地的完整链路。
零、先理解"前端路由"到底是什么
在传统多页应用中,每个 URL 对应服务器上的一个真实文件,点击链接就是让浏览器重新向服务器要一份新 HTML。而在单页应用(SPA)里,整个站点只有一份 HTML,"切换页面"其实是 JavaScript 在前端动态替换 DOM。前端路由就是负责"监听 URL 变化 → 找到对应组件 → 渲染到页面"这套机制的模块。
可以用一个类比理解:Vue Router 就像一部电梯的楼层控制系统。URL 是你按下的楼层按钮,路由表是电梯知道每层是什么的映射表,router-view 是电梯门打开后你走进的那个楼层空间。整栋楼(页面)不动,只是把你送到不同楼层(组件)。
为什么这件事重要?
| 能力 | 没有前端路由 | 有 Vue Router |
|------|--------------|---------------|
| URL 可分享 | 否,永远是同一个地址 | 是,每个视图有独立 URL |
| 浏览器前进后退 | 失效 | 正常工作 |
| 首屏加载 | 每次切换都白屏 | 只加载一次,切换无刷新 |
| SEO / 埋点 | 难以按页面统计 | 可按路由统计 PV |
| 代码组织 | v-if 地狱 | 声明式路由表 |
一、Vue Router 基础
1. 安装与配置
Vue Router 4(Vue3)安装:
npm install vue-router@4Vue Router 3(Vue2)安装:
npm install vue-router@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;在 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');在根组件放置出口与导航:
<!-- App.vue -->
<template>
<header>
<router-link to="/">首页</router-link>
<router-link to="/about">关于</router-link>
</header>
<main>
<!-- 当前路由匹配的组件渲染在这里 -->
<router-view />
</main>
</template>Vue Router 3 与 4 的注册差异对比:
// Vue Router 3(Vue2)
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter);
const router = new VueRouter({ mode: 'history', routes });
new Vue({ router, render: h => h(App) }).$mount('#app');
// Vue Router 4(Vue3)
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({ history: createWebHistory(), routes });
createApp(App).use(router).mount('#app');2. 路由模式
路由模式决定了 URL 的形态以及底层监听 URL 变化的技术方案。
History 模式(推荐):
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(),
routes
});URL 示例:https://example.com/user/123
原理:基于 HTML5 History API 的 history.pushState() 和 history.replaceState(),它们能在不刷新页面的前提下修改地址栏 URL,同时通过监听 popstate 事件感知用户点击前进/后退。
Hash 模式:
import { createRouter, createWebHashHistory } from 'vue-router';
const router = createRouter({
history: createWebHashHistory(),
routes
});URL 示例:https://example.com/#/user/123
原理:URL 中 # 后面的部分称为 hash,它的变化不会触发浏览器向服务器发起请求,同时会触发 hashchange 事件,Vue Router 借此实现前端路由。因此 hash 模式无需任何服务器配置。
Memory 模式(适用于 Node.js 环境):
import { createRouter, createMemoryHistory } from 'vue-router';
const router = createRouter({
history: createMemoryHistory(),
routes
});这种模式不依赖浏览器地址栏,路由状态保存在内存中,常用于 SSR(服务端渲染)、单元测试、以及非浏览器环境。
三种模式对比:
| 模式 | 底层 API | URL 形态 | 需服务器配置 | 典型场景 |
|------|----------|----------|--------------|----------|
| History | pushState / popstate | 干净无 # | 是 | 生产环境主流 |
| Hash | location.hash / hashchange | 带 # | 否 | 静态托管、老浏览器 |
| Memory | 内存栈 | 无地址栏 | 否 | SSR、测试 |
History 模式的服务器配置(否则刷新会 404):
# Nginx
location / {
try_files $uri $uri/ /index.html;
}// Node.js / Express
const history = require('connect-history-api-fallback');
app.use(history());
app.use(express.static('dist'));二、路由配置详解
1. 路由参数
动态路由参数:
const routes = [
{
path: '/user/:id',
name: 'User',
component: User,
props: true // 将路由参数作为 props 传递
}
];组件中获取参数:
<script setup>
import { useRoute } from 'vue-router';
const route = useRoute();
// 方式 1:通过 route.params
console.log(route.params.id);
// 方式 2:通过 props(需要设置 props: true)
defineProps(['id']);
</script>为什么推荐 props: true? 它让组件不再直接依赖 $route,组件变得更纯粹、更易测试与复用。你甚至可以在单元测试里直接给组件传 id,而不需要 mock 整个路由。
// props 还可以是函数,做转换或注入静态数据
{
path: '/user/:id',
component: User,
props: (route) => ({
id: Number(route.params.id), // 把字符串转成数字
query: route.query.q
})
}多个动态参数:
{
path: '/user/:userId/post/:postId',
name: 'UserPost',
component: UserPost
}
// 访问 /user/1/post/99 时:
// route.params.userId === '1'
// route.params.postId === '99'可选参数:
{
path: '/user/:id?',
name: 'User',
component: User
}
// /user 和 /user/123 都能匹配带正则约束的参数:
{
// 仅匹配纯数字 id,/user/abc 不会命中此路由
path: '/user/:id(\\d+)',
component: User
}*通配符参数(Vue Router 4 已移除 ,使用 catch-all 路由):**
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: NotFound
}2. 查询参数
传递查询参数:
<template>
<router-link :to="{ path: '/search', query: { q: 'vue', page: 1 } }">
搜索 Vue
</router-link>
</template>
<script setup>
import { useRouter } from 'vue-router';
const router = useRouter();
// 编程式导航
router.push({
path: '/search',
query: { q: 'vue', page: 1 }
});
</script>获取查询参数:
<script setup>
import { useRoute } from 'vue-router';
const route = useRoute();
console.log(route.query.q); // 'vue'
console.log(route.query.page); // '1'(注意始终是字符串)
</script>响应式监听查询参数变化:
<script setup>
import { useRoute } from 'vue-router';
import { watch } from 'vue';
const route = useRoute();
watch(
() => route.query,
(newQuery) => {
// 分页、筛选变化时重新拉取数据
fetchList({ q: newQuery.q, page: Number(newQuery.page) || 1 });
},
{ immediate: true }
);
</script>3. 路由传参方式对比
| 方式 | 位置 | 刷新后 | 是否需在 path 声明 | 适用场景 |
|------|------|--------|--------------------|----------|
| params | URL 路径 | 保留 | 是(:id) | 资源标识 |
| query | URL 查询 | 保留 | 否 | 搜索、过滤、分页 |
| state | history.state | 丢失 | 否 | 临时数据、不宜暴露在 URL |
使用 state 传参:
router.push('/user/123', {
state: { from: 'home' }
});
// 获取 state
const location = router.currentRoute.value.state.from;为什么有 state? 有些数据不适合放在 URL 里(太长、敏感、或纯粹的临时上下文),state 把数据挂在 history 记录上,好处是不污染 URL,代价是刷新页面就会丢失。适合"从哪个页面跳过来"这类一次性信息。
三、导航守卫
导航守卫本质上是"在路由跳转的各个阶段插入钩子函数",用于鉴权、埋点、数据预取、离开确认等横切逻辑。
1. 全局守卫
全局前置守卫:
router.beforeEach((to, from, next) => {
console.log('全局前置守卫');
console.log('to:', to.path);
console.log('from:', from.path);
// 权限检查示例
const token = localStorage.getItem('token');
if (to.meta.requiresAuth && !token) {
next('/login');
} else {
next();
}
});Vue Router 4 更推荐的返回值写法(不再必须调用 next):
router.beforeEach((to, from) => {
const token = localStorage.getItem('token');
if (to.meta.requiresAuth && !token) {
// 返回一个路由地址进行重定向
return { name: 'Login', query: { redirect: to.fullPath } };
}
// 返回 true 或 undefined 表示放行;返回 false 表示取消导航
});全局解析守卫:
router.beforeResolve(async (to, from) => {
// 在所有组件内守卫和异步路由组件被解析后调用
// 适合做"确保数据加载完成再进入"的最终检查
if (to.meta.requiresData) {
await preloadData(to);
}
});全局后置守卫:
router.afterEach((to, from) => {
console.log('全局后置守卫');
// 页面标题设置
document.title = to.meta.title || '默认标题';
// 埋点上报
sendPageView(to.fullPath);
});后置守卫不接收 next,也不能改变导航,因为此时导航已经完成,它只适合做"副作用",如改标题、埋点、关闭 loading。
2. 路由独享守卫
const routes = [
{
path: '/dashboard',
component: Dashboard,
beforeEnter: (to, from, next) => {
console.log('路由独享守卫');
const token = localStorage.getItem('token');
if (!token) {
next('/login');
} else {
next();
}
}
}
];beforeEnter 也支持传入数组,组合多个守卫:
function requireAuth(to, from, next) {
localStorage.getItem('token') ? next() : next('/login');
}
function requireAdmin(to, from, next) {
isAdmin() ? next() : next('/403');
}
const routes = [
{ path: '/admin', component: Admin, beforeEnter: [requireAuth, requireAdmin] }
];3. 组件内守卫
<script>
export default {
// 进入组件前
beforeRouteEnter(to, from, next) {
console.log('beforeRouteEnter');
// 不能访问 this,因为组件实例还没创建
next(vm => {
// 可以通过 vm 访问组件实例
console.log(vm);
});
},
// 组件更新前(同一组件被复用,仅参数变化时触发)
beforeRouteUpdate(to, from, next) {
console.log('beforeRouteUpdate');
// 可以访问 this
next();
},
// 离开组件前
beforeRouteLeave(to, from, next) {
console.log('beforeRouteLeave');
const answer = window.confirm('确定要离开吗?未保存的更改将丢失。');
next(answer);
}
};
</script>在