免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

Vue Router从入门到实践:SPA路由的核心机制与踩坑指南

Vue Router从入门到实践:SPA路由的核心机制与踩坑指南 前阵子有个朋友找我排查一个“页面跳动”的问题。他的Vue项目点菜单跳转时页面总会闪一下白底然后新内容才出现。我看完代码发现问题的根源不在CSS也不在某段异步逻辑而是整份代码里完全没有引入vue-router全用window.location.href在跳转。这相当于把单页应用做成了老式多页应用每次跳转浏览器都要重新拉一遍HTML文档不白屏才怪。这件事给留给我的印象很深很多前端新人把“用Vue框架”等同于“用了组件化”却忽略了路由才是SPA的骨架。vue-router不是可有可无的插件它决定了URL怎么跟界面联动、刷新后能不能保持位置、多级页面怎么组织、权限拦截写在哪里。这篇内容就从vue-router的定位讲起覆盖安装配置、重定向、路由模式这几个核心话题最后把我在实际项目里踩过的坑也一并写出来希望对正在系统学Vue的人有帮助。1. 为什么单页应用必须要有前端路由1.1 从多页面跳转到SPA一次观念转变在没有Vue、React这类框架的年代网站普遍是MPAMulti-Page Application结构。你有几个页面就放几个HTML文件index.html、about.html、contact.html跳转靠a标签或者直接写window.location.href。每次跳转浏览器都要向服务器重新发起请求拿回一个完整的HTML文档然后整页白屏、重绘、重新加载资源。整套流程用户能明显感觉到“卡了一下”尤其网络不好时那个转圈等待的过程非常劝退。SPASingle Page Application单页应用的思路则完全不同。整个应用只加载一个index.html壳子后续所有“页面切换”都不再向服务器要HTML文档而是由JavaScript动态替换页面内容显示区域。这样带来的体验提升是质变的切换速度接近原生应用页面状态可以保留来回跳转也不会丢失表单里填了一半的内容。不过SPA也顺手丢掉了传统网页最基础的东西——浏览器地址栏。如果所有内容都在同一个页面里动态切换那地址栏里的URL就是个摆设用户没法收藏某个具体页面也没法把一个带参数的列表页链接发给同事。1.2 前端路由的本质URL与组件之间的映射关系前端路由解决的就是上面这个问题。它把URL变化与组件的渲染绑定在一起URL从/user/list变成/user/detail/1时路由会拦下这次变化解析出/user/detail/1这笔规则找出应该渲染哪个组件再通过router-view这个容器把组件渲染出来。你可以把它理解成一张翻译表URL是用户的输入语言组件是前端最终要展示的内容路由就是中间那个翻译官。真正转起来之后你会发现路由要处理的事情远不止“几个if else判断URL”那么简单。URL的解析、动态参数提取、路由守卫、懒加载、嵌套路由、重定向、404兜底这些全是路由的活。手写一套简易路由其实不难但边界情况会磨到你怀疑人生。vue-router之所以成为Vue生态里最基础的库之一正是因为它把这一整套链路都成熟地封装好了我们只需要维护那张路由表剩下的事情交给它。2. 安装与初始化vue-router版本选对了吗2.1 Vue2配vue-router 3Vue3配vue-router 4在动手敲第一条路由之前先确认你的Vue版本这是新手很容易踩的第一道坎。Vue2对应的是vue-router 3.xVue3对应的是vue-router 4.x。如果直接在项目里执行npm install vue-routernpm会默认装最新大版本4.x拿到Vue2项目里一跑控制台立刻报错最常见的表现是this.$router拿不到、组件里显示不出路由出口。Versions不匹配导致的报错往往比“路由配置写错了”更难排查因为错误提示并不直观。安装时明确指定版本号# Vue 2 项目 npm install vue-router3 # Vue 3 项目 npm install vue-router4如果是用Vite从零创建Vue3项目也可以执行npm create vuelatest创建向导里会有一个“是否安装Vue Router”的交互选项选“是”之后脚手架自动把路由目录结构也一并生成好。这种方式最适合新项目省去自己写初始化配置的功夫。2.2 最小化配置路由实例如何在入口文件注册无论用哪个版本注册路由的核心步骤是三件套定义路由表、创建路由实例、挂载到应用实例。Vue3的最小化配置长这样import { createRouter, createWebHashHistory } from vue-router import HomeView from ./views/HomeView.vue const routes [ { path: /, name: home, component: HomeView } ] const router createRouter({ history: createWebHashHistory(), routes }) export default router然后在main.js里注册import { createApp } from vue import App from ./App.vue import router from ./router createApp(App).use(router).mount(#app)Vue2的写法不太一样需要Vue.use(Router)4.x之后统一改用createApp(...).use(router)。这里有一个我比较坚持的习惯路由配置一定要单独放src/router/index.js不要让入口文件承担路由表的维护工作。项目规模一大路由表几百行是很正常的事全都挤在main.js里后期根本没法维护。配合按模块拆分路由数组再合并维护体验会好很多。3. 第一个路由从URL到组件的完整链路3.1 routes数组的四个基础字段一条最基础的路由记录通常由这几个字段组成pathURL的路径必须以/开头。name路由名称方便在跳转时用router.push({ name: ... })避免硬编码URL字符串。componentURL匹配后需要渲染的组件。children子路由后面嵌套路由那一节再细说。下面是一个最基础的双页面示例const routes [ { path: /, name: home, component: HomeView }, { path: /about, name: about, component: AboutView } ]浏览器地址栏变成http://localhost:8080/#/about时vue-router解析出/about去routes数组里匹配到第二条记录取出AboutView组件渲染到App根组件里的router-view位置。这里需要补一个容易踩的小坑路由表的匹配不是“先到先得”而是根据路由声明的优先级和路径匹配规则来决定的。不过对于静态路径只要不出现完全相同的path一般都不冲突。真正会出现诡异的优先级问题多半是在动态路由和静态路由混用的时候后面再说。3.2 router-view和router-link的分工router-view是个动态容器告诉vue-router“匹配到的组件往这里放”。router-link则是a标签的进化版我们用它声明跳转而不是手写href。nav router-link to/首页/router-link router-link to/about关于/router-link /nav router-view /router-link最终渲染出来的确实是一个a标签但vue-router会拦截它的点击事件阻止浏览器默认的整页请求改走前端路由的跳转逻辑。这一点和直接写href有本质区别后者会触发浏览器发起新的HTTP请求、重新加载文档SPA的优势就全丢了。另外router-link内置了激活状态管理访问/about时对应的链接会自动加上router-link-active等class方便做导航高亮自己手写判断逻辑很容易漏掉边界情况。3.3 路由懒加载什么时候用import()项目页面的规模稍微大一点全部组件都打包进一个JS文件首屏加载时间会非常难看。Vue官方推荐的做法是按需加载也就是懒加载。{ path: /about, name: about, component: () import(../views/AboutView.vue) }component字段传入一个箭头函数箭头函数内部import()会返回一个Promise。Vite和webpack都能识别这种写法把AboutView.vue单独拆成一个chunk只有当用户真正访问/about时才去加载对应的JS文件。开发模式下几乎感受不到差异但打包之后看dist目录会发现多出了很多独立的JS文件那个就是懒加载拆包后的结果。实际业务里除了首屏必须展示的根组件和布局组件我基本都懒加载。唯一需要小心的是拆包粒度太细也不行一个页面一个文件会导致碎片化请求过多常见经验是“页面级组件用懒加载同页面的弹窗、子组件打包在一起”。4. 重定向与404兜底别让用户撞上空白页4.1 redirect的三种写法重定向是路由里一个非常重要的能力。最常见的场景是用户收藏了旧链接老项目迁移后路径变了希望访问旧地址时自动跳到新地址或者根路径/希望默认展示某个子页面再比如登录状态失效时统一踢回登录页。redirect最基本的用法就是直接写目标路径const routes [ { path: /, redirect: /home }, { path: /home, component: HomeView } ]如果目标路由是用name管理的也可以写对象形式{ path: /, redirect: { name: home } }更复杂的场景下redirect还支持函数形式函数接收目标路由信息to作为参数根据当前查询参数、用户状态动态决定跳去哪个地址{ path: /old-entry, redirect: (to) { if (to.query.redirect) { return to.query.redirect } return /home } }有一点必须强调redirect和component是互斥的。一条路由记录里一旦配置了redirectcomponent字段会被忽略因为用户访问这个path时根本不会渲染组件而是直接被送到目标地址。4.2 alias别名同一个组件响应多个路径很多人分不清alias和redirect的区别。假设业务上希望/user和/user/profile都展示同一个个人资料页但URL保持不变这就是alias的用武之地{ path: /user/profile, component: UserProfile, alias: /user }访问/user/profile和/user渲染的是同一个UserProfile组件地址栏不会跳转。也就是说redirect是“转向新地址”alias是“同一地址的多个入口”。实际工作中我用到alias最多的时候是接口迁移期。比如老活动页面的二维码已经投出去了路径不能变但新页面已经上线就用alias把旧路径指到新组件上等旧链接自然淘汰后再清理掉。4.3 404兜底路由永远留给用户一个出口任何一个面向用户的Web应用都得处理“用户手输了一个不存在的路径”这种情况。与其让页面白屏不如做一个友好的404页并在路由里兜底。vue-router 4的兜底写法{ path: /:pathMatch(.*)*, name: NotFound, component: NotFoundView }vue-router 3的写法则是{ path: *, component: NotFoundView }为什么4.x要用/:pathMatch(.*)*这么长的表达式因为它会把用户输入的错误路径作为参数放进route.params.pathMatch里404页面可以读取这个参数并展示“你访问的 /xxx 不存在”体验会细致一些。还有个容易被忽略的地方是404路由要放在路由表最末尾否则会抢先匹配到正常路由。5. 路由模式hash、history、memory生产环境怎么选5.1 hash模式省心但URL不够体面vue-router默认使用的就是hash模式。使用createWebHashHistory()时URL长这样http://localhost:8080/#/user/list#号后面的部分由前端路由全权控制。hash模式最大的优势在于URL中#后面的片段变化不会触发浏览器向服务器发送请求。这意味着不管用户怎么刷新只要你的index.html能打开路由就一定能正常定位到对应组件。部署时服务器几乎不需要做额外配置这对很多没有专职运维的团队来说非常友好。代价是URL里的#看起来有点“丑”在某些平台分享时内容会丢失对SEO极其不友好——搜索引擎爬虫很早期就决定不索引#之后的链接。如果项目是一个完全不需要SEO的内部管理系统hash模式是完全够用的。5.2 history模式地址干净但必须后端配合把createWebHashHistory()换成createWebHistory()URL就变成http://localhost:8080/user/list干净利落和普通多页网站的URL没有区别。但代价接踵而至当用户直接通过地址栏访问/user/list时浏览器会真的向服务器发起一个GET /user/list请求。如果服务器上并不存在这个路径对应的静态文件或接口那么服务器会返回404页面就白屏了。解决办法是让服务器把“所有未匹配到文件资源的请求”全部指向index.html后续路径解析交给前端路由。以最常见的Nginx为例location / { try_files $uri $uri/ /index.html; }开发模式下Vite或webpack-dev-server默认都带了这个能力所以本地跑永远没问题一到线上就暴露。不少初学者第一次遇到“history模式刷新404”时都会一脸懵排查半天发现是服务器配置问题。另外如果项目部署在服务器的子目录下比如https://example.com/admin/还要额外处理publicPath和路由的base路径否则CSS和JS资源路径会错乱页面样式全丢。5.3 三种模式对比与适用场景vue-router 4还提供了createMemoryHistory()这个模式不会操作浏览器地址栏路由状态保存在内存中。它主要用在SSR、单元测试以及一些非浏览器环境中日常业务开发基本用不到。模式创建函数v4URL形式是否需服务器配合适用场景hashcreateWebHashHistory带#不需要内网系统、快速上线、不关心SEOhistorycreateWebHistory干净需要生产部署、重视URL与SEOmemorycreateMemoryHistory无地址栏变化不需要SSR、测试、非浏览器环境面试里常问的一个问题“history模式为什么刷新会404”本质上就是考你有没有想清楚“前端路由拦截的是请求但服务器不一定提前知道你的路由表”。6. 动态路由、嵌套路由与参数传递6.1 动态路径参数:id是怎么回事真实的业务页面很少是固定静态路径的。用户详情页、商品详情页、文章详情页这类“用一个组件展示不同数据”的场景需要动态路由{ path: /user/:id, name: user-detail, component: UserDetail }匹配规则很简单/user/1、/user/abc都能命中这条路由/user/1/orders就不会。路径里的:id是一个动态段匹配到的具体值可以通过路由参数拿到。Vue3组合式API里的读取方式import { useRoute } from vue-router const route useRoute() console.log(route.params.id)Vue2选项式API里则是this.$route.params.id6.2 route.params和route.query的分工除了路径参数另一种常见的传参方式是query也就是URL里?后面的片段// 跳转时 router.push({ path: /user/list, query: { page: 2, size: 10 } }) // 读取时 const route useRoute() console.log(route.query.page)我个人的经验标准是URL中有语义化资源ID时用params比如/user/42筛选、分页、排序这些辅助参数用query比如/user/list?page2。这样URL既能表达资源层级又能承载列表状态别人把一个完整链接发给你时你能凭URL就大概知道页面状态。6.3 用props配置把路由参数解耦出来直接在组件里写route.params.id用起来方便但会带来一个副作用组件和路由强耦合了单独测试这个组件时要先mock路由上下文非常麻烦。vue-router提供了props配置巧妙地把路由参数映射成组件props。{ path: /user/:id, component: UserDetail, props: true }这样在UserDetail组件里只需要把id当普通prop接收const props defineProps([id])props除了布尔值还可以是对象或函数。对象形式适合传一些静态的配置数据函数形式则可以根据当前路由动态生成props比如从query里读分页参数后一起传给组件。这种解耦方式在页面组件复用和单元测试时价值很大建议尽早养成习惯。6.4 嵌套路由children解决真实页面层级多数B端项目都有“外层布局内层页面”的结构最外一层是侧边栏、顶部导航点击菜单时整页切换的只有中间内容区。这种结构如果跑平铺路由会造成布局组件反复卸载和挂载体验很差。正确做法是用children嵌套{ path: /dashboard, component: DashboardLayout, children: [ { path: , name: dashboard-home, component: DashboardHome }, { path: user, name: dashboard-user, component: DashboardUser } ] }这里有两个非常关键的细节。第一children里的path不要以/开头如果写成/user它会被当作顶层路由处理外层布局就失效了。第二子路由的最终访问路径是父路径加上子路径拼接而成的上面user最终的访问地址是/dashboard/user。还有一个需要区分的地方当path为空字符串时表示父路由页面本身也要渲染子组件。比如访问/dashboard时DashboardLayout内的router-view默认渲染DashboardHome这个写法在“默认子路由”场景里非常常用。7. 上线前必须处理的坑死循环、组件复用与刷新4047.1 “将您重定向的次数过多”的排查链路很多Vue开发者在项目里见过浏览器报“将您重定向的次数过多”这个错。我在实际项目里遇到过两次。第一次是全局前置守卫里写了类似这样的逻辑router.beforeEach((to, from, next) { next(/home) })不管用户想去哪个页面都无条件next(/home)。结果访问/home时再次触发守卫又执行next(/home)无限循环。第二次是路由表里两个记录互相重定向/a跳到/b/b又跳回/a。这种循环浏览器会在20次左右之后报错不会真的无限请求但页面已经完全无法操作了。排查思路其实很直接打开DevTools的Network面板看循环里有哪些URL在反复跳转基本就能定位是守卫的问题还是路由表redirect互相指的问题。不要只盯着View里报错提示去看实际请求最直观。7.2 同一个路由组件被复用created为什么不再触发场景很常见列表页点击不同用户进入同一个UserDetail组件URL从/user/1变成/user/2。你会发现created生命周期没有再次执行因为vue-router出于性能考虑复用了同一个组件实例。正确的处理方式是监听路由参数的变化在参数改变时重新请求数据import { watch } from vue import { useRoute } from vue-router const route useRoute() watch(() route.params.id, (newId, oldId) { // 在这里重新拉取用户数据 fetchUser(newId) })Vue2的对应方案是watch: { $route: ... }。这个问题面试也常考本质是测试你有没有真正理解“组件实例复用”和“数据刷新”两件事的区别。7.3 部署后的刷新404基本都是服务器少了try_files本地开发一切正常npm run build打包后扔到服务器从首页点进子页面没问题但只要在子页面按F5刷新就变成404。这个问题在history模式下几乎是100%发生的原因我在第5节写过浏览器把/user/list当成了一个真实请求发给服务器服务器没有这个页面于是返回404。解决办法就是用try_files做fallback。如果你没有权限改服务器配置那唯一省心的退路就是改用hash模式。这也是“历史遗留项目里为什么还有那么多hash模式路由”的原因之一。7.4 路由守卫里的鉴权逻辑别到处复制关于路由守卫最后分享一个我自己的习惯。不要在每个组件里写if (localStorage.getItem(token))这种判断太散、太容易漏。我的做法是在路由表里预先声明哪些页面需要登录{ path: /admin, component: AdminLayout, meta: { requiresAuth: true } }然后在全局前置守卫里统一处理router.beforeEach((to, from, next) { if (to.meta.requiresAuth !isLogin()) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })登录成功后再通过redirect参数跳回原来想去的页面。这种集中式的鉴权方案后面规则再复杂也只会改动一个文件不会出现“漏改一个页面导致权限漏洞”的情况。最后再补充一个个人小习惯每次新建路由文件时我都会顺手把routes数组按业务模块拆成小块最后再合并到一起。比如userRoutes、orderRoutes、settingsRoutes各自独立最后[...userRoutes, ...orderRoutes, ...settingsRoutes]汇入主表。前期多花五分钟后期维护路由表时能省下大量反复查找的时间。对于Vue路由这套东西理解一遍原理之后剩下的就是把每一个细节在实际项目中多敲几遍遇到问题时的排查经验会比任何文档都管用。
返回列表