读完这篇文章,你将彻底弄懂:JWT 是什么、Zustand 怎么用、Mock 数据如何模拟后端、路由守卫如何保护页面 ------ 以及它们之间是如何默契配合的。
1. 开胃菜:这是啥玩意儿?
想象一下,你开了一家高级会员制健身房 🏋️:
- 前台(登录页) :客人来了,报名字和手机号(输入用户名密码)。
- 会员卡(JWT Token) :验证通过后,前台给客人发一张带防伪标识的会员卡(有效期 24 小时)。
- 门禁系统(路由守卫) :每个入口都有保安,只看卡不看人,有卡就放行,没卡请去前台办卡。
- 智能储物柜(Zustand Store) :客人可以把会员卡和随身物品存进去,全健身房通用,不用每次都去前台取。
- 自动扫码器(Axios 拦截器) :客人每次使用器材时,机器自动扫描会员卡,不用客人手动掏卡。
这就是整个项目的核心故事! 而我们今天要拆解的源码,就是这套系统的完整实现。
2. 名词解释大全
2.1 JWT(JSON Web Token)
| 维度 | 内容 |
|---|---|
| 官方定义 | 一种紧凑的、URL 安全的令牌格式,用于在各方之间安全传输声明信息 |
| 大白话 | 一张自带防伪标识的电子会员卡,里面存着你的身份信息,别人改不了,因为改了防伪码就对不上 |
| 代码体现 | jwt.sign({ user: 'admin' }, 'secret819!$', { expiresIn: 86400 }) 生成 Token;jwt.verify(token, 'secret819!$') 验证 |
| 解决痛点 | 传统 Session 存在服务端内存中,分布式部署时多台服务器需要共享 Session,麻烦;JWT 无状态,任何服务器都能独立验证 |
💡 特别提醒 :JWT 的签名机制保证了数据未被篡改 ,但并不加密数据!Payload 里的内容是 Base64 编码的,任何人都能解码查看 ,所以绝对不要在 JWT 里存放密码、银行卡号等敏感信息!
2.2 Zustand
| 维度 | 内容 |
|---|---|
| 官方定义 | 一个轻量级的 React 状态管理库,基于不可变状态和订阅模式 |
| 大白话 | 一个全健身房通用的智能储物柜,任何器材区(组件)都能存取物品(状态),不用层层传递 |
| 代码体现 | export const useAuthStore = create((set) => ({ token: '', setAuth: ({ token, user }) => set({ token, user }) })) |
| 解决痛点 | 不用 Redux 那套繁琐的 Action/Reducer/Provider 模板,几行代码就能实现全局状态共享 |
2.3 Mock 服务(vite-plugin-mock)
| 维度 | 内容 |
|---|---|
| 官方定义 | 在开发环境下拦截 HTTP 请求,返回模拟数据的 Vite 插件 |
| 大白话 | 仿真健身房前台,不用等真正的后端开发完成,前端自己就能模拟所有接口 |
| 代码体现 | viteMockServe({ mockPath: 'mock', localEnabled: true }) 开启;mock/login.js 中定义 /api/login 的响应逻辑 |
| 解决痛点 | 前后端并行开发时,前端不用"干等"后端接口,自己就能跑通完整流程 |
2.4 Axios 拦截器(Interceptor)
| 维度 | 内容 |
|---|---|
| 官方定义 | 在请求发送前或响应返回后,统一处理逻辑的钩子函数 |
| 大白话 | 自动扫码器------每次请求自动把会员卡(Token)挂到请求头上,不用手动操作 |
| 代码体现 | instance.interceptors.request.use(config => { config.headers['Authorization'] =Bearer ${token}; return config }) |
| 解决痛点 | 不用在每个 API 调用处都写一遍 headers: { Authorization: token },一处配置,全局生效 |
2.5 路由守卫(RequireAuth)
| 维度 | 内容 |
|---|---|
| 官方定义 | 在路由渲染前进行条件判断,决定是否允许访问目标页面 |
| 大白话 | 健身房门禁保安,有卡(Token)才让进付费区,没卡就引导去前台(登录页) |
| 代码体现 | <Route path="/pay" element={<RequireAuth><Pay /></RequireAuth>} />;守卫内判断 if (!token) return <Navigate to="/login" /> |
| 解决痛点 | 防止未登录用户直接通过 URL 访问受保护页面,提升应用安全性 |
3. 项目文件结构总览
在深入流程之前,先看清每个文件的职责:
text
arduino
📁 项目根目录
├── vite.config.js ← Vite 配置,开启 Mock 服务
├── mock/
│ └── login.js ← Mock 登录接口(模拟后端)
├── src/
│ ├── api/
│ │ ├── config.js ← Axios 配置 + 请求/响应拦截器
│ │ ├── user.js ← 登录 API 调用
│ │ └── repo.js ← 仓库 API 调用(需要 token)
│ ├── store/
│ │ ├── user.js ← Zustand 用户状态管理(含持久化)
│ │ └── todos.js ← Zustand todos 状态管理
│ ├── components/
│ │ ├── Nav.js ← 导航栏(显示登录状态)
│ │ └── RequireAuth.js ← 路由守卫(检查 token)
│ ├── pages/
│ │ ├── Login.js ← 登录页面
│ │ ├── Home.js ← 首页
│ │ └── Pay.js ← 支付页(受保护)
│ └── App.js ← 根组件,定义路由 + 懒加载
4. 核心流程图解
4.1 完整登录流程时序图

4.2 后续请求自动携带 Token 流程

5. 完整流程详解:从第一次访问到登录成功
阶段一:启动项目(vite.config.js)
javascript
javascript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { viteMockServe } from 'vite-plugin-mock'
export default defineConfig({
plugins: [
react(),
viteMockServe({
mockPath: 'mock', // 👈 告诉 Vite:去 mock 文件夹找接口定义
localEnabled: true, // 👈 开发环境启用 Mock
})
],
})
作用 :启动 Vite 开发服务器时,自动加载 mock/ 目录下的文件,拦截所有 /api/* 请求。
阶段二:用户打开页面(App.js)
javascript
javascript
import { useAuthStore } from './store/user'
import { getRepo } from './api/repo'
function App() {
useEffect(() => {
// 一进来就调用 getRepo(),但这个请求会被拦截器处理
(async () => {
const res = await getRepo() // 这个请求需要 token
})()
}, [])
return (
<Router>
<Nav /> {/* 👈 导航栏,显示登录状态 */}
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/pay" element={
<RequireAuth> {/* 👈 路由守卫!检查有没有 token */}
<Pay />
</RequireAuth>
} />
</Routes>
</Router>
)
}
此时:
Nav组件读取useAuthStore,发现token是空字符串 → 显示"Login"链接/pay路由被RequireAuth包裹 → 检查 token,没有 → 重定向到/login
阶段三:路由守卫拦截(RequireAuth.js)
javascript
javascript
import { Navigate } from "react-router-dom"
import { useAuthStore } from '../store/user'
function RequireAuth({ children }) {
// 从 store 读取 token(优先用内存的,因为 localStorage 变化不会触发重新渲染)
const token = useAuthStore(state => state.token)
if (!token) {
// 没有 token → 重定向到登录页
return <Navigate to="/login" replace />
}
// 有 token → 渲染子组件
return children
}
export default RequireAuth
阶段四:用户看到登录页(Login.js)
javascript
javascript
import { useAuthStore } from '../store/user'
import { login } from '../api/user'
function Login() {
const navigate = useNavigate()
const setAuth = useAuthStore(state => state.setAuth) // 👈 获取"设置认证"方法
const [formData, setFormData] = useState({ username: '', password: '' })
const [errors, setErrors] = useState({ username: '', password: '' })
const [isValid, setIsValid] = useState(false)
// 表单验证逻辑
useEffect(() => {
const newErrors = { username: '', password: '' }
if (!formData.username.trim()) {
newErrors.username = '用户名不能为空'
} else if (formData.username.length < 3) {
newErrors.username = '用户名至少3位'
}
if (!formData.password.trim()) {
newErrors.password = '密码不能为空'
} else if (formData.password.length < 6) {
newErrors.password = '密码至少6位'
}
setErrors(newErrors)
setIsValid(!newErrors.username && !newErrors.password)
}, [formData])
const handleLogin = async (e) => {
e.preventDefault()
try {
const res = await login(formData) // 👈 调用 API
// login() 里面是:axios.post('/login', data)
if (res.code === 0) {
setAuth({ token: res.token, user: res.user }) // 👈 保存到 store
navigate('/') // 👈 跳转到首页
} else {
alert(res.message || '登录失败')
}
} catch (err) {
alert('登录失败')
}
}
return (
<div>
<h2>登录</h2>
<form onSubmit={handleLogin}>
{/* 用户名输入框 */}
{/* 密码输入框 */}
<button type="submit" disabled={!isValid}>登录</button>
</form>
</div>
)
}
阶段五:登录请求发出(api/config.js + api/user.js)
这是回答你核心疑问的关键部分!
javascript
javascript
// api/config.js - Axios 配置
import axios from 'axios'
const instance = axios.create({
baseURL: '/api', // 👈 所有请求自动加 /api 前缀
timeout: 5000, // 5 秒超时
})
// 📌 请求拦截器:每次请求发送前执行
instance.interceptors.request.use(config => {
// config 是 Axios 的请求配置对象
// 它包含了:url、method、headers、data、params 等所有请求信息
const token = localStorage.getItem('token') // 👈 从 localStorage 读 token
if (token) {
// 如果有 token,加到请求头
// 'Bearer ' 是标准前缀,表示"持有者令牌"
config.headers['Authorization'] = `Bearer ${token}`
}
// ⚠️ 如果没有 token,就不加(登录请求就是这种情况!)
return config // 👈 必须返回,否则请求发不出去
})
// 📌 响应拦截器:每次收到响应后执行
instance.interceptors.response.use(
res => {
return res.data // 👈 只返回 data,屏蔽 axios 的元数据
},
err => {
return Promise.reject(err)
}
)
export default instance
javascript
javascript
// api/user.js - 登录 API
import axios from './config'
export const login = async (data) => {
const res = await axios.post('/login', data)
// 实际请求:POST /api/login
// 因为 config.js 中 baseURL 是 '/api'
return res
}
这里重点回答你的问题:
config 是什么?
config 是 Axios 内部创建的请求配置对象,包含:
javascript
css
{
url: '/login',
method: 'post',
headers: { 'Content-Type': 'application/json' },
data: { username: 'admin', password: '123456' },
baseURL: '/api',
timeout: 5000,
// ... 更多配置
}
拦截器就是修改这个对象 ,比如往 headers 里塞 token。
第一次访问没有 token 怎么办?
登录请求不需要 token! 流程如下:
text
javascript
用户点击登录
↓
axios.post('/login', { username, password })
↓
请求拦截器执行:localStorage 里没有 token → 不加 Authorization 头
↓
请求正常发送到服务器(Mock)
↓
服务器验证账号密码 → 生成 token → 返回给前端
↓
前端保存 token 到 localStorage
↓
后续请求自动带上 token
阶段六:Mock 服务器处理登录(mock/login.js)
javascript
javascript
import jwt from 'jsonwebtoken'
export default [
{
url: '/api/login', // 👈 匹配 axios 的 /api/login
method: 'post',
timeout: 2000,
response: (req) => {
const body = req.body
// 验证账号密码
if (body.username !== 'admin' || body.password !== '123456') {
return { code: -1, message: 'username or password 错误' }
}
// 🔑 生成 JWT Token
const token = jwt.sign(
{
user: body.username, // 用户信息
role: 'admin' // 角色权限
},
'secret819!$', // 密钥(签名用)
{ expiresIn: 86400 } // 24 小时过期
)
// 👈 返回 token 给前端
return {
code: 0,
user: { username: body.username },
token: token
}
}
}
]
阶段七:前端保存 Token(store/user.js)
javascript
javascript
import { create } from 'zustand'
export const useAuthStore = create((set) => ({
// 📦 状态定义(初始值从 localStorage 恢复)
token: localStorage.getItem('token') || '',
user: JSON.parse(localStorage.getItem('user')) || null,
// 🎯 Action:登录
setAuth: ({ token, user }) => {
// ① 持久化到 localStorage(刷新页面不丢失)
localStorage.setItem('token', token)
localStorage.setItem('user', JSON.stringify(user))
// ② 更新内存状态(触发 UI 重新渲染)
set({ token, user })
},
// 🚪 Action:退出
logout: () => {
localStorage.removeItem('token')
localStorage.removeItem('user')
set({ token: '', user: null })
}
}))
登录成功后发生了什么:
javascript
php
setAuth({ token: 'eyJhbGciOiJIUzI1NiIs...', user: { username: 'admin' } })
↓
localStorage 存了一份(刷新页面还在)
↓
内存状态更新(所有订阅的组件立即重新渲染)
↓
Nav 组件看到 token 变了 → 显示"Hello, admin!"和"Logout"按钮
↓
navigate('/') 跳转到首页
阶段八:后续请求自动带 Token
javascript
javascript
// api/config.js - 请求拦截器再次上场
instance.interceptors.request.use(config => {
// 这次 localStorage 里有 token 了!
const token = localStorage.getItem('token')
if (token) {
// ✅ 自动加上 Authorization 头
config.headers['Authorization'] = `Bearer ${token}`
}
return config
})
// 组件中调用需要认证的 API
const res = await getRepo() // 调用 /api/repo
// 实际发出的请求:
// GET /api/repo
// Headers: { Authorization: 'Bearer eyJhbGciOiJIUzI1NiIs...' }
阶段九:服务器验证 Token(mock/repo.js)
javascript
javascript
import jwt from 'jsonwebtoken'
export default [
{
url: '/api/repo',
method: 'get',
response: (req) => {
// 从请求头获取 token
const authHeader = req.headers.authorization
const token = authHeader?.split(' ')[1] // 去掉 "Bearer " 前缀
if (!token) {
return { code: 401, message: '未提供 Token' }
}
try {
// ✅ 验证 token(签名 + 过期时间)
const decoded = jwt.verify(token, 'secret819!$')
// 验证通过,返回数据
return {
code: 0,
data: [
{ id: 1, name: 'react-demo' },
{ id: 2, name: 'vue-demo' }
]
}
} catch (error) {
if (error.name === 'TokenExpiredError') {
return { code: 401, message: 'Token 已过期' }
}
return { code: 401, message: 'Token 无效' }
}
}
}
]
阶段十:导航栏显示登录状态(Nav.js)
javascript
javascript
import { Link } from 'react-router-dom'
import { useAuthStore } from '../store/user'
function Nav() {
const token = useAuthStore(state => state.token)
const user = useAuthStore(state => state.user)
const logout = useAuthStore(state => state.logout)
const handleLogout = () => {
logout() // 清除 localStorage + 内存状态
}
return (
<nav>
<Link to="/">Home</Link>
<Link to="/pay">Pay</Link>
{!token && <Link to="/login">Login</Link>}
{user && <a href="#">Hello, {user.username}!</a>}
{token && <button onClick={handleLogout}>Logout</button>}
</nav>
)
}
export default Nav
6. 重难点深度剖析
6.1 难点一:JWT 的签名与验证机制
为什么要有这个设计?
HTTP 是无状态的,服务器每次收到请求都不知道是谁发的。传统方案是 Session:服务器存一份用户信息,给客户端发个 SessionId。但问题是------分布式部署时,用户第一次请求落在服务器 A,第二次可能落在服务器 B,B 不认识这个 SessionId,就要求重新登录。
JWT 的解法是:把用户信息直接编码进 Token 里,用签名保证不可篡改 。任何服务器拿到 Token,只要用同一个密钥验证签名通过,就能信任里面的数据------完全无状态。
核心代码逐行注释
javascript
javascript
import jwt from 'jsonwebtoken'
// 登录接口的 Mock 响应
response: (req) => {
const body = req.body
// 验证账号密码(硬编码测试数据)
if (body.username !== 'admin' || body.password !== '123456') {
return { code: -1, message: 'username or password 错误' }
}
// 🔑 生成 JWT Token
const token = jwt.sign(
// ① Payload:要存储的用户数据(明文,仅 Base64 编码)
{
user: body.username, // 用户名
role: 'admin' // 角色权限
},
// ② 密钥:用于签名,相当于"防伪印章"
'secret819!$',
// ③ 选项:过期时间
{
expiresIn: 86400 // 24 小时(单位:秒)
}
)
return {
code: 0,
user: { username: body.username },
token: token // 返回给前端
}
}
验证过程(在受保护接口中)
javascript
kotlin
// 后端验证 Token
try {
// jwt.verify 做了三件事:
// 1. 拆分 Token 为 Header + Payload + Signature
// 2. 用密钥重新计算 Signature
// 3. 比对是否一致 + 检查 exp 是否过期
const decoded = jwt.verify(token, 'secret819!$')
// 验证通过,decoded 就是 { user: 'admin', role: 'admin', iat: 123, exp: 456 }
} catch (error) {
// 验证失败:可能是签名不匹配(被篡改)或已过期
return { code: 401, message: 'Token 无效' }
}
💡 这里你可能会有疑问 :
jwt.sign和jwt.verify用的密钥是同一个字符串'secret819!$',这就是对称加密的特点------加密和解密用同一个密钥。所以这个密钥绝对不能泄露,否则任何人都能伪造 Token!
6.2 难点二:Zustand 的"闭包陷阱"与持久化
为什么要有这个设计?
React 的状态管理核心难题是:如何在组件树中共享可变数据 。传统方案是 Props 逐层传递(太麻烦)或 Context(性能问题)。Zustand 用发布-订阅模式实现了:任何组件都能订阅 Store 的某一部分,只有该部分变化时才重新渲染。
核心代码逐行注释
javascript
javascript
import { create } from 'zustand'
export const useAuthStore = create((set) => ({
// 📦 状态定义(初始值)
token: localStorage.getItem('token') || '', // 从 localStorage 恢复
user: JSON.parse(localStorage.getItem('user')) || null, // 解析 JSON
// 🎯 Action:登录
setAuth: ({ token, user }) => {
// 先持久化到 localStorage(刷新页面不丢失)
localStorage.setItem('token', token)
localStorage.setItem('user', JSON.stringify(user))
// 再更新内存状态(触发 UI 更新)
set({ token, user })
},
// 🚪 Action:退出
logout: () => {
// 清除 localStorage
localStorage.removeItem('token')
localStorage.removeItem('user')
// 重置内存状态
set({ token: '', user: null })
}
}))
为什么需要同时存 localStorage 和内存?
javascript
scss
// ❌ 只存 localStorage
localStorage.setItem('token', token)
// 问题:组件不会自动重新渲染,因为 localStorage 变化不会触发 React 更新
// ❌ 只存内存
set({ token })
// 问题:刷新页面后数据丢失,用户需要重新登录
// ✅ 两者都存
localStorage.setItem('token', token) // 持久化
set({ token }) // 触发 UI 更新
⚠️ 注意 :
set是 Zustand 提供的状态更新函数 ,它内部实现了浅比较------如果新值和旧值相同,不会触发重新渲染。这是性能优化的关键。
6.3 难点三:Axios 拦截器的"洋葱模型"
为什么要有这个设计?
每个请求都要手动加 Authorization 头?每个响应都要手动解 res.data?太累了!拦截器就像流水线上的自动化工位,在请求"出厂"前统一包装,在响应"入库"时统一拆封。
核心代码逐行注释
javascript
javascript
import axios from 'axios'
const instance = axios.create({
baseURL: '/api', // 所有请求自动加前缀
timeout: 5000, // 5 秒超时
})
// 🔧 请求拦截器(请求发出前执行)
instance.interceptors.request.use(config => {
// config 是请求配置对象
// 包含:url、method、headers、data、params 等
// 在这里我们可以修改 config 的任何属性
const token = localStorage.getItem('token')
if (token) {
// 将 Token 放入请求头
// 'Bearer ' 是标准前缀,表示"持有者令牌"
config.headers['Authorization'] = `Bearer ${token}`
}
// 必须返回 config,否则请求发不出去
return config
})
// 🔧 响应拦截器(收到响应后执行)
instance.interceptors.response.use(
res => {
// 只返回数据部分,屏蔽 axios 的元数据
// res 包含:data、status、headers、config、request 等
return res.data
},
err => {
// 统一错误处理
return Promise.reject(err)
}
)
export default instance
拦截器的执行顺序("洋葱模型")
text
kotlin
请求 → 请求拦截器(前置处理)→ 发送请求 → 响应拦截器(后置处理)→ 组件
↑ ↓
统一加 Token 统一解 res.data
💡 这里你可能会有疑问 :为什么响应拦截器返回的是
res.data而不是res?因为res里还包含了config、headers、status等元数据,业务层只关心data。统一处理后,组件里拿到的就是干净的{ code, data, message }。
6.4 难点四:首次访问无 Token 的完整处理链路
这是你最关心的问题,我们用一个完整的时序图来展示:

7. 避坑指南:新手的十面埋伏
7.1 坑点一:localStorage 的同步读取时机
❌ 错误示范
javascript
javascript
// 在组件中直接使用 localStorage
function UserInfo() {
const token = localStorage.getItem('token') // ❌ 组件不会自动更新
return <div>{token ? '已登录' : '未登录'}</div>
}
✅ 正确姿势
javascript
javascript
// 使用 Zustand Store,因为 set 会触发重新渲染
function UserInfo() {
const token = useAuthStore(state => state.token) // ✅ 响应式
return <div>{token ? '已登录' : '未登录'}</div>
}
💥 后果
直接读 localStorage 不会触发 React 重新渲染,用户登录后 UI 不会更新,造成"登录了但界面还显示未登录"的诡异 bug。
7.2 坑点二:JWT 过期后未处理 401
❌ 错误示范
javascript
csharp
// 没有处理 401 状态码
const res = await getRepo() // ❌ Token 过期时直接报错,用户卡死
✅ 正确姿势
javascript
ini
// 在响应拦截器中统一处理 401
instance.interceptors.response.use(
res => res.data,
err => {
if (err.response?.status === 401) {
// Token 过期,清除状态并跳转登录
useAuthStore.getState().logout()
window.location.href = '/login'
}
return Promise.reject(err)
}
)
💥 后果
Token 过期后,用户还在操作页面,所有请求都失败,但界面没有任何提示------用户以为 App 坏了,体验极差。
7.3 坑点三:Mock 和真实接口混用导致混淆
❌ 错误示范
javascript
php
// vite.config.js
viteMockServe({
mockPath: 'mock',
localEnabled: true, // ❌ 一直开着,联调时也走 Mock
})
✅ 正确姿势
javascript
arduino
// 通过环境变量控制
viteMockServe({
mockPath: 'mock',
localEnabled: process.env.NODE_ENV === 'development', // ✅ 仅开发环境
prodEnabled: false, // 生产环境绝对关闭
})
💥 后果
联调时忘记关 Mock,前端请求被 Mock 拦截,永远调不到真实后端------查了半天才发现是 Mock 在"捣乱"。
7.4 坑点四:JWT 中存放敏感信息
❌ 错误示范
javascript
php
// 把密码、银行卡号等敏感信息放进 JWT
const token = jwt.sign(
{
user: 'admin',
password: '123456', // ❌ 绝对不能放!
creditCard: '1234-5678' // ❌ 绝对不能放!
},
'secret819!$'
)
✅ 正确姿势
javascript
arduino
// 只放必要的身份标识信息
const token = jwt.sign(
{
userId: '123', // ✅ 用户 ID
username: 'admin', // ✅ 用户名
role: 'admin', // ✅ 权限角色
// 不要放密码、银行卡、身份证号等敏感信息
},
'secret819!$'
)
💥 后果
JWT 的 Payload 只是 Base64 编码,任何人都能解码查看!如果放了敏感信息,等于把密码公开了。
8. 面试官问什么?
面试题 1:说说 JWT 和 Session 的区别,为什么选 JWT?
回答大纲:
- 存储位置不同:Session 存在服务端内存/Redis,JWT 存在客户端。
- 状态特性不同:Session 是有状态的,JWT 是无状态的。
- 分布式支持:Session 需要共享存储(如 Redis),JWT 任意服务器都能独立验证。
- 安全性差异:Session 可以主动失效(管理员踢人),JWT 只能等过期。
- 选型建议:分布式系统、微服务架构适合 JWT;单体应用、需要主动控制会话的场景适合 Session。
面试题 2:Zustand 和 Redux 有什么异同?什么时候用 Zustand?
回答大纲:
-
相同点:都是单向数据流,都基于不可变状态,都能跨组件共享状态。
-
不同点:
- Redux 需要 Action、Reducer、Provider 等模板代码;Zustand 只需
create。 - Redux 依赖 Context 传递,Zustand 用发布-订阅,性能更好。
- Redux 有完善的中间件生态(如 Redux-Thunk、Saga),Zustand 生态较轻。
- Redux 需要 Action、Reducer、Provider 等模板代码;Zustand 只需
-
选型建议:中小型项目、简单状态共享 → Zustand;大型项目、复杂副作用管理 → Redux。
面试题 3:Axios 拦截器是如何实现的?为什么能拦截请求?
回答大纲:
- 核心机制:Axios 在发送请求前和接收响应后,会遍历拦截器数组依次执行。
- 实现原理 :Axios 内部维护了
interceptors.request和interceptors.response两个数组,每个拦截器都是一个对象{ resolved, rejected }。 - 执行顺序 :请求拦截器后添加的先执行 (类似栈),响应拦截器先添加的先执行(类似队列)。
- 关键点 :每个拦截器都必须返回
config或response,否则链式调用会中断。
面试题 4:刷新页面后 JWT Token 还在吗?如何保持登录状态?
回答大纲:
- JWT Token 存在哪里 :存在
localStorage或sessionStorage中。 - 刷新页面时 :页面重新加载,React 状态重置,但
localStorage数据还在。 - 恢复状态 :Zustand Store 初始化时从
localStorage读取 token,恢复登录状态。 - 验证 :用户操作时,请求拦截器自动从
localStorage取 token 加到请求头;服务器验证 token 是否有效。
9. 总结
从健身房比喻到代码逐行拆解,我们完整走完了这套 React + JWT + Zustand + Mock 登录鉴权系统的每个角落:
| 核心组件 | 职责 | 关键代码行 |
|---|---|---|
| JWT | 无状态身份认证 | jwt.sign() / jwt.verify() |
| Zustand | 全局状态管理 + 持久化 | create((set) => ({ ... })) |
| Mock 服务 | 模拟后端接口 | viteMockServe({ mockPath: 'mock' }) |
| Axios 拦截器 | 统一加 Token / 解响应 | interceptors.request.use() |
| 路由守卫 | 保护受控页面 | RequireAuth 组件 + Navigate |
完整请求链路回顾
text
markdown
1. 用户第一次访问 → 无 token → 路由守卫拦截 → 跳转登录页
2. 输入账号密码 → 登录请求(无 token)→ Mock 验证 → 返回 token
3. setAuth() → localStorage 存 token + 内存更新 → UI 刷新
4. 后续请求 → 拦截器自动加 token → 服务器验证 → 返回数据
5. 刷新页面 → localStorage 恢复 token → 保持登录状态
6. Token 过期 → 服务器返回 401 → 清除状态 → 跳转登录
这套模式已成为现代前端开发的标准范式,理解它,你就掌握了 React 全家桶生态中最重要的协作流程。🎉