07 Axios 与登录状态:网页真正调用登录接口
07 Axios 与登录状态:网页真正调用登录接口
本章你会做出什么
登录按钮会向后端发送请求,成功后保存 Token 和用户信息并进入后台。页面刷新时,会携带已有 Token 请求 profile 以恢复身份;Token 失效后自动回到登录页。
先理解三个概念
1. Axios 请求实例
每个页面都手写完整请求会重复处理地址、Token 和错误。Axios 实例是统一入口,页面只调用 authApi.login()。
2. Pinia Store
Store 用于保存跨页面共享状态。本项目只把 Token 与当前登录用户放进认证 Store;某个列表的临时数据留在页面内即可。
3. 本地存储与刷新恢复
JavaScript 内存会随刷新清空。将 Token 存在 localStorage 后,刷新时可先带 Token 请求后端获得最新用户权限。注意:这是作品级简化方案,生产系统可以改为更安全的短 Token 与 HttpOnly 刷新 Cookie。
本章最终目录变化
client/src/
api/
modules.ts
request.ts
stores/
auth.ts
views/LoginView.vue # 接入提交事件
一步一步操作
第 1 步:定义统一响应
每个 API 都返回 { code, message, data, requestId }。请求层把 data 提取出来,页面拿到的是具体用户或工单。
第 2 步:封装请求拦截器
发请求前读取 Token 放入 Authorization。收到 401 时移除无效 Token;业务错误统一显示 Element Plus 提示。
第 3 步:建立认证 Store
Store 提供 login、restore、logout 与 hasPermission,路由守卫下一章会使用它们。
第 4 步:让登录按钮调用 Store
为登录页增加 submit 方法,成功后跳转 /dashboard。
关键文件完整代码
client/src/api/request.ts
import axios from 'axios'
import { ElMessage } from 'element-plus'
import type { ApiResponse } from '../types'
export const TOKEN_KEY = 'servicedesk_access_token'
const request = axios.create({
baseURL: '/api/v1',
timeout: 10000
})
request.interceptors.request.use((config) => {
const token = localStorage.getItem(TOKEN_KEY)
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
request.interceptors.response.use(
(response) => response,
(error) => {
const message = error.response?.data?.message ?? '请求失败,请稍后重试'
if (error.response?.status === 401) localStorage.removeItem(TOKEN_KEY)
ElMessage.error(message)
return Promise.reject(error)
}
)
export const api = {
get: async <T>(url: string, config?: object) => (await request.get<ApiResponse<T>>(url, config)).data.data,
post: async <T>(url: string, body?: object) => (await request.post<ApiResponse<T>>(url, body)).data.data,
patch: async <T>(url: string, body?: object) => (await request.patch<ApiResponse<T>>(url, body)).data.data,
put: async <T>(url: string, body?: object) => (await request.put<ApiResponse<T>>(url, body)).data.data
}
client/src/api/modules.ts(认证阶段版本)
import type { AuthUser, Category } from '../types'
import { api } from './request'
export const authApi = {
login: (input: { username: string; password: string }) =>
api.post<{ accessToken: string; user: AuthUser }>('/auth/login', input),
profile: () => api.get<AuthUser>('/auth/profile')
}
export const commonApi = {
categories: () => api.get<Category[]>('/categories')
}
client/src/stores/auth.ts
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { authApi } from '../api/modules'
import { TOKEN_KEY } from '../api/request'
import type { AuthUser } from '../types'
export const useAuthStore = defineStore('auth', () => {
const user = ref<AuthUser | null>(null)
const ready = ref(false)
const isAuthenticated = computed(() => Boolean(user.value))
const login = async (input: { username: string; password: string }) => {
const result = await authApi.login(input)
localStorage.setItem(TOKEN_KEY, result.accessToken)
user.value = result.user
ready.value = true
}
const restore = async () => {
const token = localStorage.getItem(TOKEN_KEY)
if (!token) {
ready.value = true
return
}
try {
user.value = await authApi.profile()
} catch {
localStorage.removeItem(TOKEN_KEY)
user.value = null
} finally {
ready.value = true
}
}
const logout = () => {
localStorage.removeItem(TOKEN_KEY)
user.value = null
}
const hasPermission = (required?: string | string[]) => {
if (!required) return true
const codes = Array.isArray(required) ? required : [required]
return codes.some((code) => user.value?.permissionCodes.includes(code))
}
return { user, ready, isAuthenticated, login, restore, logout, hasPermission }
})
修改 LoginView.vue 的脚本和按钮
将脚本替换为:
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '../stores/auth'
const router = useRouter()
const auth = useAuthStore()
const loading = ref(false)
const form = reactive({ username: 'supervisor', password: '123456' })
const submit = async () => {
loading.value = true
try {
await auth.login(form)
await router.replace('/dashboard')
} finally {
loading.value = false
}
}
</script>
将登录按钮修改为:
<el-button type="primary" :loading="loading" class="submit" @click="submit">登录系统</el-button>
启动并验证
- 保持后端与前端运行。
- 访问
http://localhost:5173/login。 - 使用
supervisor / 123456登录,应进入/dashboard。 - 打开浏览器开发者工具的 Application/Local Storage,应看到
servicedesk_access_token。 - 刷新页面。当前章页面不会自动触发
restore,第 8 章在路由守卫中接入它;此时先确认 Store 能完成登录。
常见报错与原因
| 现象 | 原因 | 处理 |
|---|---|---|
| 登录请求显示 404 | Vite 代理或后端路径不一致 | 检查 /api/v1/auth/login 和 vite.config.ts |
| 登录总提示 401 | 内存账号没初始化或密码输错 | 检查第 5 章的 initialize() |
ElMessage 没有样式 |
Element Plus 样式没有全局导入 | 检查第 6 章 main.ts |
本章完成清单
- 登录页可以调用后端。
- 登录成功后本地能看到 Token。
- Store 中能获取当前用户与权限码。
- 我知道刷新恢复会在路由守卫中完成。
面试时这一章能怎么讲
我封装 Axios 请求实例自动携带 Token,并统一处理认证失效和业务错误;Pinia 只维护跨页面需要的登录用户与权限信息,页面局部数据不会无意义地进入全局状态。