Vue Router 路由详解
单页应用(SPA)只有一个 HTML 页面,但用户需要"页面感"——点导航变地址、能后退、能分享链接。路由就是解决这个问题的:地址栏 URL 和页面内容的一一对应关系。Vue 生态里做这件事的就是 Vue Router(本文基于 Vue Router 4,对应 Vue 3)。
先看一个典型场景:BinMaker 给公司做了一套后台管理系统,左侧菜单十几个模块——资产管理、采购管理、报表中心……如果是传统多页应用,每个菜单就是一个 .html 文件,切菜单就是整页刷新。SPA 的玩法不同:只有一个 index.html,切菜单时用 JS 换掉中间的内容区。但这样做有个麻烦:地址栏永远停在 /#/,用户 F5 刷新直接回到首页,想给同事分享"资产详情页"只能靠嘴描述。
Vue Router 解决的就是这三件事:
- URL 与组件绑定:
/assets显示资产列表组件,/assets/123显示详情组件 - 浏览历史:前进、后退、刷新都保持当前页面(这是 SPA 最容易丢的体验)
- 导航控制:登录才能进的管理页、离开前的确认提示、路由级懒加载
版本对应:Vue Router 3 还是 4?
写代码之前,先把版本说清楚——Vue Router 3 配 Vue 2,Vue Router 4 配 Vue 3,两者 API 不兼容:
| 维度 | Vue Router 3(配 Vue 2) | Vue Router 4(配 Vue 3) |
|---|---|---|
| 创建方式 | new VueRouter({ routes }) | createRouter({ history, routes }) |
| 挂载 | new Vue({ router }) | app.use(router) |
| history 模式 | mode: 'history' / 'hash' | createWebHistory() / createWebHashHistory() |
| 动态路由 | router.addRoutes() | router.addRoute()(单条) |
| 类型支持 | JS 为主 | 原生 TS 重写,类型完备 |
| 现状 | 维护模式,仅兼容 Vue 2 | 活跃开发,本文基于此 |
判断口诀:看到
new VueRouter是 3 代,看到createRouter是 4 代。老项目升级 Vue 3 时必须同步升级 Router。
路由怎么工作:一张流程图
Vue Router 4 的核心机制分三块:路由表(URL 与组件的映射)、history(地址栏变化怎么感知)、匹配与渲染(URL 变了怎么换组件)。流程如下:
流程拆开看:用户在地址栏输入或点击 <router-link> 触发导航 → 路由表匹配出对应的组件(可能经过守卫拦截)→ 渲染到 <router-view> 出口。整个过程不刷新页面。
快速上手:写一个最简单的路由
<!-- App.vue -->
<nav>
<router-link to="/">首页</router-link>
<router-link to="/assets">资产管理</router-link>
</nav>
<!-- 路由组件渲染出口 -->
<router-view />// router/index.js —— Vue Router 4 的创建方式
import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue';
import Assets from '../views/Assets.vue';
const router = createRouter({
history: createWebHistory(), // 地址栏干净,无 # 号
routes: [
{ path: '/', component: Home },
{ path: '/assets', component: Assets },
],
});
export default router;// main.js —— 挂到 app 上
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
createApp(App).use(router).mount('#app');三个关键点:
createRouter+history:Vue Router 3 的mode配置变成了显式的 history 函数,三种模式见下文<router-view>:路由匹配到的组件渲染到这里,类似插槽的出口<router-link>:渲染成<a>标签,点击不刷新页面、只更新内容区
三种 history 模式怎么选
| 模式 | 创建函数 | 地址栏形态 | 适用场景 |
|---|---|---|---|
| Web History | createWebHistory() | https://site.com/assets | 最推荐,URL 干净、利于 SEO |
| Hash | createWebHashHistory() | https://site.com/#/assets | 静态托管、无服务端转发能力 |
| Memory | createMemoryHistory() | 无真实地址 | 测试、SSR,不依赖浏览器 |
Web History 模式有个硬性前提:服务端要把所有路径都指向 index.html(Nginx 配 try_files $uri $uri/ /index.html),否则刷新 /assets 会 404。Hash 模式靠 # 后面的内容变化触发导航,不经过服务器,所以任何静态托管都能用,代价是 URL 带 # 不好看。
实跑验证过的一个冷知识:Memory 模式的对象只有
go()方法、没有back()/forward()(浏览器历史语义专属),所以它定位就是测试和 SSR 场景,别在浏览器环境当主模式用。
动态路由:/assets/123 这种地址怎么配
列表页 → 详情页是后台系统最经典的路由场景。详情页地址带 id,用路径参数:
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/assets', component: AssetsList },
{ path: '/assets/:id', component: AssetDetail }, // :id 是动态段
],
});组件里读取参数:
<template>
<div>当前资产编号:{{ route.params.id }}</div>
</template>import { useRoute } from 'vue-router';
const route = useRoute();
console.log(route.params.id); // 从 URL 里解析出的 123实跑验证的行为:
/assets/123匹配/assets/:id,params.id === '123'(字符串)- 用命名路由跳转更稳:
router.push({ name: 'assetDetail', params: { id: '456' } }),不依赖字符串拼接
注意:从 /assets/1 切到 /assets/2 时,组件会被复用(同一个组件实例),onMounted 不会重新执行——要在 watch(() => route.params.id, ...) 里响应参数变化,或者给 <router-view :key="route.fullPath"> 强制重建。
嵌套路由:菜单有层级怎么办
后台系统的菜单几乎都是树状的——"资产管理"下挂"列表"和"详情"。嵌套路由用 children:
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/assets',
component: AssetsLayout, // 父级布局(含侧边栏 + 子路由出口)
children: [
{ path: '', component: AssetsList }, // /assets
{ path: 'create', component: AssetCreate }, // /assets/create
{ path: ':id', component: AssetDetail }, // /assets/123
],
},
],
});父组件 AssetsLayout.vue 里必须有 <router-view />,子路由的组件渲染在这里。实跑验证:访问 /assets/create 时,路由匹配了两层组件(父布局 + 子页面),这就是"嵌套路由 matched 两级"的含义。
导航守卫:谁有权进这个页面
后台系统逃不开权限控制——未登录用户访问管理页要踢回登录页。Vue Router 4 提供全局守卫、路由级守卫、组件内守卫三级机制,最常用的是全局 beforeEach:
router.beforeEach((to, from) => {
const token = localStorage.getItem('token');
// to.meta 是路由配置里自定义的元信息
if (to.meta.requiresAuth && !token) {
return { path: '/login', query: { redirect: to.fullPath } }; // 重定向并记录来路
}
return true; // 放行
});// 路由表里标记哪些页面需要登录
{ path: '/assets', component: AssetsList, meta: { requiresAuth: true } },守卫函数的返回值语义(实跑验证过):
| 返回值 | 行为 |
|---|---|
true | 放行,继续导航 |
false | 取消本次导航,停在原地 |
| 路由对象 / 字符串 | 重定向到新地址 |
不返回 / undefined | 放行(慎用,建议显式 return true) |
另外两个钩子:
afterEach(to, from):导航完成后触发,适合做页面标题、埋点统计(没有返回值)beforeRouteLeave(组件内):离开前确认,比如"表单未保存,确定离开?"
守卫的执行顺序:beforeEach(全局)→ beforeEnter(路由级)→ 组件内守卫 → afterEach(全局)。守卫可以串联多个,按注册顺序执行。
路由懒加载:首屏为什么快
后台系统页面多了,把所有组件一次性打进 bundle.js,首屏下载几百 KB 起步。路由懒加载让每个页面只在被访问时才加载:
// 按需加载:访问 /assets 时才下载 AssetsList 的 chunk
{ path: '/assets', component: () => import('../views/AssetsList.vue') },Vite/webpack 会把 import() 的模块拆成独立 chunk,<router-view> 渲染前自动加载对应 chunk。这是 SPA 首屏性能优化性价比最高的一招,配合 Suspense 还能做加载态。
总结:什么时候用什么
| 场景 | 用什么 |
|---|---|
| 地址栏干净 + 有服务端 | Web History(默认推荐) |
| 纯静态托管 / 无服务端转发 | Hash 模式 |
| 单测 / SSR | Memory 模式 |
| 详情页 / 列表页带参 | 动态路由 :id + useRoute().params |
| 菜单多级 | 嵌套路由 children |
| 登录 / 权限控制 | beforeEach + meta.requiresAuth |
| 首屏优化 | 组件懒加载 () => import() |
版本提醒:本文是 Vue Router 4(配 Vue 3);如果你还在维护 Vue 2 老项目,路由写法是 new VueRouter({ mode: 'history' }),差异对照见 Vue 版本演进:Vue 2 与 Vue 3 全对比。
链式导航:路由解决了"页面怎么切",那页面里的数据怎么管?看 Pinia 状态管理详解;路由组件本身就是 Vue 组件,通信方式见 Vue 组件通信全景;刚接触 Vue 先看 Vue 快速入门。
