🎯 你正在阅读「半路出家玩前端」系列文章 🎯
🔥 弹简特 个人主页
❄️ 个人专栏直通车:
- 🔌 接口测试从入门到跑路
- ☕ 一个后端的 JavaEE 续命指南
- 🛜 网络原理续命手册
- ☕ Java项目-轻聊
- ☕ Java项目-企悦抽
✨ 靠热爱去书写自己,靠勇敢去书写生活!
🌟 博主简介:

文章目录:
-
- 前言
-
- 1、首先我们的案例原型
- 2、案例结构
- 3、案例代码
-
- [3.1 根组件](#3.1 根组件)
- [3.2 main.js注册文件](#3.2 main.js注册文件)
- [3.3 登录页面组件](#3.3 登录页面组件)
- [3.4 主页面组件](#3.4 主页面组件)
- [3.5 路由配置](#3.5 路由配置)
- [一、Vue 到底怎么执行?](#一、Vue 到底怎么执行?)
-
- [1.1 打开浏览器后的真实顺序](#1.1 打开浏览器后的真实顺序)
-
- [📖 说明(对照 `main.js`)](#📖 说明(对照
main.js))
- [📖 说明(对照 `main.js`)](#📖 说明(对照
- [1.2 一次跳转时 Router 内部怎么跑](#1.2 一次跳转时 Router 内部怎么跑)
-
- [📖 图解说明(对照 `router.js`)](#📖 图解说明(对照
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 启动三连)
- [7.1 `App.vue`:只有显示屏](#7.1
- [八、底层存储:栈 / 堆 / `.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 为什么模板能写脚本里的变量名?

📖 核心结论
- 同一个
.vue文件 = 同一个组件实例 <script setup>顶层的const username/const login会被 Vue 自动暴露 给<template>- 模板写
username,不是"碰巧同名",是编译时挂上的 - 跨页面(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 | 双清 |