1. 项目缘起:为什么一个登录功能值得单独成篇?
做前端开发的朋友,尤其是刚接触Vue生态的,可能觉得登录功能不就是调个接口、存个token、跳个页面的事儿吗?我最初也是这么想的,直到在一个真实的中后台项目中,因为登录模块的“小问题”导致整个项目延期上线,我才彻底改变了看法。登录,是用户进入你应用的第一道门,也是整个前端应用安全、状态和数据流的基石。它远不止一个表单那么简单,它涉及到用户状态管理、路由守卫、接口安全、错误处理、用户体验等一系列环环相扣的环节。
这个“VueDemo-11.登录功能”项目,就是我在踩过无数坑之后,沉淀下来的一套相对完整、可复用的登录解决方案。它不是简单的axios.post('/login')就完事了,而是从零开始,构建一个健壮、安全、用户体验良好的登录模块。我会带你从最基础的登录表单开始,一步步深入到Token管理、路由权限控制、用户信息持久化等核心细节,并分享那些官方文档里不会写的“血泪教训”。无论你是Vue新手,还是想优化现有登录逻辑的老手,相信都能从中找到有价值的参考。
2. 登录模块的核心架构与设计思路
在动手写代码之前,我们先要理清一个现代SPA(单页应用)登录模块到底需要哪些部分。如果把登录功能比作一个公司的前台接待系统,那么它至少需要:一个接待表单(登录页)、一个身份核验流程(接口调用)、一个临时通行证(Token)、一个记录员工信息的工牌(用户状态)、以及一套门禁规则(路由守卫)。
2.1 技术栈选型与项目初始化
本项目基于 Vue 3 + Composition API + TypeScript + Vite 构建,这是目前Vue生态中最主流、最前沿的技术组合。选择它们的原因如下:
- Vue 3 & Composition API:提供了更灵活、逻辑更聚合的代码组织方式。对于登录这种涉及多个响应式状态(如
loading,form,errorMessage)和副作用(如API调用、路由跳转)的功能,使用<script setup>和Composition API会让代码更清晰。 - TypeScript:为登录接口的请求/响应数据、用户信息对象、Token等提供明确的类型定义,能极大减少因数据类型错误导致的bug,尤其是在团队协作中。
- Vite:极快的启动和热更新速度,能提升开发体验。
首先,我们初始化项目并安装核心依赖:
# 使用Vite官方模板创建项目 npm create vue@latest vue-login-demo # 按照提示选择需要的特性:TypeScript, Vue Router, Pinia cd vue-login-demo npm install # 安装UI库(这里以Element Plus为例,可根据喜好选择Ant Design Vue, Naive UI等) npm install element-plus @element-plus/icons-vue # 安装HTTP客户端和状态管理库 npm install axios pinia2.2 目录结构规划
一个清晰的目录结构是良好架构的开始。我们的src目录会这样组织:
src/ ├── api/ # 所有接口请求封装 │ ├── modules/ # 按模块划分的接口文件,如auth.ts │ └── index.ts # 统一导出和axios实例配置 ├── stores/ # Pinia状态管理 │ └── user.ts # 用户状态存储 ├── router/ # 路由配置 │ ├── index.ts │ └── guards.ts # 路由守卫逻辑 ├── views/ # 页面组件 │ ├── Login.vue # 登录页 │ └── Home.vue # 主页(登录后) ├── components/ # 可复用组件 ├── utils/ # 工具函数,如token操作 └── types/ # TypeScript类型定义这个结构将登录相关的逻辑清晰地分散到了api(数据)、stores(状态)、router(权限)和views(视图)中,符合“关注点分离”的原则。
3. 从零构建登录页面与表单交互
登录页是用户的第一印象,我们需要一个简洁、友好且健壮的表单。
3.1 基础表单组件搭建
在views/Login.vue中,我们使用Element Plus快速搭建一个表单。这里的关键不在于UI多炫酷,而在于表单验证和用户体验。
<template> <div class="login-container"> <el-card class="login-box"> <h2>系统登录</h2> <el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" @keyup.enter="handleLogin" > <el-form-item prop="username"> <el-input v-model="loginForm.username" placeholder="请输入用户名" :prefix-icon="User" size="large" /> </el-form-item> <el-form-item prop="password"> <el-input v-model="loginForm.password" type="password" placeholder="请输入密码" :prefix-icon="Lock" size="large" show-password /> </el-form-item> <el-form-item> <el-button :loading="loading" type="primary" size="large" @click="handleLogin" class="login-btn" > 登录 </el-button> </el-form-item> </el-form> </el-card> </div> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' import { User, Lock } from '@element-plus/icons-vue' import type { FormInstance, FormRules } from 'element-plus' import { useUserStore } from '@/stores/user' import { useRouter } from 'vue-router' const router = useRouter() const userStore = useUserStore() // 表单引用与数据 const loginFormRef = ref<FormInstance>() const loginForm = reactive({ username: '', password: '' }) const loading = ref(false) // 表单验证规则 const loginRules: FormRules = { username: [ { required: true, message: '用户名不能为空', trigger: 'blur' }, { min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' } ], password: [ { required: true, message: '密码不能为空', trigger: 'blur' }, { min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' } ] } // 登录处理函数 const handleLogin = async () => { // 1. 表单预验证 if (!loginFormRef.value) return const valid = await loginFormRef.value.validate() if (!valid) return // 2. 开始加载 loading.value = true try { // 3. 调用Pinia Action进行登录 await userStore.login(loginForm) // 4. 登录成功,跳转到首页或重定向页 router.push('/') } catch (error) { // 错误处理已在userStore中统一进行,这里可以补充一些UI提示 console.error('登录失败:', error) } finally { // 5. 无论成功失败,都关闭加载状态 loading.value = false } } </script>注意:这里将核心的登录逻辑
userStore.login()抽离到了Pinia Store中,这是非常关键的一步。它保证了业务逻辑与UI组件的分离,使得登录逻辑可以被其他组件(如导航栏的退出登录)复用,也更容易进行单元测试。
3.2 表单验证的实战心得
表单验证看似简单,但细节决定体验:
- 即时验证 vs 提交时验证:我们采用了
blur(失去焦点)触发验证,这是一种平衡。纯change(输入变化)验证会过于频繁打扰用户,而只在submit时验证反馈又太慢。blur是一个不错的折中点。 - 防重复提交:通过
loading状态和按钮的:loading属性,有效防止了用户在请求未返回时连续点击导致的重复提交。 - 键盘回车支持:
@keyup.enter="handleLogin"是一个提升用户体验的细节,符合用户习惯。 - 错误信息展示:Element Plus的
FormItem会自动在对应表单项下方显示message,我们无需手动控制,保持简洁。
4. 状态管理(Pinia)与Token持久化方案
这是登录功能最核心的部分之一。我们需要一个中心化的地方来管理用户的登录状态、Token和基本信息。
4.1 创建用户状态存储
在stores/user.ts中,我们定义User Store:
// types/user.ts - 先定义类型 export interface ILoginForm { username: string password: string } export interface IUserInfo { id: number name: string avatar: string roles: string[] // 用于权限控制 } // stores/user.ts import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { loginApi, getUserInfoApi, logoutApi } from '@/api/modules/auth' import type { ILoginForm, IUserInfo } from '@/types/user' import { setToken, getToken, removeToken } from '@/utils/auth' export const useUserStore = defineStore('user', () => { // State const token = ref<string>(getToken() || '') // 从本地存储初始化 const userInfo = ref<IUserInfo | null>(null) const roles = ref<string[]>([]) // Getters const isLoggedIn = computed(() => !!token.value) const userName = computed(() => userInfo.value?.name || '') // Actions const login = async (loginForm: ILoginForm) => { try { // 调用登录接口 const { data } = await loginApi(loginForm) // 假设接口返回 { code: 200, data: { token: 'xxx' } } const accessToken = data.token // 1. 存入state token.value = accessToken // 2. 持久化到本地存储(关键!) setToken(accessToken) // 3. 获取用户详情信息 await getUserInfo() } catch (error) { // 清除可能已存储的token token.value = '' removeToken() // 向上抛出错误,由UI层处理(如显示错误提示) throw error } } const getUserInfo = async () => { try { // 携带token调用获取用户信息接口 const { data } = await getUserInfoApi() userInfo.value = data roles.value = data.roles || [] } catch (error) { // 获取用户信息失败,可能是token失效,执行登出 logout() throw error } } const logout = async () => { try { // 可选:调用服务端登出接口,让服务端使token失效 await logoutApi() } finally { // 无论服务端登出成功与否,前端都必须清理 resetToken() } } const resetToken = () => { // 清空state token.value = '' userInfo.value = null roles.value = [] // 清空本地存储 removeToken() } return { token, userInfo, roles, isLoggedIn, userName, login, getUserInfo, logout, resetToken } })4.2 Token持久化的安全考量与工具函数
Token的存储位置是一个安全与便利的权衡。常见的方案有:
- LocalStorage:最方便,但易受XSS攻击。
- SessionStorage:页面关闭即消失,体验不好。
- Cookie (HttpOnly):可防XSS,但需注意CSRF防护,且前端JS无法直接读写。
对于大多数中后台管理系统,采用LocalStorage是主流做法,因为其便利性远大于风险(后台系统通常XSS攻击面较小)。我们可以在utils/auth.ts中封装相关操作:
// utils/auth.ts const TOKEN_KEY = 'access_token' export function getToken(): string | null { return localStorage.getItem(TOKEN_KEY) } export function setToken(token: string): void { localStorage.setItem(TOKEN_KEY, token) } export function removeToken(): void { localStorage.removeItem(TOKEN_KEY) } // 可选:设置token过期时间(如果后端返回了expires_in) export function setTokenWithExpiry(token: string, expiresIn: number): void { const now = new Date() const expiryTime = now.getTime() + expiresIn * 1000 localStorage.setItem(TOKEN_KEY, token) localStorage.setItem(`${TOKEN_KEY}_expiry`, expiryTime.toString()) } export function getTokenWithExpiry(): string | null { const token = localStorage.getItem(TOKEN_KEY) const expiryTime = localStorage.getItem(`${TOKEN_KEY}_expiry`) if (!token || !expiryTime) { return null } if (Date.now() > parseInt(expiryTime)) { // Token已过期,清理 removeToken() localStorage.removeItem(`${TOKEN_KEY}_expiry`) return null } return token }重要经验:永远不要只依赖前端来判断Token是否有效。前端存储的过期时间只是一个优化手段,用于提前清除明显过期的token。真正的有效性验证,必须在每次请求用户信息等敏感接口时,由后端返回的HTTP状态码(如401)来决定。前端在收到401后,应强制跳转回登录页。
5. 路由守卫与权限控制实战
路由守卫是守护应用内页面访问权限的“门卫”。没有它,用户即使不登录,也能直接通过URL访问内部页面。
5.1 路由配置与元信息
首先,在router/index.ts中定义路由,并使用meta字段标记哪些路由需要认证。
import { createRouter, createWebHistory } from 'vue-router' import type { RouteRecordRaw } from 'vue-router' const routes: Array<RouteRecordRaw> = [ { path: '/login', name: 'Login', component: () => import('@/views/Login.vue'), meta: { requiresAuth: false } // 明确标记登录页不需要认证 }, { path: '/', name: 'Home', component: () => import('@/views/Home.vue'), meta: { requiresAuth: true } // 需要认证才能访问 }, { path: '/admin', name: 'Admin', component: () => import('@/views/Admin.vue'), meta: { requiresAuth: true, roles: ['admin'] } // 需要认证且角色为admin }, // 404页面 { path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('@/views/NotFound.vue') } ] const router = createRouter({ history: createWebHistory(), routes }) export default router5.2 实现全局前置守卫
在router/guards.ts中(或在router/index.ts中直接编写),我们实现核心的守卫逻辑:
// router/guards.ts import type { Router } from 'vue-router' import { useUserStore } from '@/stores/user' import { getToken } from '@/utils/auth' export function setupRouterGuard(router: Router) { // 全局前置守卫 router.beforeEach(async (to, from, next) => { const userStore = useUserStore() const hasToken = getToken() // 从本地存储检查token // 1. 判断目标路由是否需要认证 if (to.meta.requiresAuth === false) { // 前往无需认证的页面(如登录页),直接放行 next() return } // 2. 有Token的情况 if (hasToken) { if (to.path === '/login') { // 已登录却想去登录页,重定向到首页 next({ path: '/' }) } else { // 检查用户信息是否已获取 if (userStore.userInfo) { // 已获取用户信息,检查角色权限 checkUserRole(to, userStore.roles, next) } else { try { // 未获取用户信息,则发起请求获取 await userStore.getUserInfo() // 获取成功后,再次检查角色权限 checkUserRole(to, userStore.roles, next) } catch (error) { // 获取用户信息失败(如token失效),清除token并跳转到登录页 await userStore.resetToken() next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) } } } } else { // 3. 没有Token的情况 if (to.meta.requiresAuth) { // 想去需要认证的页面,重定向到登录页,并记录目标地址以便登录后回跳 next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) } else { // 想去无需认证的页面,放行 next() } } }) } // 辅助函数:检查用户角色是否匹配路由要求的角色 function checkUserRole(to: any, userRoles: string[], next: Function) { const requiredRoles = to.meta.roles as string[] | undefined if (requiredRoles && requiredRoles.length > 0) { // 路由要求特定角色 const hasRole = userRoles.some(role => requiredRoles.includes(role)) if (hasRole) { next() } else { // 角色不符,跳转到无权限页面或首页 next({ path: '/403' }) // 需要先定义403路由 } } else { // 路由不要求特定角色,有权限即可访问 next() } }最后,在main.ts中应用这个守卫:
// main.ts import { createApp } from 'vue' import App from './App.vue' import router from './router' import { setupRouterGuard } from './router/guards' const app = createApp(App) app.use(router) // 安装路由守卫 setupRouterGuard(router) app.mount('#app')5.3 路由守卫的常见“坑”与解决方案
- 无限重定向循环:这是最常见的坑。场景:Token过期 -> 守卫跳转
/login->/login页的created钩子又去检查Token -> 发现过期Token无效 -> 又触发某个跳转... 解决方案:在登录页组件中,避免在初始化时进行任何可能触发路由跳转的认证检查。或者,确保守卫逻辑中的条件判断互斥且周全。 - 异步获取用户信息导致的白屏:在
beforeEach中await userStore.getUserInfo()时,页面会等待请求完成才渲染。如果网络慢,用户会看到白屏。优化方案:- 方案A(推荐):在应用根组件(如
App.vue)的onMounted中主动获取一次用户信息,并存储起来。这样守卫中只需要判断userInfo是否存在,无需等待请求。 - 方案B:对于非敏感路由(如首页),可以先放行,让页面骨架屏先出来,然后在页面组件内再去获取和渲染用户相关数据。
- 方案A(推荐):在应用根组件(如
- 动态路由的权限集成:如果路由表是根据用户角色动态生成的,情况会更复杂。通常需要在登录成功后,根据角色权限,通过
router.addRoute()动态添加路由,然后再进行跳转。这要求守卫逻辑能处理“路由尚未添加”的状态。
6. 网络请求层(Axios)的精细化封装
一个健壮的登录模块离不开强大的网络请求层。我们需要统一处理Token的携带、请求拦截、响应拦截和错误处理。
6.1 创建Axios实例与请求拦截器
在api/index.ts中:
// api/index.ts import axios from 'axios' import type { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse } from 'axios' import { getToken } from '@/utils/auth' import { useUserStore } from '@/stores/user' import router from '@/router' // 创建axios实例 const service: AxiosInstance = axios.create({ baseURL: import.meta.env.VITE_APP_BASE_API, // 从环境变量读取 timeout: 10000 // 10秒超时 }) // 请求拦截器 service.interceptors.request.use( (config: InternalAxiosRequestConfig) => { // 在发送请求前做些什么 const token = getToken() if (token) { // 如果token存在,将其添加到请求头 config.headers.Authorization = `Bearer ${token}` } return config }, (error) => { // 对请求错误做些什么 return Promise.reject(error) } ) // 响应拦截器 service.interceptors.response.use( (response: AxiosResponse) => { // 对响应数据做点什么 const res = response.data // 假设后端统一返回格式为 { code: number, data: any, message: string } if (res.code === 200) { return res.data // 直接返回业务数据 } else { // 业务逻辑错误(如密码错误) // 可以在这里统一进行消息提示 console.error(`请求错误 [${response.config.url}]:`, res.message) return Promise.reject(new Error(res.message || 'Error')) } }, (error) => { // 对响应错误做点什么 (HTTP状态码非2xx) console.error('请求失败:', error.response?.status, error.message) if (error.response) { switch (error.response.status) { case 401: // Token过期或无效 const userStore = useUserStore() userStore.resetToken() // 跳转到登录页,并携带当前页面路径 router.push(`/login?redirect=${encodeURIComponent(router.currentRoute.value.fullPath)}`) break case 403: // 权限不足 router.push('/403') break case 404: // 资源不存在 router.push('/404') break case 500: // 服务器内部错误 console.error('服务器内部错误,请联系管理员') break default: console.error(`未知错误: ${error.response.status}`) } } else if (error.request) { // 请求已发出但没有收到响应(网络错误) console.error('网络错误,请检查您的网络连接') } else { // 请求配置出错 console.error('请求配置错误:', error.message) } return Promise.reject(error) } ) export default service6.2 按模块封装API请求
在api/modules/auth.ts中,我们封装登录相关的具体请求:
// api/modules/auth.ts import request from '@/api/index' import type { ILoginForm, IUserInfo } from '@/types/user' // 登录接口 export function loginApi(data: ILoginForm) { // 注意:这里返回的是Promise,响应拦截器会帮我们提取出 res.data return request.post<{ token: string }>('/auth/login', data) } // 获取用户信息接口 export function getUserInfoApi() { return request.get<IUserInfo>('/auth/userinfo') } // 登出接口 export function logoutApi() { return request.post('/auth/logout') }6.3 拦截器中的关键细节
- Token的动态获取:在请求拦截器中,我们使用
getToken()工具函数从localStorage读取,而不是直接从Pinia Store读取。这是因为Store可能在页面刷新后重置,而localStorage是持久化的,能保证在应用初始化早期(Store可能还未恢复水合)拦截器也能拿到Token。 - 错误处理的粒度:在响应拦截器中,我们将错误分为业务逻辑错误(后端返回的特定code,如1001表示密码错误)和HTTP错误(401, 403等)。业务错误通常由调用方在
catch中处理并展示给用户(如“用户名或密码错误”),而HTTP错误(如401)则在拦截器中统一处理,强制进行登出和跳转。 - 避免在拦截器中直接使用UI组件:拦截器是纯JS环境,无法直接使用
ElMessage等UI组件进行提示。一种做法是通过触发一个全局事件(如EventBus),或者在拦截器中reject错误,由具体的页面调用处来捕获并展示UI提示。上面的示例采用了后者,将错误信息reject出去。
7. 登录功能的扩展与进阶优化
一个基础的登录流程跑通后,我们还可以从用户体验和安全角度进行诸多优化。
7.1 自动登录与登录状态保持
用户关闭浏览器再打开,或者刷新页面后,如何保持登录状态?这需要一套完整的初始化逻辑。
方案:在应用入口处初始化用户状态
在App.vue或一个专门的初始化函数中:
<!-- App.vue --> <script setup lang="ts"> import { onMounted } from 'vue' import { useUserStore } from '@/stores/user' const userStore = useUserStore() onMounted(async () => { // 检查本地是否有token const token = localStorage.getItem('access_token') if (token) { try { // 有token,则尝试获取用户信息以验证token有效性 await userStore.getUserInfo() console.log('自动登录成功') } catch (error) { // token无效,清除状态 console.log('token已失效,请重新登录') userStore.resetToken() } } }) </script>7.2 添加验证码与防暴力破解
对于安全性要求更高的系统,登录时需要图形验证码或短信验证码。
前端实现要点:
- 获取验证码:在登录页加载时或点击验证码图片时,请求
/auth/captcha接口,后端返回一个图片Base64或唯一ID。 - 携带验证码:将用户输入的验证码(或验证码ID)作为参数,与用户名密码一同提交到登录接口。
- 错误处理:当验证码错误时,后端应返回明确错误码,前端需刷新验证码图片。
// 在Login.vue的data或store中增加 const captcha = reactive({ image: '', // base64图片 key: '', // 后端返回的验证码唯一标识 code: '' // 用户输入的验证码 }) // 获取验证码的函数 const getCaptcha = async () => { const { data } = await getCaptchaApi() captcha.image = `data:image/png;base64,${data.image}` captcha.key = data.key }7.3 多标签页同步登录状态
当用户在A标签页退出登录,B标签页应如何感知并同步状态?可以使用localStorage的storage事件或BroadcastChannelAPI。
使用storage事件的简单方案:
在App.vue或一个全局监听器中:
// 监听localStorage变化 window.addEventListener('storage', (event) => { if (event.key === 'access_token' && event.newValue === null) { // 检测到token被清空(其他标签页退出登录) const userStore = useUserStore() userStore.resetToken() // 跳转到登录页 router.push('/login') } })在退出登录的logout函数中,主动清除localStorage,从而触发其他标签页的监听事件。
7.4 请求重试与Token刷新(双Token方案)
对于使用Access Token + Refresh Token的无感刷新方案,前端逻辑会复杂一些。
基本流程:
- 登录成功后,后端返回
access_token(短期,如2小时)和refresh_token(长期,如7天)。 - 前端将两者都安全存储。
- 当
access_token过期,请求接口返回401时,前端不是直接跳登录页,而是尝试用refresh_token去调用刷新接口,获取新的access_token。 - 刷新成功后,用新token重试原先失败的请求,用户无感知。
- 如果
refresh_token也过期或无效,则彻底跳转登录页。
这需要在Axios响应拦截器中实现一个复杂的“请求队列”和“刷新锁”机制,以避免并发请求时多次刷新Token。由于实现较为复杂,此处不展开代码,但它是构建企业级应用的重要考量。
8. 完整的登录流程串联与调试
现在,让我们把所有的碎片串联起来,看看一个完整的登录流程是如何工作的:
- 用户访问:用户打开应用,访问首页
/。 - 路由守卫拦截:全局前置守卫
beforeEach触发。检查localStorage无Token,且目标路由meta.requiresAuth为true。守卫将用户重定向到/login?redirect=/。 - 登录页展示:登录页组件
Login.vue加载,渲染表单。 - 用户提交:用户输入信息并提交,触发
handleLogin。 - 调用Store Action:
handleLogin调用userStore.login(formData)。 - 发起网络请求:在Store的
loginaction中,通过封装好的loginApi发起POST请求。Axios请求拦截器自动将Token(此时还没有)或未来的Token加入请求头。 - 处理响应:请求成功,后端返回Token。Store的
loginaction将Token存入localStorage和Pinia state,然后调用getUserInfo获取用户详情。 - 状态更新与跳转:用户信息获取成功,更新Store。
Login.vue中的try块执行完毕,router.push('/')跳转回首页。 - 再次触发守卫:跳转到
/再次触发beforeEach。此时守卫检测到有Token,且已存在userInfo,检查角色权限(如有)后放行。 - 首页渲染:首页组件
Home.vue加载,可以通过userStore获取并显示用户信息。
调试技巧与常见问题排查:
问题:登录成功但不跳转,或跳转后用户信息消失。
- 检查1:Vue Devtools中查看Pinia Store的state,确认
token和userInfo是否已正确更新。 - 检查2:Application面板查看
localStorage,确认Token是否已持久化。 - 检查3:Network面板查看获取用户信息的请求是否成功,响应数据是否符合预期类型。
- 检查4:检查路由守卫逻辑,特别是
hasToken和userInfo的判断条件,以及next()的调用路径,确保没有逻辑漏洞导致无限循环。
- 检查1:Vue Devtools中查看Pinia Store的state,确认
问题:页面刷新后登录状态丢失。
- 检查:确保在
App.vue或Store的初始化中,有从localStorage读取Token并尝试恢复用户状态的逻辑。
- 检查:确保在
问题:接口请求报401,但Token明明存在。
- 检查1:在Axios请求拦截器中打印
config.headers.Authorization,确认Token格式正确(如Bearer xxx)。 - 检查2:检查后端Token验证逻辑和过期时间。
- 检查3:可能是跨域问题导致请求头未携带。检查后端CORS配置,确保允许
Authorization头。
- 检查1:在Axios请求拦截器中打印
构建一个完整的登录功能,就像搭建一个精密的仪表盘,每一个齿轮(表单、验证、请求、状态、路由)都必须严丝合缝。这个“VueDemo-11”项目,从最直观的登录页面开始,层层深入到状态管理、路由权限和网络封装的底层逻辑,几乎涵盖了一个生产级登录模块所需的所有核心知识。在实际开发中,你还需要根据具体的业务需求(如SSO单点登录、第三方登录、动态路由等)进行适配和扩展。希望这份详细的拆解和实战经验,能帮助你少走弯路,构建出更稳定、安全的前端应用入口。