外观
第4章:用路由组织页面与访问入口
约 13417 字大约 45 分钟
Vue Router动态路由嵌套路由导航守卫
2026-07-28
第 3 章的任务板只有一个页面。现在“校园微商城管理端”要增加登录页、工作台、商品列表、商品详情和个人中心。如果继续用多个布尔变量决定显示哪一块,刷新页面后状态会丢失,浏览器返回键不好用,详情地址也无法直接复制给别人。
路由不是单纯“做菜单”。它要解决三个问题:当前 URL 应该显示哪个页面,这个页面应该放进哪个公共布局,进入页面前要不要先检查登录状态。
项目目标
从单页任务板搭出管理端路由骨架:公共登录页、后台嵌套布局、商品动态详情、404 页面和登录守卫;同时知道前端守卫只能改善页面入口体验,不能代替后端授权。
学习说明
本章依次学习路由接入、动态路由、嵌套路由、导航守卫和部署边界,并通过章末任务完成综合应用。
配套资源
本章示例工程位于 resources/ch04/classroom-demo/,章末任务起始工程位于 resources/ch04/after-class-starter/。进入对应项目目录后,先执行 npm install,再执行 npm run dev。
课堂工程是“校园微商城管理端”(/admin),提供截至 4.5 的演示代码,包含原地址回跳示范,但不包含当堂练习答案。按 4.2—4.5 逐步实操时,用各节代码替换对应文件;4.4 起商品路径从 /products/* 调整为 /admin/products/*。新增关于页、未知商品提示、个人中心、已登录回跳与退出按钮均需自行完成。
课后起始工程和参考实现使用 4.10 要求的“校园服务后台”(/service),用于迁移同一套路由知识,业务名称和页面文件无需改成商品管理。
🎯 学习梯度指引(分层通关)
- 核心必学(保底通关 · 管理端路由骨架):掌握 4.1、4.2(路由最小接入)、4.3(动态参数与 props 解耦)、4.4(嵌套路由与公共侧栏布局及 404 兜底)、4.5(基础未登录守卫拦截)。能够独立搭建中后台管理系统的“左侧固定侧栏 + 右侧内容切换”基础路由架构,并实现基础访问拦截。
- 进阶选学(🌟 自主拓展 · 权限闭环与部署视野):4.5(精准原路回跳
redirect与已登录防重复进登录页)、4.6(History 模式与 Hash 模式对比及生产环境 SPA 服务器回退原理)。供学有余力或有进阶架构需求的学生自主选学探索,不作为基础达标强制考核。
4.1 本章任务清单
本章沿着“先让 URL 能切换页面,再搭后台布局,最后加登录入口”的顺序推进。每完成一个知识点都要立即运行,避免一次改太多文件后不知道错在哪里。
| 阶段 | 本章要做什么 | 完成后应该看到 |
|---|---|---|
| 路由最小接入 | 注册 router,创建首页和商品列表页 | 点击导航后地址变化,页面不整页刷新 |
| 商品动态详情 | 配置 /products/:id,使用 props 接收商品 id | 访问 /products/101 时显示商品 ID 101 |
| 后台嵌套布局 | 创建后台父路由、公共侧栏和子路由出口 | /admin/* 共用侧栏,只替换内容区 |
| 登录守卫 | 用 meta 标记后台页面,用 beforeEach 检查登录 | 未登录先去登录页,登录后回到原地址 |
| 404 与刷新检查 | 添加兜底路由,检查 History 模式 | 未知地址显示 404,能解释深层地址刷新条件 |
开工顺序
- 在
resources/ch04/classroom-demo/中确认项目可以正常启动。 - 先完成首页与商品列表的最小路由,不要一开始就加入守卫。
- 接入商品动态详情,再把商品页面放进后台公共布局。
- 加入登录页、
meta和beforeEach,验证登录前后的跳转。 - 最后测试未知地址、浏览器返回键和深层地址刷新。
完成后应该是什么样
项目至少包含登录页、后台工作台、商品列表、商品详情、个人中心和 404 页面。地址栏能说明当前位于哪个页面;后台页面共用一套布局;未登录访问后台时先进入登录页,登录后回到原目标地址。
4.2 知识点一:路由最小接入
学习目标
掌握 Vue Router 的最小接入流程,能够把 URL、路由记录、页面组件和 RouterView 对应起来,理解单页应用路由驱动视图的底层机制。
语法/概念
单页应用(SPA)与传统多页应用对比
在传统的多页应用(MPA)中,每次点击导航链接,浏览器都会向服务器发送全新的页面请求,导致整页闪白重新加载、脚本与样式重复解析、页面临时状态丢失。
单页应用(SPA, Single Page Application)从根本上改变了这种体验:整个应用只在初次进入时加载一次宿主 HTML 文件,后续所有的“页面切换”完全由前端路由接管——通过监听浏览器地址栏的 URL 变化,在客户端动态销毁旧页面组件并挂载新页面组件,实现零闪白、无缝顺滑的浏览体验。
核心流向心智模型
Vue Router 从捕获 URL 到最终呈现页面的完整链路如下:
浏览器地址栏 URL 变化
↓
routes 路由映射表逐项查找匹配
↓
命中规则并获取对应 component 页面组件
↓
动态挂载并渲染到 <RouterView /> 页面插座中路由系统四大核心对象职责拆解
路由系统的运转依赖四大核心对象的协同配合:
| 核心对象 | 角色定位 | 核心职责与工作机制 | 关键注意事项 |
|---|---|---|---|
createRouter | 调度中枢工厂 | 接收历史模式与路由表配置,实例化全局路由管理器 router,负责全局导航控制与监听 | 全局唯一,通过 app.use(router) 挂载到应用根实例 |
routes | 路由映射规则表 | 由路由记录对象组成的数组,建立 URL 路径与具体页面组件的一对一匹配规则 | 数组顺序影响匹配优先级,未声明的路径将无法被匹配 |
<RouterLink> | 声明式导航链接 | 渲染为原生的 <a> 标签,但会自动拦截点击行为并阻止页面刷新,通过浏览器历史记录 API 改变 URL | 替代传统 <a href>,当前激活链接会自动添加 .router-link-active 类名 |
<RouterView> | 动态页面插座 | 充当占位容器(Outlet),根据当前匹配的路由规则,将对应的页面组件动态渲染在此处 | 页面组件的渲染入口,若页面缺失该标签,无论 URL 如何变化均无法展示页面内容 |
核心配置代码骨架与属性剖析
// 导入创建路由实例与 Web History 模式的核心函数
import { createRouter, createWebHistory } from 'vue-router'
// 导入实际承担页面展示职责的 Vue 单文件组件
import HomeView from './views/HomeView.vue'
// 1. 定义路由映射规则表
const routes = [
{
path: '/', // 浏览器访问的 URL 路径
name: 'home', // 路由专属唯一名称(命名路由,解耦路径维护)
component: HomeView, // URL 命中时负责渲染挂载的页面组件
},
]
// 2. 创建并导出路由器调度实例
const router = createRouter({
history: createWebHistory(), // 使用 HTML5 Web History 模式(自然路径,无 # 号)
routes, // 注册上方声明的路由映射表
})
export default router插件注册顺序与执行时机
在应用入口 main.js 中,路由插件必须在应用挂载前完成注册:
createApp(App)
.use(router) // 必须先注册路由插件,注入全局组件与监听
.mount('#app') // 再将 Vue 根组件挂载到 DOM 容器中- 注册机制:
app.use(router)会向 Vue 应用根实例中注入全局属性(如$router、$route)、注册全局可用组件(<RouterLink>、<RouterView>)并启动全局 URL 监听。 - 颠倒后果:若在
.mount('#app')之后才调用.use(router),Vue 在首次解析App.vue模板时将无法识别<RouterView />,导致首屏直接白屏。
播放下面的流程,观察 URL、路由记录、页面组件和出口的先后关系。
交互演示
URL 如何匹配到页面组件
把“地址变化”与“组件渲染”连成一条可调试链。
- 1地址变化URL
- 2路由匹配routes
- 3组件加载component
- 4出口渲染RouterView
当前发生
用户点击 RouterLink 或执行 router.push。
观察证据
地址栏路径变化且页面没有整页刷新。
课堂演示
步骤 1:启动课堂演示项目。
在终端执行:
cd docs/courses/web-frontend-framework/resources/ch04/classroom-demo npm install npm run dev运行后,终端应显示本地访问地址。浏览器能打开页面,就说明项目环境正常。
步骤 2:创建最小路由所需文件。
保留
src/main.js和src/App.vue,创建src/router.js、src/views/HomeView.vue、src/views/ProductListView.vue。文件创建后,编辑器中应看到两个页面组件和一个路由文件。步骤 3:注册 router 并写出两个页面。
Vue 应用注册插件使用的完整方法是 app.use。先观察
.use(router)与.mount('#app')的顺序,再解锁代码。保存后,点击“首页”和“商品管理”,地址栏应在
/与/products之间变化,内容区也应切换,但浏览器不应整页闪白刷新。步骤 4:用地址栏直接验证。
分别把
/和/products粘贴到地址栏访问。两个地址都应显示对应页面;浏览器后退按钮也应能返回上一个页面。
常见错误
- 页面完全空白:先检查
main.js是否在.mount()前调用.use(router)。 - 地址变化但内容不变:检查
App.vue是否有<RouterView />。 - 导航整页刷新:检查是否误用了普通
<a href>,站内导航应使用RouterLink。
当堂练习(分层双轨制)
- 【必做任务(基础通关)】:新增
src/views/AboutView.vue,页面显示“关于管理端”;在src/router.js中增加/about路由并在App.vue导航栏中添加“关于”链接。保存后点击链接应进入/about,浏览器后退能回到原页面。只使用本节学过的路由记录、RouterLink和RouterView。 - 【选做挑战(🌟 自主拓展)】:在
App.vue的样式中利用.router-link-active类名定制高亮样式(例如添加下划线或加粗变色反馈),使当前所在页面的导航链接呈现醒目的激活状态。
4.3 知识点二:动态路由
学习目标
掌握动态参数、命名路由和 props: true,能够让商品详情页从 URL 接收商品 id,理解路由参数与组件解耦的设计思想。
语法/概念
动态路由解决的业务痛点
在真实的电商系统中,商品数量成千上万。如果使用静态路由,就必须在路由表中手动罗列 /products/101、/products/102、/products/103……这会导致路由配置文件急剧膨胀,且无法应对动态上架的新商品。
动态路由(Dynamic Route Matching)通过在路径中声明参数占位符,使得一条通用规则能够匹配无限量的同构业务资源。
核心配置代码骨架与四项核心属性深度拆解
动态路由的核心配置对象结构如下:
{
path: '/products/:id', // 1. 动态路径:冒号后为参数占位符
name: 'product-detail', // 2. 命名路由:全局唯一标识,解耦路径与跳转
component: ProductDetailView, // 3. 页面组件:命中规则时挂载渲染的目标组件
props: true, // 4. 参数解耦:自动将路由参数注入为组件 props
}这四个配置属性各自承担着关键的架构职责:
| 配置属性 | 属性类型 | 核心作用与工作机制 | 教学要点与避坑说明 |
|---|---|---|---|
path | string | 动态路径规则声明。使用冒号(:)定义动态参数占位符。当用户访问 /products/101 时,URL 中对应位置的 101 会被自动捕获并存入路由对象的 params.id 中 | 冒号是必不可少的占位标识;提取出来的参数值始终为 string 字符串类型 |
name | string | 命名路由标识。为该路由赋予一个唯一的语义化代号。后续在做页面跳转时,直接通过 name 指向该页面,而无需在多处手写硬编码 URL 路径 | 当后期将路径改为 /goods/:id 时,只需修改配置中的 path 一处,全局所有使用 name 的跳转代码无需修改 |
component | Component | 目标展示组件。当地址栏 URL 与 path 规则匹配成功时,将该单文件组件实例化并动态渲染到外层的 <RouterView /> 出口中 | 必须确保对应组件已正确导入(import) |
props | boolean | 路由参数解耦核心开关。设置为 true 时,Vue Router 会将捕获到的 route.params 键值对自动作为组件的 props 传入 | 极力推荐的工业级实践,避免组件内部直接依赖 useRoute(),大幅提升组件的复用性与独立可测性 |
关键机制深度解析:为什么极力推荐 props: true 参数解耦?
模式对比:强耦合模式 vs 属性解耦模式
未开启
props: true(强耦合模式): 组件内部必须深度依赖路由上下文:<script setup> import { useRoute } from 'vue-router' const route = useRoute() const productId = route.params.id // 强依赖全局 route 实例 </script>弊端:组件与 Vue Router 形成强绑定。若日后该详情组件需要作为抽屉弹窗在其他页面直接使用(通过
<ProductDetailView :id="selectedId" />),或者编写脱离路由环境的单元测试,组件将因缺失路由上下文而报错崩溃。开启
props: true(解耦模式 · 推荐实践): 开启后,路由参数直接转化为标准 Vue Props 输入:<script setup> // 组件只关心外界传入的属性,完全不感知路由的存在 defineProps({ id: { type: String, required: true, }, }) </script>优势:组件退化为纯粹的“数据驱动型展示组件”,既可通过路由访问,也可作为常规子组件灵活复用,职责边界清晰。
命名路由跳转的最佳实践
从列表进入详情页时,推荐使用“命名路由 + params 对象”:
<!-- 推荐:结构自解释、参数类型清晰、路径重构零成本 -->
<RouterLink
:to="{
name: 'product-detail',
params: { id: product.id },
}"
>
查看详情
</RouterLink>- 相比手写模板字符串的优势:若写成
:to="'/products/' + product.id",一旦路径变动(如改为/goods/:id),所有分散在各处的拼接代码都必须全局查找并替换,且极易因斜杠漏写而引发 404;使用对象语法由 Vue Router 底层自动拼接,安全健壮。
课堂演示
步骤 1:创建商品详情组件。
在课堂演示项目中新建
src/views/ProductDetailView.vue。创建后浏览器暂时不会显示详情页,因为还没有动态路由记录。步骤 2:配置动态路由并从列表传入 id。
把相关文件改成下面这样:
商品动态详情src
router.js
views
ProductListView.vue
ProductDetailView.vue
src/router.jsimport { createRouter, createWebHistory } from 'vue-router' import HomeView from './views/HomeView.vue' import ProductDetailView from './views/ProductDetailView.vue' import ProductListView from './views/ProductListView.vue' export default createRouter({ history: createWebHistory(), routes: [ { path: '/', name: 'home', component: HomeView, }, { path: '/products', name: 'product-list', component: ProductListView, }, { // :id 为动态占位符,匹配所有 /products/xxx 格式的地址 path: '/products/:id', name: 'product-detail', component: ProductDetailView, // 开启解耦模式:将 route.params.id 作为 prop 自动传递给 ProductDetailView props: true, }, ], })src/views/ProductListView.vue<script setup> // 模拟商品数据列表 const products = [ { id: 101, name: '机械键盘' }, { id: 102, name: '无线鼠标' }, ] </script> <template> <section> <h1>商品列表</h1> <ul> <li v-for="product in products" :key="product.id"> <span>{{ product.name }}</span> <!-- 声明式命名路由跳转:将商品唯一 id 作为动态参数 params 传入 --> <RouterLink :to="{ name: 'product-detail', params: { id: product.id }, }" > 查看详情 </RouterLink> </li> </ul> </section> </template>src/views/ProductDetailView.vue<script setup> // 得益于路由配置中的 props: true,组件无需引入 useRoute,直接通过 defineProps 声明接收 defineProps({ id: { type: String, // 动态路由参数在 URL 中默认为字符串类型 required: true, }, }) </script> <template> <section> <h1>商品详情</h1> <!-- 模板直接渲染从路由参数解耦传入的 id --> <p>当前商品 ID:{{ id }}</p> <!-- 返回列表页时同样使用命名路由,避免路径硬编码 --> <RouterLink :to="{ name: 'product-list' }"> 返回商品列表 </RouterLink> </section> </template>保存后,商品列表应显示两个“查看详情”链接。点击“机械键盘”,地址应变成
/products/101,详情页应显示“当前商品 ID:101”。步骤 3:直接输入详情地址。
在地址栏输入
/products/102并刷新。页面仍应显示商品 ID 102,说明商品身份保存在 URL 中,不依赖刚才从哪个页面跳过来。步骤 4:检查参数名。
临时把组件 prop 的
id改成productId,页面会得不到原来的值,因为路由占位符仍叫id。恢复名称后,详情应重新显示。
动态参数只表示页面身份
本章不发送接口请求。详情页只显示 URL 中的 id,不能因为页面出现了 id 就认为商品数据已经加载成功。
当堂练习(分层双轨制)
- 【必做任务(基础通关)】:课堂案例只有商品
101和102。修改ProductDetailView.vue:访问这两个 id 时正常显示详情;访问/products/999等未知 id 时显示“商品不存在”,不能出现空白页。只判断当前id是否在已知 id 列表中,不发接口请求。 - 【选做挑战(🌟 自主拓展)】:在商品不存在时,额外提供一个“返回商品列表”的命名路由跳转链接(使用
<RouterLink :to="{ name: 'product-list' }">),确保用户在误入未知商品时有明确的返回途径。
4.4 知识点三:嵌套路由与公共布局
学习目标
掌握父子路由、嵌套 RouterView、子路由相对路径规则与 404 兜底路由,能够让后台页面共用侧栏骨架并在局部内容区顺畅切换。
语法/概念
后台管理系统布局痛点与嵌套路由设计思想
在典型的中后台管理系统中,页面的外部骨架(左侧导航菜单、顶部面包屑与用户信息)在绝大多数页面中是固定常驻的,只有右侧主体内容区域需要随业务模块切换。
如果采用扁平的普通路由配置,每个页面组件都必须在模板里手动编写一份侧栏代码。这不仅会导致海量模板代码冗余,而且每次路由切换时都会触发整套侧栏的重新挂载与销毁,导致侧栏滚动位置、折叠状态丢失,界面出现明显抖动。
嵌套路由(Nested Routes)通过“父路由承载公共布局、子路由挂载局部插座”的树状分层设计,完美实现外层布局骨架持久常驻、内层业务模块无感切换。
核心配置代码骨架与关键属性深度拆解
{
path: '/admin', // 1. 父级路径:管理端模块统一前缀
component: AdminLayout, // 2. 布局组件:包含公共侧栏与子路由插座
redirect: { name: 'dashboard' }, // 3. 访问重定向:直接访问 /admin 时自动跳转到默认子页面
children: [ // 4. 嵌套子路由表:声明挂载在该布局内的所有子页面
{
path: 'dashboard', // 核心规则:相对路径,绝无前导斜杠 /
name: 'dashboard', // 子路由唯一标识
component: DashboardView, // 挂载到 AdminLayout 内部插座的页面组件
},
{
path: 'products', // 浏览器完整访问路径拼接为:/admin/products
name: 'product-list',
component: ProductListView,
},
{
path: 'products/:id', // 嵌套子级同样支持动态参数,完整路径为:/admin/products/:id
name: 'product-detail',
component: ProductDetailView,
props: true, // 动态参数依然支持解耦传入
},
],
}配置属性深度拆解:
| 配置属性 | 属性类型 | 核心作用与工作机制 | 教学要点与避坑说明 |
|---|---|---|---|
path: '/admin' | string | 父级模块根路径。作为所有子路由的统一命名空间与前缀 | 顶级路由必须以斜杠 / 开头,声明基础访问入口 |
component: AdminLayout | Component | 公共布局外壳组件。渲染固定常驻的侧栏、顶栏以及供子页面渲染的局部 <RouterView /> | 必须在模板中留有子路由插座,否则子路由将无处展示 |
redirect | RouteLocation | 默认路由重定向。当用户直接访问 /admin 时,由路由器自动重写 URL 并引导至工作台 | 避免用户访问父路径时看到空荡荡的未激活工作区 |
children | RouteRecord[] | 嵌套子路由数组。子路由规则由父级统一纳管,路径自动完成层级拼接 | 子路由的 path 必须严格遵循相对路径规范 |
核心机制深度解析:双层 <RouterView /> 插座层级模型
嵌套路由的本质是“插座的多级套嵌”,整个系统包含两个层级的出口:
访问 URL: /admin/dashboard
│
├─ 第一级(顶层出口 · 位于 App.vue):
│ 匹配到 /admin 根规则 → 渲染 AdminLayout.vue 布局组件
│
└─ 第二级(局部出口 · 位于 AdminLayout.vue 的 <main> 中):
匹配到 children 中的 dashboard → 渲染 DashboardView.vue 到布局插座内部- 顶层插座(
App.vue):负责决定整页的宏观版式。例如访问/login时渲染全屏独立的登录页面,访问/admin/*时渲染带有完整后台骨架的AdminLayout。 - 内部子插座(
AdminLayout.vue):负责管理端内部各模块的局部切换。切换菜单时,外部的AdminLayout(以及内部的侧栏组件)完全不受影响,不发生销毁重建。
关键避坑指南:前导斜杠 / 的致命陷阱
这是初学者配置嵌套路由时最高频的语法错误:
- 正确写法(相对路径):
path: 'dashboard'或path: 'products'。Vue Router 会自动将其与父路径拼接为/admin/dashboard与/admin/products。 - 错误写法(误加前导斜杠):
path: '/dashboard'。- 严重后果:在 Vue Router 的底层解析规范中,任何以
/开头的路径都会被强制视为根绝对路径。一旦写成path: '/dashboard',它将不再挂载在/admin之后,导致路由匹配规则严重混乱,无法正常呈现后台布局。
- 严重后果:在 Vue Router 的底层解析规范中,任何以
404 兜底路由与正则通配机制
在单页应用中,用户随时可能手动输入错误的 URL 地址。必须在路由表的最末端提供一条通配兜底路由:
{
// 使用自定义正则表达式通配符匹配所有未命中的任意路径
path: '/:pathMatch(.*)*',
name: 'not-found',
component: NotFoundView,
}- 通配机制剖析:Vue Router 4 废弃了旧版本的简单星号
*,采用标准正则语法。/:pathMatch(.*)*中,:pathMatch是捕获参数的名称,(.*)表示匹配任意数量的任意字符,末尾的*表示支持重复匹配多级路径(如/foo/bar/baz)。 - 必须放置在路由表最底部:Vue Router 的匹配策略是自上而下、先声明先命中。如果把通配路由写在数组最前方,后续所有正常业务路由都会被它提前拦截;只有把通配路由放在数组最后一位,它才能在所有业务路由均未命中时发挥兜底保护作用。
本节最终页面关系如下:
管理端路由结构
src
App.vue# 最外层路由出口
router.js# 父子路由与 404
layouts
AdminLayout.vue# 后台侧栏与子路由出口
views
HomeView.vue
DashboardView.vue
ProductListView.vue
ProductDetailView.vue
ProfileView.vue# 当堂练习新增
NotFoundView.vue
课堂演示
步骤 1:创建后台布局和必要页面。
新建
src/layouts/AdminLayout.vue、src/views/DashboardView.vue、src/views/NotFoundView.vue。商品列表和商品详情继续使用上一节已经创建的组件。步骤 2:配置父子路由和 404。
子页面最终要进入父布局中的 RouterView。先指出布局里的出口,再解锁后台骨架。
保存后,访问
/admin应自动进入/admin/dashboard,并显示侧栏和工作台内容。步骤 3:按顺序验收嵌套路由。
- 访问
/admin/products:侧栏保持不动,内容区显示商品列表; - 访问
/admin/products/101:内容区显示商品 ID 101; - 使用浏览器返回键:只切换路由页面;
- 访问
/this-page-does-not-exist:显示 404 页面。
每个地址都要直接粘贴到地址栏访问一次。保存和运行后,不能只靠点击侧栏完成验收。
- 访问
两个最高频踩坑警示
- 前导斜杠陷阱(/):子路由
path必须写相对路径(如path: 'products'),如果随手多写了开头斜杠(写成path: '/products'),Vue Router 会把它当作根绝对路径处理,导致无法按预期挂载在/admin下。 - 缺失子插座出口:后台地址在地址栏匹配成功但右侧内容区一片空白,优先检查
AdminLayout.vue中是否放置了子路由出口<RouterView />。
当堂练习(分层双轨制)
- 【必做任务(基础通关)】:新增
src/views/ProfileView.vue,显示“个人中心”;把它配置为/admin/profile子路由(注意子路由path保持相对路径profile,不要加前导斜杠/),并在AdminLayout.vue侧栏增加“个人中心”导航链接。保存后切换“工作台、商品管理、个人中心”时,侧栏必须保持不动,只替换内容区。 - 【选做挑战(🌟 自主拓展)】:在
AdminLayout.vue的样式中利用.router-link-active为“个人中心”及其他菜单项定制选中高亮底色与文字颜色,让用户在侧栏中能清晰辨识当前处于哪个管理模块。
4.5 知识点四:导航守卫
学习目标
掌握路由 meta 元信息标记、全局前置守卫 beforeEach 的运行机制与返回值控制,能够实现未登录拦截、登录后原路精准回跳,并规避守卫死循环陷阱。
语法/概念
中后台访问控制与门禁安检心智模型
在管理端系统中,绝大部分页面涉及业务数据与管理权限,不能允许访客在未授权的情况下随意通过修改地址栏直接进入。
全局前置导航守卫(router.beforeEach)就如同“机场登机口的安检通道”:用户在前端发起的每一次页面跳转,在目标组件实际被挂载渲染之前,都必须先在此接受资格核验。安检合格方可放行进入;核验不合格则会被引导至服务台(登录页)办理入场凭据。
核心守卫代码骨架与关键属性深度拆解
全局前置守卫的标准实现骨架如下:
router.beforeEach((to, from) => {
// 1. 获取客户端会话中的登录凭证状态
const signedIn = Boolean(sessionStorage.getItem('demo-session'))
// 2. 核心鉴权条件:目标页面要求登录 且 当前处于未登录状态
if (to.meta.requiresAuth && !signedIn) {
// 3. 拦截跳转:重定向至登录页,并暂存原目标地址以便登录后原路返回
return {
name: 'login',
query: { redirect: to.fullPath },
}
}
// 4. 凭证有效或访问公开页面,放行本次导航
return true
})核心概念与关键机制深度拆解:
| 核心要素 | 机制类型 | 核心作用与底层逻辑 | 关键注意事项 |
|---|---|---|---|
to | RouteLocation | 即将进入的目标路由对象。包含解析后的完整信息(如 to.path、to.name、to.meta、to.fullPath、to.params) | 守卫通过检查 to 的属性来决定放行还是拦截 |
from | RouteLocation | 当前正要离开的路由对象。记录跳转前的来源页面上下文 | 可用于判断导航来源路径或记录回退历史 |
meta | RouteMeta | 路由元信息打标。为路由记录附加自定义业务元数据。父路由标记 requiresAuth: true,其所有子路由均处于保护伞下 | 解耦鉴权逻辑与具体页面组件,配置清晰可维护 |
return 返回值 | 控制信号 | 决定导航最终走向:return true 放行;return { name: 'login' } 重定向;return false 取消导航 | Vue Router 4 极力推崇 return 控制,避免旧版 next() 的多次调用漏洞 |
核心机制深度解析:登录后精准原路回跳(redirect)闭环
在实际业务中,未登录用户直接点击特定分享链接(如 /admin/products/101)被拦截是常见场景:
- 痛点:若用户登录成功后一律死板跳转到工作台首页,用户不得不重新在一层层菜单中寻找刚才想要查看的商品,交互流程割裂。
- 闭环方案:
- 拦截时暂存:守卫拦截未登录访问时,将目标完整地址
to.fullPath(包含路径与查询参数)保存至 URL 的 query 参数中:query: { redirect: to.fullPath }。此时登录页 URL 变为/login?redirect=%2Fadmin%2Fproducts%2F101。 - 登录后读取并送达:登录成功后,读取
route.query.redirect,调用router.replace(target)直接精准跳转回目标商品详情页,若无该参数则兜底进入默认工作台,达成无缝流畅的业务闭环。
- 拦截时暂存:守卫拦截未登录访问时,将目标完整地址
关键避坑指南:守卫死循环陷阱(Infinite Redirect Loop)
这是初学者在编写守卫时极容易导致的灾难性错误:
- 错误示范:
// 错误!未做排除判断,必然引发死循环! router.beforeEach(() => { if (!signedIn) { return { name: 'login' } } return true }) - 死循环原理:
- 用户未登录访问
/admin→ 命中!signedIn→ 重定向至/login; - 进入
/login再次触发beforeEach守卫; - 守卫再次执行,检测到依然
!signedIn,再次重定向至/login; - 如此无限递归触发,最终导致浏览器页面卡死崩溃,控制台抛出
RangeError: Maximum call stack size exceeded错误。
- 用户未登录访问
- 正规防御手段: 必须为跳转施加边界保护,例如只对带有
to.meta.requiresAuth的受保护页面进行拦截,或者显式判断排除登录页自身(to.name !== 'login')。
演示登录标记与生产安全边界
本章演示统一使用浏览器会话存储:
// 写入演示会话标记
sessionStorage.setItem('demo-session', 'yes')
// 退出登录时清除标记并回到登录页
function logout() {
sessionStorage.removeItem('demo-session')
router.push({ name: 'login' })
}安全认知边界
前端路由守卫与 sessionStorage 仅用于提升客户端单页应用的用户体验与页面流转控制(防君子不防小人)。客户端的数据和代码完全可以被用户在开发者工具中篡改。涉及真实业务数据的访问,必须由后端接口验证 JWT Token 或 Session 会话,前端守卫绝不能替代后端权限校验。
播放下面的流程,观察“准备进入后台—检查标记—进入登录页—完成登录—回到原地址”的顺序。
交互演示
导航守卫如何控制访问入口
守卫只做访问判断,不把权限安全全部押在前端。
- 1发起导航to / from
- 2读取会话auth state
- 3做出决定allow / redirect
- 4后端复核API authorization
当前发生
路由器得到目标页面和来源页面。
观察证据
守卫能读取目标路由 meta。
课堂演示
步骤 1:创建公共登录页。
新建
src/views/LoginView.vue。登录页放在后台父路由外面,因此它不会显示后台侧栏。步骤 2:给后台路由加 meta,并注册 beforeEach。
路由记录中保存访问策略的字段是 meta。解锁后,把路由文件和登录页改成下面这样。示例只展示演示主线;若已完成 4.2、4.4 的关于页或个人中心练习,请保留自己新增的导入、路由和导航,不要整文件覆盖后丢掉练习成果。
保存后,后台父路由和它的子路由都会带有
requiresAuth访问标记。步骤 3:验证未登录拦截和登录回跳。
打开浏览器 Console,先执行:
sessionStorage.removeItem('demo-session')然后直接访问
/admin/products/101。页面应先进入:/login?redirect=/admin/products/101点击“模拟登录”后,应回到
/admin/products/101,并显示商品 ID 101。步骤 4:验证 404 不被后台守卫误拦截。
访问
/unknown-page,应直接显示 404,而不是进入登录页。说明只有带requiresAuth的路由才进行登录检查。
避坑警示:守卫死循环陷阱(Infinite Redirect Loop)
在 beforeEach 中进行条件跳转时,务必确保目标路由不会再次触发相同的重定向规则!例如:若直接写 if (!signedIn) return { name: 'login' },却没有判断目标页面本身是不是登录页(to.name === 'login')或是否需要鉴权(to.meta.requiresAuth),守卫就会在 /login 上不断重定向到自己,引发浏览器卡死或报 RangeError: Maximum call stack size exceeded 错误。
边界提醒
前端守卫只负责页面入口体验。用户可以修改浏览器里的 JavaScript 和存储内容,所以敏感接口仍必须由服务端检查身份和权限。
当堂练习(分层双轨制)
- 【必做任务(基础通关)】:清除模拟登录标记,直接访问
/admin/products/101,确认被拦截到登录页;点击模拟登录后应能进入后台。再清除标记,确认下一次访问后台会重新被拦截,而/login和未知地址的 404 页面仍可公开访问。 - 【选做挑战(🌟 自主拓展)】:补上“已经登录的人访问
/login时自动回到工作台”的防重复进入分支。完成后先模拟登录,再手动在地址栏访问/login,验证地址会自动变回/admin/dashboard且不会出现死循环。课堂工程有意不提供这个分支。 - 【选做挑战(🌟 自主拓展)】:在
AdminLayout.vue侧栏或顶栏增加“退出登录”按钮,绑定点击事件执行sessionStorage.removeItem('demo-session')并跳转回登录页,验证退出后再尝试进入后台能够被守卫精准重新拦截。
4.6 知识点五:History 部署边界
🌟 进阶选学:路由模式对比与生产部署边界
在平时本地开发(npm run dev)时,Vite 开发服务器已经内置了单页应用路由回退,无论使用 History 模式还是 Hash 模式,页面切换和直接刷新都能正常工作。本节内容主要帮助学生建立起“前端单页路由与 Web 生产服务器(如 Nginx/Apache)协同工作”的架构视野,理解 URL 中的 # 到底起什么作用,供学有余力或准备上线的同学自主探索,不作为基础实训卡点考核。
学习目标
掌握 History 模式与 Hash 模式的底层网络请求差异,理解单页应用在生产服务器(如 Nginx)上的 404 刷新边界与 SPA 回退配置原理。
语法/概念
本地开发与生产部署的认知割裂
在前端工程化开发中,很多初学者会遇到这样的困惑:
- 在本地执行
npm run dev时,不论如何切换菜单、直接刷新深层详情地址(如/admin/products/101),页面均能正常渲染; - 但一旦执行
npm run build将打包产物放到生产服务器(如 Nginx、Apache、云存储静态托管)后,只要在深层地址按 F5 刷新,浏览器立即弹出服务器底层的 404 Not Found 错误。
这种现象的根源在于浏览器网络请求机制与服务器静态文件查找规则。
底层机制探秘:为什么 # 号有魔力?(两种模式对比)
Vue Router 提供了两种核心的历史记录管理模式:
| 对比维度 | HTML5 History 模式 (createWebHistory) | Hash 模式 (createWebHashHistory) |
|---|---|---|
| URL 外观 | 自然美观,如 /admin/products/101 | 包含井号,如 /#/admin/products/101 |
# 的网络语义 | 无井号,路径完全遵循传统 URL 规范 | 井号后为URL 锚点(Hash Fragment),HTTP 协议规定绝不向服务器发送 |
| 刷新时的请求 | 浏览器向服务器发起 GET /admin/products/101 真实文件请求 | 浏览器向服务器仅发起 GET / 根路径请求,获取 index.html |
| 生产服务器依赖 | 必须配置服务器 SPA 回退(Fallback),否则深层刷新报 404 | 静态文件服务器开箱即用,无需任何额外反向代理或回退配置 |
| 架构与选型场景 | 现代商业项目、B端管理端与 C端产品的首选 | 内部临时演示系统、无服务器配置权限的纯静态托管空间 |
核心代码骨架与模式声明
History 模式配置(工业界主流实践):
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
// 采用 HTML5 History API(pushState / replaceState / popstate)
// URL 呈现为真实的自然路径,需要服务端配套配置支持
history: createWebHistory(),
routes,
})Hash 模式配置(免服务端配置实践):
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({
// 采用 location.hash 与 hashchange 事件监听
// 所有路由信息均保存在 # 之后,无需服务端做任何重写配置
history: createWebHashHistory(),
routes,
})生产环境 Nginx 单页回退配置原理
在 History 模式下,当用户在浏览器中刷新 /admin/products/101 时,Nginx 会在本地磁盘寻找是否有对应文件。因为单页应用只有一个唯一的宿主入口文件 dist/index.html,服务器找不到对应文件就会抛出 404。
解决方案是为 Web 服务器配置 SPA 单页应用回退规则。以工业界最常用的 Nginx 为例:
server {
listen 80;
server_name mall.campus.edu;
location / {
# 前端打包产物 dist 目录所在的物理磁盘路径
root /usr/share/nginx/html;
index index.html;
# 核心回退规则:按顺序探测物理文件、物理目录;若均未找到,兜底重写至 index.html
try_files $uri $uri/ /index.html;
}
}$uri:检查当前请求是否存在具体的物理静态资源(如main.js、style.css、logo.png)。如果存在,直接作为静态文件返回(200)。$uri/:检查当前请求是否对应物理子目录。/index.html:若前两者均不命中(说明当前请求是一个前端自定义的路由路径),Nginx 将请求内部转发回唯一的单页入口index.html。- 闭环生效:浏览器接收到
index.html后执行其中的 JavaScript 脚本,Vue Router 启动并检测当前地址栏的/admin/products/101,精准渲染对应的商品详情组件,成功解决生产环境 404 痛点。
课堂演示
步骤 1:确认当前使用 History 模式。
先完成模拟登录,确认
sessionStorage中已经有demo-session。再检查src/router.js中是否导入并调用createWebHistory()。执行npm run dev后访问/admin/products/101,地址栏不应出现#。步骤 2:直接刷新深层地址。
在
/admin/products/101页面按刷新。Vite 开发服务器下应仍能返回 Vue 应用,再由路由显示商品详情。步骤 3:对照现象表判断问题属于谁。
现象 先判断什么 点击菜单正常,刷新深层地址出现服务器 404 静态服务器是否配置 SPA 回退 所有页面都打不开 Vue 应用或部署目录本身是否正确 页面能开,图片资源 404 BASE_URL与部署子路径是否一致观察后应能说明:点击菜单正常,不代表生产环境刷新深层 URL 一定正常。
步骤 4:保留最终课程写法。
本章最终代码继续使用
createWebHistory()。保存后重新运行,地址栏应保持自然路径,不出现#。
当堂练习(分层双轨制)
- 【必做任务(基础通关)】:确认当前项目使用
createWebHistory(),访问商品详情/admin/products/101,记录此时的自然 URL 样式;在浏览器中直接按 F5 刷新,验证本地 Vite 开发服务器能够正常响应并渲染详情页。 - 【选做挑战(🌟 自主拓展)】:把
createWebHistory()临时换成createWebHashHistory(),保存后访问商品详情,观察地址栏中#的位置变化;尝试在新标签页直接粘贴并刷新带有#的地址,对比两者在表现形式和服务器请求上的本质差异,验证完毕后切回createWebHistory()。
4.7 排错矩阵
| 现象 | 证据 | 常见原因 |
|---|---|---|
| 页面完全空白 | Console 报 router 或组件错误 | main.js 没有 .use(router),或导入路径写错 |
| 地址变化但页面不变 | 页面中找不到对应出口 | 缺少 <RouterView /> |
| 点击导航整页刷新 | Network 中重新请求整个 HTML | 站内导航误用了普通 <a href> |
商品 id 是 undefined | 路由参数名与组件 prop 不一致 | :id、props: true、组件 id 没有对齐 |
| 子路由匹配不到 | /admin/products 没进入后台布局 | 子路由 path 错写成不需要的绝对路径 |
| 后台布局出现但内容区空白 | 父路由已匹配,子页面没显示 | AdminLayout 缺少嵌套 RouterView |
| 未登录也能进入后台 | to.meta.requiresAuth 是空值 | 受保护父路由没有配置 meta |
| 登录后只回工作台 | 登录前 URL 有 redirect,登录后没读取 | LoginView 丢失了 route.query.redirect |
| 守卫反复跳转 | 地址在登录页和后台之间来回变化 | 登录页也被错误拦截,或分支没有结束 |
| 未知地址显示空白 | 路由表没有匹配结果 | 缺少最后的 404 兜底路由 |
| 开发环境正常,部署刷新 404 | 服务器响应的不是 index.html | History 模式缺少 SPA 回退 |
学习自检
停一下:URL 与页面一致吗
确认路由映射、布局出口、动态参数和访问入口都能验证。
4.8 本章小测
章节测评
3 题第4章小测:路由与访问入口
检查 URL 映射、嵌套出口和安全边界。
4.9 本章知识自检
| 能力 | 达标标准 |
|---|---|
| 最小接入 | 能说明 app.use、RouterLink 和 RouterView 分别做什么 |
| 路由映射 | 能从 URL 找到对应路由记录与页面组件 |
| 动态参数 | 商品详情通过 props 接收 id,直接刷新地址仍能恢复 |
| 嵌套路由 | 后台公共布局稳定,子页面进入正确的 RouterView |
| 404 | 未知地址显示明确页面和回退入口 |
| 登录守卫 | 未登录拦截、登录后回跳、已登录不再停留登录页 |
| 登录标记 | 全章统一使用 sessionStorage 的 demo-session |
| 安全边界 | 能解释前端守卫不能代替后端授权 |
| 部署边界 | 能说明 History 刷新为什么依赖服务器回退 |
4.10 课后任务:校园服务后台导航
任务目标
使用 resources/ch04/after-class-starter/ 起始项目,为校园服务后台完成登录页、工作台、场地列表、场地动态详情、公共后台布局、登录守卫和 404 页面。未登录直接访问后台时先进入登录页,基础任务登录后进入工作台;回到原本想访问的地址属于进阶拓展。
起始工程已提供公共布局、工作台、场地列表、模拟登录及基础未登录拦截。按 TODO(基础) 补上动态详情路由、404 兜底和退出登录;按 TODO(提高) 选做原地址回跳与防重复登录。
基础通关要求(全员必做)
- 建立
/login、/service/dashboard、/service/rooms、/service/rooms/:id和 404 兜底路由。 /service下的工作台、场地列表和场地详情共用同一套侧栏布局与内容出口<RouterView />。- 场地详情通过
props: true接收动态参数id,页面显示当前场地 id,不发送接口请求。 - 使用路由
meta: { requiresAuth: true }标记受保护页面;未登录时由beforeEach守卫拦截跳转至登录页。 - 登录标记统一使用
sessionStorage的demo-session;登录成功后能进入工作台,退出登录后再次访问后台必须被重新拦截。 - 未知地址显示 404 页面,并提供回到校园服务工作台的链接。
- 项目使用
createWebHistory(),并能在开发环境直接打开场地详情地址。
进阶拓展任务(自主选做 · 🌟 自主拓展)
- 精准原路回跳:在守卫拦截未登录访问时,将目标路径保存至
query.redirect;登录成功后读取并优先精准回跳至原目标地址(如/service/rooms/201)。 - 防重复登录:在
beforeEach中判断若已登录用户手动访问/login,自动重定向回工作台。 - 部署认知探索:临时把
createWebHistory()换成createWebHashHistory(),观察地址栏出现#后再恢复;直接刷新一个 History 深层地址,思考并记录生产服务器为什么需要单页应用(SPA)回退到index.html。
必须使用的知识
- 基础核心:嵌套路由与父布局中的
RouterView、动态参数与props: true、命名路由、路由meta与基础beforeEach守卫拦截、404 兜底路由、sessionStorage状态检查。 - 进阶拓展(🌟 自主拓展):
redirect原地址保存与登录后精准回跳、防重复登录拦截、Hash 模式与 History 模式对比及生产环境 SPA 回退机制。
完成效果
- 基础效果:
- 未登录直接访问后台时被守卫拦截到登录页。
- 点击模拟登录后可进入后台,工作台、场地列表和场地详情共用同一侧栏,切换子页面时只替换内容区。
- 场地详情页能正常接收并显示“当前场地 ID”。
- 清除登录标记后再次访问后台,会重新被拦截至登录页。
- 访问未知地址时显示 404 和“回到工作台”链接,不出现空白页。
- 拓展效果(🌟 自主拓展):
- 访问
/service/rooms/201被拦截后,登录成功直接返回该详情页; - 已登录访问
/login时自动回到工作台; - 理解 Hash 地址与 History 地址的差异及生产服务器回退原因。
- 访问
验收步骤
- 清除
demo-session,直接访问/service/dashboard,确认被拦截进入登录页。 - 点击模拟登录,确认能顺利进入工作台。
- 依次切换工作台、场地列表和场地详情,确认公共侧栏保持不动,内容区正常切换。
- 清除登录标记后再次访问
/service/dashboard,确认重新进入登录页。 - 访问
/unknown-page,确认显示 404 页面和可点击的回退入口。 - 执行
npm run build,确认构建成功且 Console 没有路由错误。 - (🌟 自主拓展验收):验证带
redirect的精准回跳、已登录访问/login的重定向,以及 Hash/History 切换观察。
提交内容
- 完整项目源码,不包含
node_modules/和dist/; - 动态详情与 404 两张运行效果截图;
- 一张
npm run build成功终端截图; - (🌟 自主拓展选交):登录回跳截图、Hash 地址截图或不超过 150 字的 SPA 回退理解说明。
边界提醒
前端守卫只能控制页面入口,不能代替后端权限校验。真实项目仍必须让服务端验证身份、登录状态和操作权限。
本章小结
- 路由把 URL 映射到页面组件,
RouterView是匹配页面的显示出口。 - 动态参数把商品身份放进 URL,命名路由和
params减少手动拼接路径。 - 嵌套路由让多个后台页面复用同一布局,404 路由负责接住未知地址。
meta与beforeEach可以组织登录入口,redirect让登录后回到原目标页。- 全章演示登录标记统一使用
sessionStorage的demo-session,但它不能代替后端授权。 - History 模式地址自然,生产服务器需要为深层 URL 配置单页应用回退。
- 下一章会使用 Element Plus 完成商品列表、表单、弹窗和操作反馈。
