【Vue3速成】05-vue3官方库路由机制之路由守卫

🎯 你正在阅读「半路出家玩前端」系列文章 🎯

🔥 弹简特 个人主页

❄️ 个人专栏直通车:

靠热爱去书写自己,靠勇敢去书写生活!


🌟 博主简介:


文章目录:

    • 前言
    • [一、Vue 到底怎么执行?](#一、Vue 到底怎么执行?)
      • [1.1 打开浏览器后的真实顺序](#1.1 打开浏览器后的真实顺序)
        • [📖 说明(对照 `main.js`)](#📖 说明(对照 main.js))
      • [1.2 一次跳转时 Router 内部怎么跑](#1.2 一次跳转时 Router 内部怎么跑)
        • [📖 图解说明(对照 `router.js`)](#📖 图解说明(对照 router.js))
    • [二、Script ↔ Template:前端最易懵的地方](#二、Script ↔ Template:前端最易懵的地方)
      • [2.1 为什么模板能写脚本里的变量名?](#2.1 为什么模板能写脚本里的变量名?)
        • [📖 核心结论](#📖 核心结论)
      • [2.2 输入 / 点击时数据怎么走](#2.2 输入 / 点击时数据怎么走)
    • 三、三层数据:别再混在一块
    • 四、对照源码:Login.vue
      • [4.1 脚本:账本](#4.1 脚本:账本)
      • [4.2 模板:前台](#4.2 模板:前台)
      • [4.3 成功 / 失败两条路](#4.3 成功 / 失败两条路)
    • [五、对照源码:router.js(门牌 + 门禁)](#五、对照源码:router.js(门牌 + 门禁))
      • [5.1 门牌表](#5.1 门牌表)
      • [5.2 门禁(完整逻辑)](#5.2 门禁(完整逻辑))
    • [六、对照源码:Home.vue(挂载 + 注销)](#六、对照源码:Home.vue(挂载 + 注销))
      • [6.1 挂载后读卡](#6.1 挂载后读卡)
      • [6.2 注销:撕卡 + 回登录](#6.2 注销:撕卡 + 回登录)
    • 七、入口与出口(两行也不能少)
      • [7.1 `App.vue`:只有显示屏](#7.1 App.vue:只有显示屏)
      • [7.2 启动三连](#7.2 启动三连)
    • [八、底层存储:栈 / 堆 / `.value`](#八、底层存储:栈 / 堆 / .value)
    • 九、全流程串起来(一张脑图)
    • 十、边界速查表

前言

本期我们通过学习一个登录案例,来理解我们路由机制中的路由守卫内容。

1、首先我们的案例原型

2、案例结构

3、案例代码

3.1 根组件
html 复制代码
<!--
  根组件:只负责提供路由出口
  真正显示 Login / Home 的,是下面的 <router-view>
-->
<script setup>
// 根组件不需要再手动 import Login/Home
// 组件由 router.js 的 routes 按路径自动匹配并渲染
</script>

<template>
  <div id="app-root">
    <!--
      路由出口(显示屏):
      - 访问 /login → 渲染 Login.vue
      - 访问 /home  → 渲染 Home.vue
      没有这个标签,路由配置再对页面也是空白
    -->
    <router-view />
  </div>
</template>

<style scoped>
/* 根组件保持简洁,具体样式交给各业务页面 */
</style>
3.2 main.js注册文件
js 复制代码
/**
 * 应用入口文件
 * 职责:创建 Vue 应用 → 注册路由 → 挂载到页面
 * 顺序很重要:必须先 use(router),守卫和路由表才会生效
 */

// 从 Vue 导入创建应用的方法
import { createApp } from 'vue'

// 引入全局样式(可选,不影响登录/守卫逻辑)
import './style.css'

// 引入根组件 App.vue(里面必须有 <router-view> 作为路由出口)
import App from './App.vue'

// 引入路由配置(包含 routes 门牌表 + beforeEach/afterEach 门禁)
import router from './routers/router.js'

// 1) createApp(App)  : 创建应用实例
// 2) .use(router)    : 把路由器插进应用(此后才能跳转/拦截)
// 3) .mount('#app')  : 挂到 index.html 里的 #app 节点上
createApp(App).use(router).mount('#app')
3.3 登录页面组件
html 复制代码
<!--
  登录页:核对 → 记账 → 进门
  1) v-model 绑定账号密码
  2) === 校验(笔记示例:admin / 123456)
  3) 写入 Storage(公共信箱,供 Home 和守卫共享)
  4) router.push 跳到首页
-->
<script setup>
// ref:创建响应式变量(输入框改了,变量跟着变)
import { ref } from 'vue'

// useRouter:拿到路由实例,才能在 JS 里编程式跳转
import { useRouter } from 'vue-router'

// 账号 / 密码:给默认值方便测试(与课程笔记一致)
// 正式环境可改成 ref('')
const username = ref('admin')
const password = ref('123456')

// 获取路由对象(后面用 router.push 跳转)
const router = useRouter()

/**
 * 登录按钮点击事件
 * 成功:写通行证 + 跳首页
 * 失败:弹窗提示,保留已输入内容方便重试
 */
const login = () => {
  // 调试:看当前输入(注意必须 .value,否则拿到的是 ref 包装对象)
  console.log('尝试登录:', username.value, password.value)

  // 严格相等校验(推荐 ===,不要用 ==,也不要用单个 &)
  // 边界:漏写 .value → 永远对不上
  if (username.value === 'admin' && password.value === '123456') {
    // ---------- 写入公共信箱(通行证)----------
    // Storage 只能存字符串,所以必须存 username.value
    // 错误示范:setItem('username', username) → 常变成 [object Object]

    // localStorage:关浏览器还在(公告栏)
    window.localStorage.setItem('username', username.value)

    // sessionStorage:关标签页就清(门口小黑板)
    // 守卫 router.js 读的是 sessionStorage,这里必须同步写入
    window.sessionStorage.setItem('username', username.value)

    // ---------- 跳转到首页 ----------
    // 编程式导航:等价于去门牌 /home
    // 也可携带 query,但本项目以 Storage 共享登录态为主
    router.push({
      path: '/home',
      query: { username: username.value },
    })
  } else {
    // 失败分支:提示错误;不清空输入,降低重试成本
    alert('登录失败,账号或者密码错误!')
  }
}
</script>

<template>
  <div class="login-page">
    <h3>登录页面</h3>

    <!-- v-model:双向绑定,输入即同步到 username / password -->
    <label for="login-username">账号:</label>
    <input
      id="login-username"
      type="text"
      v-model="username"
      placeholder="请输入账号!"
    />
    <br />

    <!-- type=password:密码不明文显示;漏写会变成普通文本框 -->
    <label for="login-password">密码:</label>
    <input
      id="login-password"
      type="password"
      v-model="password"
      placeholder="请输入密码!"
    />
    <br />

    <!-- @click:点击触发 login 函数 -->
    <button type="button" @click="login">登录</button>
  </div>
</template>

<style scoped>
/* 页面样式保持简单,聚焦登录逻辑 */
.login-page {
  padding: 16px;
}
</style>
3.4 主页面组件
html 复制代码
<!--
  首页(受保护页):
  - 进入前会被 router.beforeEach 检查是否已登录
  - 挂载后从 Storage 读取用户名并展示
  - 注销时清除通行证并跳回登录页
-->
<script setup>
// ref:页面上要展示的用户名(响应式)
// onMounted:组件挂到页面后再读 Storage,时序更稳
import { ref, onMounted } from 'vue'

// 注销后需要跳回登录页
import { useRouter } from 'vue-router'

const router = useRouter()

// 欢迎语用的用户名;读不到时模板里显示「游客」
const username = ref('')

/**
 * 页面挂载完成后执行:
 * 从 Storage 取出登录时写入的 username
 * 注意:key 和 Storage 类型必须与 Login / 守卫保持一致
 */
onMounted(() => {
  // 守卫和登录都以 sessionStorage 为准
  const cacheName = window.sessionStorage.getItem('username')

  // 可能为 null(理论上守卫会拦住,这里仍做兜底)
  username.value = cacheName || ''

  console.log('[Home] 读取到的用户名:', username.value)
})

/**
 * 退出登录:
 * 1) 撕掉通行证(双清,防止登录时双写导致幽灵登录)
 * 2) 清空页面显示
 * 3) 跳回登录页
 */
const logout = () => {
  // 清会话存储(门口小黑板)
  window.sessionStorage.removeItem('username')

  // 清持久存储(公告栏)------ Login 里也写了 localStorage,这里一起删
  window.localStorage.removeItem('username')

  // 手动清空视图数据,欢迎语立刻变为游客
  username.value = ''

  // 回到登录页(之后再访问 /home 会被守卫拦截)
  router.push('/login')
}
</script>

<template>
  <div class="home-page">
    <h1>Home页面</h1>

    <!-- username 有值显示名字,否则显示游客 -->
    <h3>欢迎{{ username || '游客' }}登录</h3>

    <!-- 点击退出:清 Storage + 回登录页 -->
    <button type="button" @click="logout">退出登录</button>
  </div>
</template>

<style scoped>
.home-page {
  padding: 16px;
}
</style>
3.5 路由配置
js 复制代码
/**
 * 路由层:门牌对照表 + 小区门禁(路由守卫)
 *
 * 数据流回顾:
 *   Login 写 Storage(通行证)
 *   → 本文件 beforeEach 查 Storage(刷卡)
 *   → Home 读 / 删 Storage(展示 / 注销)
 */

// createRouter:创建路由实例
// createWebHashHistory:Hash 模式(地址带 #),兼容好,学习期少踩刷新 404
import { createRouter, createWebHashHistory } from 'vue-router'

// 引入页面组件,供 routes 做 path → component 映射
import Home from '../components/Home.vue'
import Login from '../components/Login.vue'

// =========================
// 1. 创建路由实例(门牌表)
// =========================
const router = createRouter({
  // Hash 模式:例如 http://localhost:5173/#/login
  history: createWebHashHistory(),

  // routes:路径与组件的对照表
  routes: [
    {
      path: '/home',
      component: Home, // 受保护页:没登录会被守卫拦住
    },
    {
      path: '/login',
      component: Login, // 白名单页:未登录也要能进(去办卡)
    },
    {
      path: '/',
      redirect: '/home', // 访问根路径时先去 /home,再由守卫决定放行或赶回登录
    },
  ],
})

// =========================
// 2. 全局前置守卫(门禁)
// =========================
// 时机:每次导航「跳转前」执行
// 参数:
//   to   ------ 要去哪里
//   from ------ 从哪里来
//   next ------ 放行 / 改道;不调用 next,页面会卡住
router.beforeEach((to, from, next) => {
  // 调试日志:方便观察导航路径
  console.log(`[前置守卫] 从 ${from.path} → 到 ${to.path}`)

  // ---- 白名单:登录页必须放行 ----
  // 为什么:未登录会被赶到 /login;
  // 如果这里再拦 /login,就会「未登录→去登录→再被拦」死循环
  if (to.path === '/login') {
    next() // 放行到登录页
    return // 结束,避免后面逻辑再执行
  }

  // ---- 非登录页:查通行证 ----
  // 与 Login.vue 写入保持一致(博客笔记二常用 sessionStorage)
  const username = window.sessionStorage.getItem('username')

  if (username !== null && username !== '') {
    // 有用户名:刷卡成功,放行进入目标页(如 /home)
    next()
  } else {
    // 没用户名:强制改道去登录页
    next('/login')
  }
})

// =========================
// 3. 全局后置守卫(流水账)
// =========================
// 时机:导航「已经完成」之后
// 注意:没有 next,不能在这里拦截,只适合打日志 / 统计
router.afterEach((to, from) => {
  console.log(`[后置守卫] 从 ${from.path} → 到 ${to.path}`)
})

// 导出给 main.js:createApp(App).use(router)
export default router

一、Vue 到底怎么执行?

1.1 打开浏览器后的真实顺序

📖 说明(对照 main.js
javascript 复制代码
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './routers/router.js'

createApp(App).use(router).mount('#app')
//   ①创建应用   ②挂上路由+守卫   ③挂到页面 #app
步骤 发生了什么 你肉眼看到的
index.html#app 空壳容器 还没内容
createApp(App) 创建应用,根组件是 App 还没上屏
use(router) 注册门牌表 + 守卫 仍不可见
mount('#app') 开始渲染 App 出现
⑤ App 里只有 <router-view> 留出"显示屏" 等路由填内容
⑥ 按 URL(hash)匹配路由 / → redirect /home 触发导航
beforeEach 查卡 没登录 → next('/login') 跳到登录页
⑧ 渲染 Login.vue 组件挂载 看见登录表单

一句话 :不是"先画 Login",而是 先启动应用 → 注册路由 → 挂载 → 守卫裁决 → 再渲染某个页面


1.2 一次跳转时 Router 内部怎么跑

📖 图解说明(对照 router.js
javascript 复制代码
// 任意跳转都会先经过这里
router.beforeEach((to, from, next) => {
  if (to.path === '/login') {
    next()                 // 白名单:放行
    return
  }
  const username = window.sessionStorage.getItem('username')
  username ? next() : next('/login')  // 有卡放行 / 没卡改道
})

router.afterEach((to, from) => {
  // 页面已经换完了,只能打日志,不能再拦
})
分支 条件 结果
成功进 Home Storage 里有 username next() → 渲染 Home
拦截回登录 Storage 没有 next('/login')
去登录页 to.path === '/login' 直接 next(),防死循环

二、Script ↔ Template:前端最易懵的地方

2.1 为什么模板能写脚本里的变量名?

📖 核心结论
  1. 同一个 .vue 文件 = 同一个组件实例
  2. <script setup> 顶层的 const username / const login 会被 Vue 自动暴露<template>
  3. 模板写 username,不是"碰巧同名",是编译时挂上的
  4. 跨页面(Login → Home)不会自动共享脚本变量,要靠 Storage

2.2 输入 / 点击时数据怎么走

用户动作 模板写法 脚本真实发生
敲键盘 v-model="username" 近似 username.value = 新值
点登录 @click="login" 调用脚本里的 login()
显示文字 {``{ username }} 读同一个 ref(模板自动解包)
html 复制代码
<!-- 本项目 Login.vue:两边名字必须对得上 -->
<script setup>
const username = ref('admin')   <!-- ① 脚本声明 -->
const login = () => { /* ... */ }
</script>

<template>
  <input v-model="username" />  <!-- ② 模板用同名:自动暴露 + 自动解包 -->
  <button @click="login">登录</button>
</template>
位置 读 ref 的写法 原因
<script> username.value 拿到的是盒子,要开盖
<template> username Vue 自动开盖

三、三层数据:别再混在一块

层级 存什么(本项目) 活多久 谁能看见
① 组件内存 ref Login 的账号密码;Home 的欢迎名 组件销毁就没 仅当前组件 script/template
② Storage username: "admin" 字符串 session 关标签清;local 更久 Login / Home / 守卫
③ URL hash /#/login/#/home 跟着地址栏 Router 用来匹配组件
text 复制代码
登录成功时发生的"抄写":
  ① username.value  ──setItem──►  ② sessionStorage
之后:
  守卫读 ② 决定放不放行
  Home 的 onMounted 再把 ② 抄回自己的 ①

四、对照源码:Login.vue

4.1 脚本:账本

javascript 复制代码
// src/components/Login.vue
import { ref } from 'vue'
import { useRouter } from 'vue-router'

const username = ref('admin')      // 响应式:模板 v-model 绑的就是它
const password = ref('123456')
const router = useRouter()         // 用来 push 跳转

const login = () => {
  // 脚本里必须 .value
  if (username.value === 'admin' && password.value === '123456') {
    // 抄到公共信箱(守卫读 session;同时写 local 方便对照)
    window.localStorage.setItem('username', username.value)
    window.sessionStorage.setItem('username', username.value)

    // 触发导航 → 会先跑 beforeEach
    router.push({ path: '/home', query: { username: username.value } })
  } else {
    alert('登录失败,账号或者密码错误!')
  }
}

4.2 模板:前台

html 复制代码
<label for="login-username">账号:</label>
<input id="login-username" type="text" v-model="username" />

<label for="login-password">密码:</label>
<input id="login-password" type="password" v-model="password" />

<button type="button" @click="login">登录</button>

4.3 成功 / 失败两条路

text 复制代码
点登录
  ├─ 账号密码对  → setItem → push('/home') → 守卫放行 → Home
  └─ 不对        → alert → 留在 Login(输入不清空)

五、对照源码:router.js(门牌 + 门禁)

5.1 门牌表

javascript 复制代码
// src/routers/router.js(节选)
const router = createRouter({
  history: createWebHashHistory(), // 地址带 #,如 /#/login
  routes: [
    { path: '/home', component: Home },   // 要登录
    { path: '/login', component: Login }, // 白名单
    { path: '/', redirect: '/home' },     // 进门先往 home 走,再由守卫裁决
  ],
})

5.2 门禁(完整逻辑)

javascript 复制代码
router.beforeEach((to, from, next) => {
  console.log(`[前置守卫] 从 ${from.path} → 到 ${to.path}`)

  if (to.path === '/login') {
    next()     // 办卡处不能拦
    return
  }

  const username = window.sessionStorage.getItem('username')
  if (username !== null && username !== '') {
    next()           // 有卡 → 进 /home 等
  } else {
    next('/login')   // 没卡 → 赶回登录
  }
})

自测口令

text 复制代码
清掉 Storage 后访问 /#/home  → 应被赶回 /#/login
登录成功后再访问 /#/home     → 应正常进入

六、对照源码:Home.vue(挂载 + 注销)

6.1 挂载后读卡

javascript 复制代码
// src/components/Home.vue(节选)
const username = ref('')

onMounted(() => {
  // 把 Storage(②) 抄回本组件 ref(①)
  const cacheName = window.sessionStorage.getItem('username')
  username.value = cacheName || ''
})
html 复制代码
<h3>欢迎{{ username || '游客' }}登录</h3>

6.2 注销:撕卡 + 回登录

javascript 复制代码
const logout = () => {
  window.sessionStorage.removeItem('username') // 清会话
  window.localStorage.removeItem('username')   // 双清,防幽灵登录
  username.value = ''
  router.push('/login') // 再闯 /home 会被守卫拦
}
text 复制代码
注销后时间线:
  removeItem → push('/login') → 用户再访问 /home
  → beforeEach 读不到 username → next('/login')

七、入口与出口(两行也不能少)

7.1 App.vue:只有显示屏

html 复制代码
<!-- src/App.vue -->
<template>
  <router-view />
  <!-- 没有它:路由匹配到了组件也无处渲染 -->
</template>

7.2 启动三连

javascript 复制代码
createApp(App).use(router).mount('#app')
漏写哪一步 后果
use(router) 跳转/守卫全无效
<router-view> 白屏
守卫不 next() 页面卡住

八、底层存储:栈 / 堆 / .value

javascript 复制代码
const username = ref('admin')
// 栈:username(引用) → 堆:RefImpl → 堆:'admin'

window.sessionStorage.setItem('username', username.value) // ✅ 字符串
// setItem('username', username)                          // ❌ [object Object]
写法 Storage 里实际值
username.value "admin"
username(整个 ref) "[object Object]"

九、全流程串起来(一张脑图)

text 复制代码
启动 main.js
  → App 露出 router-view
  → 默认往 /home
  → 守卫:没卡 → /login
  → Login:输入 → v-model 进 ref
  → 点登录:校验 → setItem → push /home
  → 守卫:有卡 → 放行
  → Home:onMounted 读卡 → 显示欢迎
  → 注销:双清 → /login

十、边界速查表

现象 原因 处理
模板报 username 未定义 没在 script 声明 / 拼写不一致 名字对齐
校验永远失败 脚本忘了 .value username.value
已登录仍被拦 登录写 local、守卫读 session 读写统一(本项目守卫读 session)
无限跳转 /login 没放行 白名单 next()
页面卡住 某分支没调 next 每条路都 next
注销后还能进 只清了一种 Storage 双清

相关推荐
天丁o2 小时前
我把一套 Flowable + Spring Boot + Vue 的 BPM 流程引擎整理开源了:从流程设计器到待办闭环
spring boot·vue·工作流引擎·flowable·bpm流程引擎·流程审批
小堂子这厢有礼了14 小时前
Chet.Admin 模块详解⑥:字典管理 + useDict 联动表单
vue·后台管理系统·vbenadmin·chet.admin
猫猫不是喵喵.21 小时前
Vue3 hooks 函数
vue
Pu_Nine_93 天前
Vue3+TS+Express 转 Nuxt4 全栈开发笔记(Drizzle ORM + MySQL)
前端·vue·express·nuxt.js·全栈框架
脾气有点小暴4 天前
ECharts 伪 3D 柱状图完整注释 + 实现原理说明
前端·3d·信息可视化·vue·echarts
sugar__salt4 天前
Vue3 核心特性深度解析:v-model、ref 属性与 nextTick 完全指南
前端·javascript·vue.js·前端框架·vue
勇往直前plus5 天前
Vue3(篇四)单页面应用到路由管理
javascript·typescript·前端框架·vue
勇往直前plus5 天前
Vue3(篇三) Element Plus
前端·javascript·typescript·vue
shawxlee6 天前
vue3+axios挑战最简洁实用的配置封装+接口调用:请求拦截器、响应拦截器、通用请求方法(单个请求/并发请求/下载文件)、统一接口管理
前端·经验分享·vue·接口·axios·api