第八章:Vue3 异步请求与状态管理进阶(从基础到工程化)
一、入门:异步请求核心工具与基础使用
在 Vue3 项目中,异步请求(如调用后端接口获取数据)是核心需求,Axios 是最常用的 HTTP 客户端,它支持请求拦截、响应拦截、取消请求等功能,比原生 fetch
更易用、更强大。
1. Axios 环境搭建与基础请求
-
第一步:安装 Axios
在项目根目录执行命令:npm install axios
-
第二步:基础请求示例(GET/POST)
在组件中直接使用 Axios 发送请求,适用于简单场景:<template> <div> <button @click="fetchUserInfo">获取用户信息</button> <p v-if="userInfo.name">用户名:{{ userInfo.name }}</p> <p v-if="errorMsg">{{ errorMsg }}</p> </div> </template> <script setup> import axios from 'axios' import { ref } from 'vue' const userInfo = ref({}) // 存储请求结果 const errorMsg = ref('') // 存储错误信息 const isLoading = ref(false) // 控制加载状态 // GET 请求:获取用户信息(带参数) const fetchUserInfo = async () => { isLoading.value = true try { // 第一个参数:请求URL,第二个参数:URL参数(会拼接为 ?userId=123) const response = await axios.get('https://api.example.com/user', { params: { userId: 123 } }) // 请求成功:Axios 会自动解析 JSON 响应,结果在 response.data 中 userInfo.value = response.data.data } catch (err) { // 请求失败:捕获网络错误或后端返回的错误 errorMsg.value = err.response?.data?.msg || '请求失败,请重试' } finally { // 无论成功/失败,都关闭加载状态 isLoading.value = false } } // POST 请求:提交表单数据(需传 JSON 格式) const submitForm = async (formData) => { try { // 第一个参数:请求URL,第二个参数:请求体(JSON格式) const response = await axios.post('https://api.example.com/login', { username: formData.username, password: formData.password }) console.log('登录成功,token:', response.data.token) } catch (err) { console.error('登录失败:', err) } } </script>
二、精通:Axios 工程化封装与 Pinia 状态管理进阶
在实际项目中,直接在组件中使用 Axios 会导致代码冗余(如重复处理错误、重复设置请求头),且状态管理会混乱。需通过“Axios 封装”和“Pinia 结合请求”实现工程化开发。
1. Axios 封装:统一处理请求/响应(核心步骤)
创建 src/utils/request.js
文件,对 Axios 进行全局配置,实现“统一请求头、统一错误处理、请求拦截、响应拦截”:
// src/utils/request.js
import axios from 'axios'
import { useUserStore } from '@/store/userStore' // 引入用户Store(获取token)
import { ElMessage } from 'element-plus' // 假设使用 Element Plus 提示组件(可选)
// 1. 创建 Axios 实例,配置基础URL和超时时间
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, // 从环境变量获取基础URL(不同环境可配置)
timeout: 5000, // 请求超时时间(5秒)
headers: {
'Content-Type': 'application/json' // 默认请求头(JSON格式)
}
})
// 2. 请求拦截器:发送请求前执行(如添加token)
request.interceptors.request.use(
(config) => {
const userStore = useUserStore()
// 如果用户已登录,在请求头中添加 token(后端需要token验证身份)
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}`
}
return config
},
(error) => {
// 请求发送失败(如网络错误)
return Promise.reject(error)
}
)
// 3. 响应拦截器:接收响应后执行(统一处理错误)
request.interceptors.response.use(
(response) => {
// 假设后端返回格式为:{ code: 200, data: {}, msg: '' }
const res = response.data
// 业务错误:如 code 不为 200(后端定义的错误码)
if (res.code !== 200) {
// 提示错误信息(如用 Element Plus 的 ElMessage)
ElMessage.error(res.msg || '操作失败')
// 返回失败的 Promise,让组件能捕获错误
return Promise.reject(new Error(res.msg || 'Error'))
} else {
// 成功:直接返回响应数据中的 data(简化组件中获取数据的步骤)
return res.data
}
},
(error) => {
// HTTP 错误:如 401(未授权)、404(接口不存在)、500(服务器错误)
const status = error.response?.status
const userStore = useUserStore()
// 401 未授权:可能是 token 过期,跳转登录页并清除状态
if (status === 401) {
ElMessage.error('登录已过期,请重新登录')
userStore.logout() // 调用 Pinia 的 logout 方法,清空 token 和用户信息
// 跳转登录页,记录当前路径(登录后可跳回)
window.location.href = `/login?redirect=${window.location.pathname}`
}
// 其他 HTTP 错误:提示错误信息
else {
ElMessage.error(error.response?.data?.msg || '网络错误,请重试')
}
return Promise.reject(error)
}
)
// 4. 导出封装后的 Axios 实例,供其他文件使用
export default request
2. 接口模块化管理:按业务分类(避免混乱)
将不同业务的接口(如用户、商品、订单)按模块拆分,放在 src/api
目录下,便于维护:
// src/api/user.js(用户相关接口)
import request from '@/utils/request'
// 登录接口(POST)
export const login = (data) => {
return request({
url: '/user/login',
method: 'post',
data // 登录参数:{ username, password }
})
}
// 获取用户信息接口(GET)
export const getUserInfo = () => {
return request({
url: '/user/info',
method: 'get'
})
}
// 修改用户信息接口(PUT)
export const updateUserInfo = (data) => {
return request({
url: '/user/info',
method: 'put',
data // 修改参数:{ name, avatar, phone } 等
})
}
// src/api/goods.js(商品相关接口)
import request from '@/utils/request'
// 获取商品列表(带分页参数)
export const getGoodsList = (params) => {
return request({
url: '/goods/list',
method: 'get',
params // 分页参数:{ page: 1, size: 10, category: 'phone' }
})
}
// 获取商品详情
export const getGoodsDetail = (id) => {
return request({
url: `/goods/${id}`, // 动态URL(拼接商品ID)
method: 'get'
})
}
3. Pinia 结合接口请求:状态与请求解耦
在 Pinia Store 中调用接口,将请求结果存储到状态中,组件只需从 Store 获取状态,无需关心请求逻辑,实现“数据与视图分离”:
// src/store/userStore.js(优化版:结合接口请求)
import { defineStore } from 'pinia'
import { login, getUserInfo, updateUserInfo } from '@/api/user' // 引入用户接口
import { ElMessage } from 'element-plus'
export const useUserStore = defineStore('user', {
state: () => ({
username: '',
token: localStorage.getItem('token') || '', // 从localStorage读取token(持久化)
avatar: '',
isLogin: !!localStorage.getItem('token'), // 根据token判断是否登录
isLoading: false // 控制请求加载状态
}),
actions: {
// 登录:调用接口,更新状态
async loginAction(formData) {
this.isLoading = true
try {
// 调用 login 接口(已封装,直接获取 data)
const data = await login(formData)
// 更新状态
this.token = data.token
this.username = data.username
this.avatar = data.avatar
this.isLogin = true
// 持久化存储 token(避免页面刷新后丢失)
localStorage.setItem('token', data.token)
ElMessage.success('登录成功')
return true // 登录成功,返回true
} catch (err) {
// 错误已在 axios 拦截器处理,此处无需额外提示
return false // 登录失败,返回false
} finally {
this.isLoading = false
}
},
// 获取用户信息:登录后调用,更新用户详情
async getUserInfoAction() {
if (!this.token) return // 无token,不请求
try {
const data = await getUserInfo()
this.username = data.username
this.avatar = data.avatar
} catch (err) {
// 若获取失败(如token过期),执行退出登录
this.logoutAction()
}
},
// 退出登录:清空状态,删除token
logoutAction() {
this.username = ''
this.token = ''
this.avatar = ''
this.isLogin = false
localStorage.removeItem('token') // 删除持久化的token
ElMessage.info('已退出登录')
},
// 修改用户信息:调用接口,更新状态
async updateUserInfoAction(data) {
this.isLoading = true
try {
await updateUserInfo(data)
// 更新本地状态(与接口返回一致)
this.username = data.username
this.avatar = data.avatar
ElMessage.success('修改成功')
return true
} catch (err) {
return false
} finally {
this.isLoading = false
}
}
}
})
4. 组件中使用:简洁调用,专注视图渲染
组件无需关心请求逻辑,只需调用 Pinia 的 actions 并获取 state,代码更简洁:
<!-- Login.vue(登录组件) -->
<template>
<div class="login-form">
<el-input
v-model="form.username"
placeholder="请输入用户名"
class="mb-4"
/>
<el-input
v-model="form.password"
type="password"
placeholder="请输入密码"
class="mb-4"
/>
<el-button
type="primary"
@click="handleLogin"
:loading="userStore.isLoading"
style="width: 100%"
>
登录
</el-button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useUserStore } from '@/store/userStore'
import { useRouter, useRoute } from 'vue-router' // 路由跳转
const userStore = useUserStore()
const router = useRouter()
const route = useRoute()
const form = ref({ username: '', password: '' }) // 表单数据
// 处理登录
const handleLogin = async () => {
// 表单验证(简化)
if (!form.value.username || !form.value.password) {
ElMessage.warning('请输入用户名和密码')
return
}
// 调用 Pinia 的 loginAction
const success = await userStore.loginAction(form.value)
if (success) {
// 登录成功,跳转回原页面(路由query中的redirect参数)
const redirect = route.query.redirect || '/'
router.push(redirect)
}
}
</script>
三、实战:完整业务流程(登录→获取用户信息→修改信息)
-
流程拆解:
用户访问登录页→输入账号密码登录→登录成功后跳转首页→首页调用getUserInfoAction
获取用户信息→个人中心调用updateUserInfoAction
修改信息→退出登录调用logoutAction
清空状态。 -
关键细节:
- Token 持久化:登录成功后将 token 存入
localStorage
,页面刷新时从localStorage
读取并恢复状态,避免重新登录。 - 请求加载状态:Pinia 中的
isLoading
控制按钮加载状态(如登录按钮“加载中”禁用点击),提升用户体验。 - 错误统一处理:Axios 拦截器捕获所有错误并提示,组件和 Pinia 无需重复写错误处理代码,减少冗余。
- Token 持久化:登录成功后将 token 存入
接下来将进入“第九章:Vue3 项目优化与部署”,讲解如何通过“打包优化(减小体积)、性能优化(提升加载速度)、Nginx 部署(上线流程)”让项目从“能跑”到“能上线”,需要我继续写吗?