news 2026/10/24 5:54:04

Vue Router学习笔记:从路由配置到守卫避坑的完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue Router学习笔记:从路由配置到守卫避坑的完整实践

学Vue到第六天,这是个特别微妙的节点。前五天我还在跟模板语法、计算属性、组件通信较劲,真要把一个多页面应用跑起来的时候,才发现Vue项目如果没有路由,就像一栋只有房间没有走廊的楼——每个组件都做得再精致,也没法让用户在页面之间自由走动。所以从第六天开始,我正式把Vue Router拉进了学习清单,这一整理就从早上折腾到晚上。

这篇内容不是官方文档的翻译,也不打算讲太多空泛的理论。我把自己第六天从零安装Vue Router、配置路由、传参、嵌套、守卫,到逐个排查报错的过程完整复盘下来。如果你Vue基础语法已经过关、正在纠结“多个页面怎么跳转”,或者已经装了路由但总是遇到“跳了地址栏却不变”“刷新就404”这类怪问题,这篇应该能帮你少走不少弯路。

1. 为什么第六天该碰路由了

1.1 组件切换不等于页面跳转

前五天里,我经常用v-if或v-show去控制页面显示。思路很简单:一个变量等于1显示首页,等于2显示列表页,等于3显示详情页。这种写法应付两三个tab问题不大,但想真正做一个应用的时候,问题就会集中爆发:地址栏永远是同一个URL,刷一下页面就回到最初状态,浏览器后退按钮点了完全没反应,别人想直接打开你正在看的那个页面更是无从下手。

你可能会说,多包几个v-if不就行了?实际做过就知道,当页面层级超过两层,状态变量会爆炸式增长,而且代码里全是一坨一坨的显示逻辑,后期维护基本靠猜。更关键的是,这种切换本质上只是“某个区域的内容替换”,浏览器根本不知道你换了页面,所以地址、历史记录、刷新状态这些全都跟不上。

这也是路由存在的根本原因。Vue Router要做的事,说穿了就三件:让地址栏跟着页面内容改变,让浏览器前进后退能配合内容切换,让刷新之后还能停留在当前页面。它不是锦上添花的功能,而是单页应用能不能真正往“应用”方向走的骨架。

1.2 路由解决了什么

为了把路由的价值讲清楚,我对比了一下“用v-if硬切”和“用路由切页”的体验差异,这样理解起来更直观:

能力v-if/v-show切换Vue Router切换
地址栏变化不变,永远是根路径随路由变化,能直观看到当前位置
刷新恢复直接回到初始状态刷新后停留在当前路由对应的页面
浏览器前进/后退不响应,按钮变灰正常记录历史,能来回跳
链接分享所有人打开都是同一个页面可分享具体页面,带参数也能复原
代码结构页面内容堆在一个组件里每个页面独立,职责清晰
权限控制基本靠手动判断有路由守卫统一拦截

看完这张表,你大概能理解为什么第六天必须碰路由:再往后学组件通信、状态管理、接口请求,都需要建立在“项目有多个页面”的基础上。没有路由,后面那些内容都会变得很奇怪。

1.3 这天之前你需要掌握什么

在正式开始之前,稍微确认一下自己的基础。第六天学路由,最好已经知道Vue的单文件组件怎么写,props和emit大概怎么通信,ref/reactive这类响应式API在组件里怎么用。不用精通,能用就行。如果这些还没完全吃透,也不影响先跑通路由,等遇到组件内部逻辑卡住时再回头补也不迟。

另外,后面所有示例默认你现在用的是Vue 3 + Vite的工程,路由插件版本对应Vue Router 4。如果你还在Vue 2 + Vue Router 3,思路类似,但API差别比较大,最好先切到Vue 3再继续。

2. 路由的两种工作模式:hash还是history

2.1 hash模式:地址栏里那个#号

刚开始学路由,你可能会注意到地址栏里会出现一个#,比如http://localhost:5173/#/home。这就是hash模式。

hash模式的原理其实不复杂:URL中#后面的部分叫hash,只改变hash值不会触发浏览器向后端发起请求,但会触发hashchange事件,同时浏览器会把这个变化记入历史记录。Vue Router在hash模式下,就是监听hashchange事件,再按照hash对应的路径去匹配组件渲染。

这种模式最大的优势是部署省心。因为#后面的内容不会发到服务端,所以随便找个静态文件服务器托管都能跑,甚至双击本地html文件,在大多数场景下也能正常跑起来。我当时用hash模式的时候,真的是零配置,完全不用管服务端要做什么。

缺点也很明显:URL里带着#不好看,而且不够“正经”。有的场景下,比如做分享、做埋点统计,还要额外处理#带来的参数解析,多少有点别扭。

2.2 history模式:更像真实的多页面

写完hash模式后,我又切到了history模式。这种模式下URL看起来就是干净的那种:http://localhost:5173/home,和传统多页面应用的地址几乎没区别。

它背后的机制是基于History API,核心是pushState和replaceState这两个方法。它们可以在不刷新页面的情况下修改浏览器地址栏,并生成历史记录。配合popstate事件,就能做到“地址变了但页面没闪一下”。

听起来是比hash模式优雅,但代价是它需要服务端配合。因为地址栏变成一个真正的URL之后,浏览器刷新时会把请求发到服务端。如果服务端没有把/home这样的路径全部重定向到index.html,那刷新就直接404。开发环境之所以没暴露这个问题,是因为Vite的dev server自带了对history模式的支持,会自动把所有路径兜回index.html,但上线部署后如果忽略这步,立刻翻车。

2.3 我当时怎么选的

我第六天的选择很简单:直接用history模式。

原因有两个。第一,我练这个项目就是为了体验真实上线的感觉,不想等部署时再换,到时还要检查一堆路由跳转有没有问题。第二,开发阶段Vite已经帮我处理了刷新兜底,日常敲代码不会受任何影响,等部署时我再在服务端加一个try_files配置就能解决。当然,如果你只是为了快速做一个小demo,完全不想操服务端的心,hash模式也不丢人。在练习阶段,两种模式切换的成本很低,代码改动不过几行的事,重点是理解它们各自在什么场景下会踩坑。

3. 从零配置一个可用路由:核心三步

3.1 安装依赖并创建Router实例

先把插件装上,我用的是Vue Router 4:

npm install vue-router@4

然后单独建一个路由配置文件,我习惯放在src/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

这里有几个点需要解释。createRouter是Vue Router 4创建实例的方法,routes数组就是路由表,里面每个对象都表示“某个路径对应哪个组件”。createWebHistory()表示使用history模式,如果你在上一步决定用hash模式,把它换成createWebHashHistory()就行。

我在学习时把组件直接静态引入,是因为这样能直观看到路由表结构。实际项目里,更常见的写法是用动态导入做路由懒加载,比如component: () => import('../views/About.vue')。好处是首屏不会一次加载所有页面的代码,访问到对应页面时才去请求该页面的JS文件。第六天打基础阶段可以先了解,但别觉得“动态导入是高手才写的”,其实就是一个函数箭头包一层,很好理解。

3.2 注册全局Router并放置页面出口

路由实例创建了,还得让整个Vue应用知道它的存在。在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)这一步是关键,它会注册路由相关的全局组件和生命周期逻辑。如果不做这一步,后面在组件里写router-link或useRoute都会直接报错。

然后在App.vue里,放一个router-view组件。这个组件就是路由的“出口”,路由匹配到哪个页面组件,它就会渲染哪个。再放一个或多个router-link作为导航入口:

<template> <div> <nav> <router-link to="/">首页</router-link> <router-link to="/about">关于</router-link> </nav> <router-view /> </div> </template>

router-link最终会被渲染成一个a标签,但它和普通a标签有区别:普通a标签点击后浏览器会重新加载整个页面,而router-link默认会拦截点击,用路由内部的逻辑切换内容,避免整页刷新。这个细节很值得体会,它决定了单页应用的“快”是快在哪里的。

3.3 两个容易写错的小细节

第一次配路由时我犯了不少糊涂,有两个细节值得单独提醒:

第一,routes里的path不要带文件后缀,也不要在末尾加.html。/about就写/about,不要写/about.html或者/about/(如果是根路径/除外)。原因很简单,路由是在管理应用的页面状态,不是真的指向某个物理文件。

第二,redirect写绝对路径,别省略前面的斜杠。比如{ path: '/a', redirect: '/b' },如果写成redirect: 'b',浏览器解析时会按相对路径处理,结果就不是你想去的那个地址了。这种错误很好排查:点击跳转后看地址栏,对比一下目标地址是http://localhost:5173/b还是http://localhost:5173/current/b,一眼就能看出问题。

4. 路由传参:四种姿势怎么选

4.1 query传参:适合搜索条件和分享场景

先写第一种方式,query传参。它把参数拼在路径后面,和传统URL的?参数一样:

router.push({ path: '/detail', query: { id: 10086 } })

跳转之后,地址栏会变成/detail?id=10086。在目标组件里通过useRoute拿参数:

import { useRoute } from 'vue-router' const route = useRoute() console.log(route.query.id)

query传参最大的好处是参数完全暴露在URL里,刷新不丢,而且分享给别人时,对方打开链接就能恢复页面状态。所以搜索页、筛选条件、分页页码这类场景,用query很合适。它也有局限:如果放太复杂的对象,URL会变得巨长而且难以阅读。遇到这种情况,更建议只传ID或关键标识,拿到标识后自己再查一遍完整数据。

4.2 params传参与命名路由:语义化更强

第二种方式会让参数直接出现在路径里。比如详情页,往往希望地址是/detail/10086而不是/detail?id=10086。这时要先把路由表里的path配置成带占位符的形式:

{ path: '/detail/:id', name: 'detail', component: Detail }

跳转时,有两条路:一条是直接用path拼接字符串:router.push('/detail/10086');另一条是用命名路由,把参数放在params里:

router.push({ name: 'detail', params: { id: 10086 } })

这里有一个新手必踩的坑:很多教程会告诉你params传参,但如果你在跳转时用的是path而不是name,params会被直接忽略。也就是说,下面这种写法传不过去:

// 这样不行 router.push({ path: '/detail', params: { id: 10086 } })

因为Vue Router在解析时,path的优先级很高,一旦提供了path,params就被扔掉了。想用params就老老实实用name,或者直接自己拼字符串。这个坑我踩了之后才知道,官方文档也写得很清楚,只是平时跳来跳去容易忽略。

4.3 用props解耦,让组件更干净

无论用query还是params,组件里拿参数都得靠useRoute(),这意味着组件和路由对象耦合在一起。如果有一天想把这个详情组件用在非路由的场景,还得再传一遍props,非常别扭。

更好的做法是开启路由的props选项:

{ path: '/detail/:id', name: 'detail', component: Detail, props: true }

开启后,路由会自动把params里的字段作为props传给组件。组件里直接声明props即可:

defineProps(['id'])

这样一来,组件内部不再依赖useRoute,也方便单元测试和维护。对于params里那些值,props: true会作用在动态路由段的参数上。如果你还希望query参数也能映射为props,就需要把props写成函数形式,手动处理一下:

{ path: '/detail', component: Detail, props: (route) => ({ id: route.query.id }) }

两种写法我都试过,日常开发中能开props: true就尽量开,组件会清爽很多。

4.4 参数变化时,组件不会重新创建

这是我在做详情页时遇到的一个隐藏问题:从/detail/1跳到/detail/2,如果这两个路径都用同一个Detail组件,Vue会复用这个组件实例,不会重新走一遍挂载流程。也就是说,onMounted里的初始化逻辑不会再次执行。

这意味着在详情页里,光靠生命周期函数去拉数据是不行的。解决方案一般是在组件里监听路由参数:

watch( () => route.params.id, (newId) => { // 拿到新id,重新请求详情 }, { immediate: true } )

加上immediate: true之后,组件第一次进入时就会执行一次,后面参数变化也会触发,逻辑就能统一起来。如果对这个了解不深,非常容易写出“第一次跳转正常、第二次跳转不刷新”的bug。

5. 嵌套路由与路由守卫:实际项目躲不开的功能

5.1 嵌套路由:页面里还有页面

第六天学到后面,我开始做一个带多个子页面的布局。比如用户中心,上面一个统一的侧边栏,左侧是导航,右侧是不同子页面的内容。这种场景很适合嵌套路由,也就是路由里套路由。

配置方式是在父路由下面加一个children数组:

{ path: '/user', component: UserLayout, children: [ { path: '', component: UserProfile }, { path: 'posts', component: UserPosts }, { path: 'settings', component: UserSettings } ] }

注意两点:children里的path不要以/开头,否则会被当成根路径去匹配;path: ''表示在父路径下默认渲染的内容,访问/user时就会显示UserProfile。父组件UserLayout里需要再放一个router-view,这样子路由匹配到的组件才能在指定位置渲染。我在第一次写的时候总忘了在父组件里加这个出口,结果子页面永远是空白,排查了好一会儿才发现是视图出口没放。

嵌套的层级理论上不限,只要每一层都有对应的router-view承接就行。实际项目中我目前最多用到三层,层级太深会让路由表变得复杂,维护成本明显上升。

5.2 beforeEach全局守卫:登录拦截的常规做法

路由守卫是我觉得Vue Router最值得写一下的部分。原因很简单:它把一个“页面能不能进”的问题,从组件内部逻辑中彻底抽离出来,统一在路由层处理。

最常用的是全局前置守卫beforeEach。它会在每次路由跳转前执行,可以用来做登录拦截。比如进入任何页面之前,检查localStorage里有没有token,没有就去登录页:

router.beforeEach((to, from) => { const token = localStorage.getItem('token') if (!token && to.path !== '/login') { return '/login' } return true })

这里的返回值很灵活:返回false表示取消本次跳转,返回一个路径表示重定向到那个路径,不写或者返回true表示放行。上面示例里,如果用户没登录且要去的不是登录页,就直接被扔到登录页。

这里有个我一开始没注意的陷阱:如果你在守卫里重定向到了/login,而/login本身也被同一个守卫拦住,那就可能死循环。所以我在判断条件里加了to.path !== '/login',确保登录页永远放行。

除了beforeEach,还有afterEach,它在跳转完成后执行,适合做页面标题更新、页面访问统计这类“事后”任务。当然守卫不止这两种,但第六天能先把全局守卫吃透就很够用了。

5.3 meta路由元信息:给页面加备注

路由配置里的每一项还可以挂一个meta字段,用来存一些和页面有关的信息。最常见的就是页面标题:

{ path: '/about', name: 'about', component: About, meta: { title: '关于我们' } }

然后在全局afterEach里统一设置document.title:

router.afterEach((to) => { document.title = to.meta.title ? `${to.meta.title} - Vue Day6` : 'Vue Day6' })

另外,meta还可以用来标记某个页面是否需要登录权限、菜单里要不要高亮、统计分类是什么等等。这样在守卫里可以组合判断,比如:

router.beforeEach((to) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { return '/login' } })

把权限需求和路由表写在一起,比在组件里到处写判断干净得多。

6. 第六天实测:几个常见路由报错与排查思路

6.1 “No match found”与404页面配置

路由配置好了,点击跳转时控制台可能报No match found for location with path "/xxx"。这个报错的意思很简单:当前路径在你的路由表里匹配不到任何记录。

我当时排查的逻辑是几步走:先看地址栏里的路径到底是多少;再翻路由表里有没有对应的path;如果路由表里有动态路由段/detail/:id,还要确认实际访问的路径有没有正常填上参数。有时候是因为嵌套路由的children里path写多了/,导致父路径没匹配上。这种问题不难定位,因为报错信息里会把当前路径原样打出来。

更好的做法是给路由表加一个兜底配置,让所有没匹配上的路径都指向一个404页面:

{ path: '/:pathMatch(.*)*', name: 'not-found', component: NotFound }

注意Vue Router 4里通配符的写法和Vue Router 3不一样,不再是*,而是/:pathMatch(.*)*。如果按旧写法配置,可能在升级项目时会踩到坑。把这个404路由放在最后,表示前面所有路由都匹配不上才会走到这里。

6.2 页面路径变了但内容没变

还有一个我折腾很久的现象:点击router-link后,地址栏的URL确实变了,但页面内容纹丝不动。

排这个问题的思路一般是先确认router-view有没有放对位置。如果在根组件里没有router-view,或者在某个子组件里又写了一个遮盖层把内容挡住了,就会出现“地址变了但什么都看不到”的情况。还有一个容易忽略的点是,如果router-view前面有v-if控制显示条件,条件不满足时路由匹配正常,但视图就出不来。

更隐蔽的情况是,路由表里写了两个相同path的配置,前面的组件不生效,后面的又没被匹配到,结果路由解析时按顺序匹配到了空组件。总之遇到“地址对了但内容没来”,先把路由表和router-view存在性这两件事查一遍,能解决八成问题。

6.3 刷新后404,问题的根源在服务端

这个是history模式下最经典的坑。开发环境几乎不会出现,因为dev server已经帮你把不存在路径的请求都兜回index.html了。但一旦部署到生产环境,服务端处理不当时,你在某个子页面一刷新,浏览器就会向服务器发一个真实请求,服务器找不到这个路径对应的文件,于是返回404。

解决思路也很明确:让服务端把前端子路径的请求统一重新定向到index.html,由前端路由自己决定渲染谁。以常见的nginx为例,需要在站点配置里加上:

location / { try_files $uri $uri/ /index.html; }

意思是优先尝试访问真实文件或目录,都找不到时就回退到index.html。这样刷新后浏览器加载的是index.html,Vue Router会解析当前地址,重新渲染对应页面。

如果你用的是hash模式,就不存在这个问题,因为#后面的内容不会发到服务端。从这一点也能理解为什么很多人图省事会选hash模式。但我的网站想保持干净美观的URL,所以还是选了history模式,上线前给服务端补好配置就行。这项配置不要等到最后一刻才想起,部署前就应该写在部署文档里。

第六天过完,路由在我心里的分量从“一个会跳转的组件占位符”变成了“整个应用状态的骨架”。我后来写页面时会习惯性先想:这个页面属于哪个层级?参数应该放URL里还是query里?需不需要守卫拦截?这些思考基本都是从那天的报错和试错中长出来的。最后分享一个自己的小习惯:每次新建路由页面,我都顺手在路由表里给它写上meta.title,这样到后期做’多标签页’或‘面包屑’功能时,直接有数据可用。路由这东西,入门只需要一小时,但要在真实项目里不踩坑,每一层细节都值得多花几分钟琢磨。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 5:52:57

媒介投放系统风险防控架构:熔断降级与自动恢复机制设计

做媒介投放系统这几年&#xff0c;我越来越觉得&#xff0c;投放引擎做得再快、再准&#xff0c;都不如一个“刹车”来得重要。前阵子某消费品牌因为一条负面舆情波及&#xff0c;广告主凌晨打电话要求全线暂停投放&#xff0c;负责投放的同学一边道歉一边手工关计划&#xff0…

作者头像 李华
网站建设 2026/10/24 5:52:54

Java开发者必读:LangChain4j敏感信息过滤实战指南

1. 敏感信息过滤&#xff1a;为什么Java开发者不能只盯着Prompt我最早接触LangChain4j的时候&#xff0c;跟大多数人一样&#xff0c;满脑子想的都是怎么构造好Prompt、怎么接上模型、怎么把对话记忆串起来。直到有一次&#xff0c;一个客服问答系统上线没几天&#xff0c;运营…

作者头像 李华
网站建设 2026/10/24 5:52:50

Spine for mac 2D骨骼动画:骨骼搭建、批量导出与验收避坑

简介&#xff1a;Spine for Mac 是面向 2D 游戏动画师与独立开发者的骨骼动画工具安装包&#xff0c;适合希望摆脱逐帧绘制、提升角色动作自然度与资源复用率的中高级用户&#xff0c;可在 macOS 上完成骨骼绑定、关键帧调节、IK 与动画状态机等创作流程。压缩包共 188 个文件&…

作者头像 李华
网站建设 2026/10/24 5:52:48

2012 R2 离线安装 .NET 3.5:sxs 源与 DISM 排错

简介&#xff1a;面向 Windows Server 2012 R2 Standard 系统管理员与运维人员的 SxS 组件文件包&#xff0c;聚焦 .NET Framework 3.5 安装失败的典型场景。当系统在线更新组件受阻或安装介质缺少相应组件时&#xff0c;可通过指定该备用源路径补齐所需 side-by-side 程序集&a…

作者头像 李华
网站建设 2026/10/24 5:52:45

TreeSize绿色版:免安装精准分析磁盘空间占用

简介&#xff1a;TreeSize 绿色版是一款面向Windows用户&#xff08;尤其适合IT运维人员、系统管理员及普通电脑爱好者&#xff09;的便携式磁盘空间分析工具&#xff0c;专为快速定位大文件与冗余文件夹、优化硬盘使用效率而设计。资源包共11个文件&#xff0c;含核心可执行程…

作者头像 李华
网站建设 2026/10/24 5:51:00

汽车塑料件高低温试验全解析:从-40℃到85℃的四季考验

一台车卖给全世界&#xff0c;它夏天可能要面对暴晒后的车内高温&#xff0c;冬天要扛住零下三四十度的严寒。那些你天天摸的仪表板、门把手、保险杠、卡扣、线束护套&#xff0c;本质上都是高分子材料——塑料在温度面前相当“敏感”。所以在零件量产之前&#xff0c;我们得先…

作者头像 李华