第八章:Vue3 异步请求与状态管理进阶(从基础到工程化)

第八章: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>

三、实战:完整业务流程(登录→获取用户信息→修改信息)

  1. 流程拆解
    用户访问登录页→输入账号密码登录→登录成功后跳转首页→首页调用 getUserInfoAction 获取用户信息→个人中心调用 updateUserInfoAction 修改信息→退出登录调用 logoutAction 清空状态。

  2. 关键细节

    • Token 持久化:登录成功后将 token 存入 localStorage,页面刷新时从 localStorage 读取并恢复状态,避免重新登录。
    • 请求加载状态:Pinia 中的 isLoading 控制按钮加载状态(如登录按钮“加载中”禁用点击),提升用户体验。
    • 错误统一处理:Axios 拦截器捕获所有错误并提示,组件和 Pinia 无需重复写错误处理代码,减少冗余。

接下来将进入“第九章:Vue3 项目优化与部署”,讲解如何通过“打包优化(减小体积)、性能优化(提升加载速度)、Nginx 部署(上线流程)”让项目从“能跑”到“能上线”,需要我继续写吗?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值