
Vue3 项目做多了之后你会发现一个现象真正决定一个前端项目骨架的不是组件怎么写而是路由怎么设计。很多刚接触 Vue3 的同学上来就啃响应式、组合式 API结果路由配不明白项目半天跑不起来。这篇研学笔记专门讲 Vue3 生态里的路由部分——从最基础的 vue-router 4 安装接入到路由表设计、跳转方式、参数传递再到新人经常踩的坑一次性帮你理清楚。如果你是刚接触 Vue3 的开发者按照这篇笔记的顺序从头跟到尾你会知道路由实例是怎么创建的路由表是怎么写的页面之间是怎么跳转的参数又是怎么传递和接收的。如果你已经能写点 Vue3 但一直没系统整理过路由这篇也能帮你查缺补漏。后面我还会持续更新 Vue3 研学系列这篇是第一篇所以难度控制在入门到进阶之间尽量讲透不讲虚的。1. 为什么研学 Vue3 要先把路由搞清楚1.1 路由到底解决什么问题前端路由这个事说小了是页面跳转说大了是应用架构。你打开一个网站点一个链接地址栏变了页面内容也变了这个变里面就包含了一套路由机制。放在多页应用时代每次跳转都是浏览器发起一个新请求服务器返回一个新页面整个页面会闪一下、白屏一次。到了 Vue3 这样的单页应用我们把路由逻辑放到浏览器端处理页面内容动态替换地址和内容仍然保持一致但页面不用整页刷新。Vue3 生态里承担这个工作的就是 vue-router 4。它基于 Vue3 的响应式系统实现提供路由记录、匹配、守卫、懒加载等一系列能力。把路由配好你的项目就有了清晰的页面组织和切换逻辑配不好后面加页面、做权限、搞嵌套布局都会变得很难受。它本质上是一个地址到组件的映射表更像是整个应用的路标系统决定用户走到哪个 URL 时看到哪块界面。1.2 前端路由的两条技术路线前端路由有两条主线一条叫 hash 模式一条叫 history 模式。hash 模式是地址栏里以 # 开头的那段内容比如http://localhost:5173/#/user/1。hash 的特点是它变化时不会向服务器发请求所以刷新页面不会出现 404部署起来不挑服务器。缺点是地址栏里带个 # 比较丑而且搜索引擎对这部分的抓取支持比较弱做面向公众的站点时不够友好。history 模式用的是 HTML5 History API地址看起来就是正常的路径比如http://localhost:5173/user/1。它美观、语义清晰但开发环境没问题生产环境如果服务器没做 fallback 配置用户刷新非根路径就会 404。具体怎么解决我在后面第 5 章会专门讲。我一般建议实际业务项目用 history 模式本地开发、部署都方便。这两个模式在 vue-router 4 里就是一行代码的差别。如果你暂时拿不准可以先从 hash 模式入手跑通功能后再切 history两者切换成本不高但会影响地址形态和部署配置别拖到上线前才想起换。1.3 一篇能覆盖的基础范围既然是系列第一篇我不想把范围拉得太宽。这一篇主要覆盖vue-router 4 的安装与接入、路由表的基本写法、路由跳转、参数传递、嵌套路由以及新人常见问题。像路由守卫、权限控制、动态路由这些内容放到后面篇章讲因为那需要你有更扎实的组件和全局思维基础。你读完这篇之后应该有的能力是能独立搭建一个带导航和两个页面的 Vue3 项目能在页面之间跳转并且传递参数能解释清楚自己项目里用的是哪种路由模式遇到路由空白、刷新 404 时知道往哪个方向排查。这些能力是后面所有路由高阶特性的地基。2. 从零开始装好 vue-router 并跑通第一个路由2.1 项目准备与安装版本选择路由不是 Vue3 自带的你需要单独装。最省事的方式是用官方脚手架创建项目npm create vuelatest创建时它会问你一堆问题比如是否使用 TypeScript、是否配置 Router。如果选了 Router脚手架会自动装好 vue-router 并生成一套路由目录。但为了说明原理我更推荐你选不装 Router自己手动安装一遍这样你能知道整个路由体系是怎么串起来的而不是拿了一套生成好的配置不知所以然。手动安装命令npm install vue-router4注意版本Vue3 必须配 vue-router 4。你要是装老项目留下来的 vue-router 3Vue3 根本用不了到处报错。这个我在带团队新人时见过太多第一坑往往不是配置而是版本。如果你用的是 Vite 构建安装完成后最好检查一下 package.json确认依赖里确实有 vue-router且版本号是 4.x 开头。2.2 路由实例与挂载流程装好之后你需要创建路由实例。标准的做法是在 src 下建一个 router 目录写 index.jsimport { createRouter, createWebHistory } from vue-router import HomeView from ../views/HomeView.vue const router createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: /, name: home, component: HomeView } ] }) export default routercreateRouter 就是 vue-router 4 的入口函数接收一个配置对象。history 字段决定模式这里我用的是 createWebHistory也就是 history 模式如果你想用 hash 模式把它换成 createWebHashHistory 就行。routes 数组就是路由表定义了当前地址对应渲染哪个组件。然后去 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.use(router)的顺序。必须在mount之前挂载因为应用启动时要先让全局路由生效组件里才能使用路由相关能力。顺序反了页面经常白屏或者拿不到路由信息。你可以把这个过程理解为先把路由服务注册到应用里告诉 Vue这个项目是有路线的然后再把应用挂到页面上开始运行。2.3 模板里的两个关键组件路由接入后要在模板里渲染路由匹配到的组件需要用到 router-view要实现跳转需要用到 router-link。比如根组件 App.vuetemplate router-view / /template这一行就是整个应用的出口。路由匹配到的组件会渲染到 router-view 所在的位置。你还可以在 App.vue 里放导航template nav router-link to/首页/router-link router-link to/about关于/router-link /nav router-view / /templaterouter-link 是一个特殊的组件它最终渲染成 a 标签但跳转逻辑由 vue-router 接管不会触发整页刷新。点击时它还会自动给当前匹配的导航加一个 router-link-active 类方便你做高亮样式。这个类名的控制粒度比较细默认是当前路径包含匹配的导航都会加上如果你希望完全精确匹配可以使用 exact-active-class 或者用精确路由匹配来控制样式。细节很多后面跳转篇我再展开。3. 路由表设计三种最常用的映射关系3.1 静态路由与页面级组件路由表就是把 path 映射到 component。最简单的是静态路径routes: [ { path: /, name: home, component: HomeView }, { path: /about, name: about, component: () import(../views/AboutView.vue) } ]这里出现了两种写组件的方式。第一种是直接 import 进来打包时这个组件会打进主包第二种是箭头函数动态 importvue-router 遇到这个组件时会单独分包只有当用户访问对应路径时才加载这就是路由懒加载。生产环境里我强烈建议所有页面级组件都用懒加载不然首页要一次性下载所有页面的代码打包体积大、首屏慢。可能有人会担心动态 import 会影响首屏性能实际上恰恰相反。懒加载是把首屏不需要的代码拆出去首屏加载的资源反而更少。等你需要跳转到那个页面时浏览器再去加载对应分块用户感知到的等待时间通常也更短。现代构建工具对动态 import 的支持已经很成熟你唯一要注意的是路径别写错相对路径的层级关系要仔细核对。3.2 动态路由参数真实项目里用户详情、文章详情这些页面路径通常是/user/1、/post/123。这种写法叫动态路径参数{ path: /user/:id, name: user-detail, component: () import(../views/UserDetailView.vue) }:id就是动态参数访问/user/1和/user/2会匹配到同一个组件组件里可以通过 route.params.id 拿到当前用户的 id。这大大减少了路由表的重复配置。常见的设计误区是把每个数字都配成静态路由比如/user/1、/user/2这是错的。动态参数就是用来处理这种重复模式的。除了用户 id文章 id、订单号、分类 slug 这类值都应该作为动态参数。你还可以在同一个 path 里定义多个参数比如/category/:categoryId/item/:itemId它们会形成层级关系只要路径结构一致就能被同一个组件接收处理。3.3 嵌套路由搭建布局骨架后台管理系统最常见的需求顶部导航、侧边栏、内容区整个布局是固定的只是内容区随路由变化。这时候就要用嵌套路由{ path: /dashboard, component: () import(../layouts/AdminLayout.vue), children: [ { path: , name: dashboard-home, component: () import(../views/DashboardHome.vue) }, { path: users, name: dashboard-users, component: () import(../views/UserList.vue) }, { path: settings, name: dashboard-settings, component: () import(../views/SettingsView.vue) } ] }在这个配置里父路由对应 AdminLayout它内部要放一个 router-view子路由匹配到的组件会渲染到那个 router-view 里!-- AdminLayout.vue -- template div classlayout aside菜单/aside main router-view / /main /div /template嵌套路由可以无限层级往下套。路径拼接规则是父路径加子路径。子路由的 path 如果以 / 开头会被当作绝对路径处理不以 / 开头会自动拼接父路径。我在项目里习惯用相对路径因为父组件一改子路由不用跟着改。注意一个容易忽略的点嵌套路由里父路由的 component 必须渲染一个 router-view否则子路由匹配到了也没有出口页面看起来就是空白。4. 路由跳转与参数接收的实操演示4.1 声明式跳转 router-link 的细节router-link 最常见的两种传参方式一种是 path query一种是 name paramsrouter-link :to{ path: /user, query: { from: list } }用户列表/router-link router-link :to{ name: user-detail, params: { id: 1 } }用户 1/router-link这两种方式接收参数也不同。path 方式跳转参数在 route.query 里name 方式跳转参数在 route.params 里。混合使用容易出问题。比如用 name 跳转传了 query虽然也能工作但 url 上会出现?fromxxx行为会变得不好预测。我的经验是需要语义化路径参数比如 /user/:id就统一用 name params跟页面数据强相关的参数比如搜索条件、来源标记就单独用 query。还有一个容易踩的细节router-link 的 to 如果写成静态字符串比如to/user/1vue-router 会自动解析匹配如果写成对象且要用 params那么必须同时写 name不能只写 path 加 params。path 加 params 会被忽略因为 vue-router 认为你已经指定了一个静态路径不需要再拼 params。这个坑特别容易出现在从旧项目迁移过来的人身上。4.2 编程式跳转 useRouter页面里很多时候不是点击链接跳转而是某个方法执行完跳转。比如表单提交成功后跳详情页。这时候需要在 setup 里拿路由实例import { useRouter } from vue-router const router useRouter() function goDetail(id) { router.push({ name: user-detail, params: { id } }) }router.push 和 router-link 的 to 接收一样的对象。push 会往历史栈里加一条记录相当于前进如果你想替换当前记录用 router.replace用户点浏览器返回不会回到上一个页面。登录成功跳首页、表单提交后跳列表这类场景replace 更合适因为你不希望用户按返回键又回到登录页。还有一个区别要说push 同一个路径时vue-router 4 会返回一个 Promise。重复跳转同一路径可能会触发 NavigationDuplicated 警告旧版本这个问题比较明显4.x 已经处理得好很多但你自己判断到同一路由时应该尽量避免重复 push。更稳妥的做法是在跳转前做个判断比如当前已经在目标页面就直接 return。4.3 参数读取 useRoute 与组件解耦组件里读取参数标准的做法是 useRouteimport { useRoute } from vue-router const route useRoute() const userId route.params.id这是最直接的方式。但有个问题组件一旦依赖 route.params就相当于和 url 耦合了做单元测试、复用组件都会很别扭。vue-router 4 支持在路由表配置里把 params 映射成组件 props{ path: /user/:id, name: user-detail, component: () import(../views/UserDetailView.vue), props: true }打开 props 后组件里直接声明 props.id 就能拿到参数script setup defineProps({ id: { type: [String, Number], required: true } }) /script这样 UserDetailView 变成一个依赖 props 的组件跟路由解耦。想预览直接传 id 给它就行。我在项目里的习惯是页面组件的参数都用 props: true 接管导航相关逻辑全部丢给父级或路由层组件自己保持笨一点。这个习惯能让你后面写测试、做组件复用的时候轻松很多。5. 常见问题排查实录5.1 页面空白常见原因新配路由后页面空白首先查三处。第一main.js 里有没有在 mount 前 app.use(router)。第二路由表里组件路径有没有写错特别是../views/xxx.vue这种相对路径多写一层少写一层打包时直接报 Module not found但这个报错很容易被忽略因为浏览器里只有白屏。第三App.vue 里有没有放 router-view。有时新建项目后模板被改过router-view 丢了那自然什么都渲染不出来。页面空白还有一个隐蔽原因路由表里有重复的 pathvue-router 会直接报错提示重复路由重名控制台其实有信息只是好多人不看控制台。排查的第一步永远是开控制台。浏览器 F12 打开 console任何报错信息都比瞎猜快。我见过一些同学在群里问为什么白屏一问 console 有红色报错只是没看而已。5.2 刷新后 404 的原因开发环境一切正常部署到服务器后用户访问/user/1刷新一下直接 404这就是典型的 history 模式服务器 fallback 问题。history 模式刷新时浏览器会向服务器请求/user/1这个地址但服务器上根本没有这个物理文件就返回 404。解决方法是让服务器把所有非静态文件请求都指向 index.html也就是常说的单页应用 fallback。Nginx 配置里类似这样location / { try_files $uri $uri/ /index.html; }如果你只用 hash 模式就没这个问题。所以很多时候不是代码写错了是部署环境的问题。这也是我建议项目初期就决定好到底用哪种模式的原因切换成本看似不高但各种历史遗留链接、服务端配置都会受影响。尤其是对接第三方登录回调、分享链接这类场景URL 形态一变坑就来了。5.3 同一路由参数变化组件不刷新的处理从/user/1跳到/user/2同一个组件实例会被复用Vue 不会重新创建组件生命周期钩子不会重新执行。新开发者经常在这里困惑为什么我换个 id页面数据还是旧的。原因是组件实例被缓存了setup 只执行一次。你需要监听参数的变化import { watch } from vue import { useRoute } from vue-router const route useRoute() watch( () route.params.id, (newId) { // 重新请求数据 } )如果你不想保留旧数据闪烁也可以在 router-view 上加 :key指定 key 让组件的不同参数认为是不同实例router-view :key$route.fullPath /这种方式适合对性能不敏感、更在意数据一致性的场景。两种方案我都用过经验是列表进详情这种只需加载一次的用 watch需要彻底重建、比如多个 tab 之间用不同参数切换的用 key 更省心。两种方式没有绝对的对错看你的数据展示需求。5.4 路由常见报错整理一个表格帮你快速对照报错场景常见原因快速处理控制台提示 No match found for location路径配置与访问地址不一致检查路由表 path确认是否缺少通配或子路由组件不渲染且无报错组件导出方式错误或 router-view 缺失检查是否使用 export default检查根组件模板Cannot resolve dependency安装版本兼容问题或路径引用错误检查 package.json 中的 vue-router 版本重复 path 警告路由表有相同路径合并路由配置或移除重复项页面跳转后 URL 变了但内容没变router-view 位置错误或被组件覆盖检查 App.vue 的 router-view 是否被误删跳转时 NavigationDuplicated重复 push 同一路由跳转前判断当前路由或改用 replace6. 第一篇的收尾心得6.1 知识点自查表读完这篇文章你至少应该能回答这几个问题hash 模式和 history 模式的区别是什么为什么建议用懒加载name 和 path 跳转传参有什么不同嵌套路由的父子怎么配置如果一个动态路由参数变化时组件没更新你会怎么处理如果都有答案了这篇的基础目标就达到了。你还可以试着动手做一个小练习搭一个带首页、关于页、用户详情页的项目首页用 router-link 跳转关于页用编程式跳转详情页配一个动态参数并接收展示。这个小练习做完你基本上就能把这篇所有知识点串起来。我在实际项目里经常遇到一种情况路由配置写得很乱各种写法混在一起后面维护的人看得头大。所以从一开始就养成统一的配置习惯特别重要比如静态路由统一用懒加载、动态路由统一开 props、嵌套路由统一用相对路径。这些习惯不会立刻带来明显收益但项目大到一定规模后价值就出来了。6.2 下一步研学方向路由一到这里结束。后面系列里我会把路由守卫讲透——beforeEach、afterEach 怎么用登录态校验放在哪路由级权限怎么控制。还会讲动态路由的高阶用法根据后端返回的权限表动态添加路由、按钮级权限怎么配合。另外响应式路由、路由动画这些都是很有意思的进阶点。我个人在实际操作中最大的体会是路由表不是写一次就完事的东西它像项目的地基项目每长一截路由就要跟着调整一截。前期配置稍微规范一点后面加页面、做权限都会省很多事。很多看起来玄乎的问题比如为什么这个页面刷新就 404为什么跳转之后数据没变本质都是对路由生命周期理解不透。把你手里的项目打开看一眼看看自己的路由表是怎么写的、用的是什么模式、组件之间是怎么跳转的先对照这篇检查一遍下一篇见。