如何针对 Vue 应用中的网络请求,设计并实现一套统一的 Loading 状态管理和错误提示机制?

各位观众老爷,大家好!我是今天的主讲人,江湖人称“Bug终结者”,今天咱们来聊聊 Vue 应用中网络请求的 Loading 状态管理和错误提示机制。这玩意儿,说简单也简单,说复杂也挺复杂,关键在于“统一”二字。咱们要让整个应用在面对网络请求的时候,表现得像一个人,而不是一群乌合之众。

一、问题分析:网络请求的痛点

在开始之前,咱们先来吐槽一下传统网络请求的痛点:

  • Loading 状态散乱: 每个组件都自己搞一套 Loading,一会儿 isLoading,一会儿 isFetching,看得人眼花缭乱,维护起来更是噩梦。
  • 错误提示不统一: 有的用 alert,有的用 console.error,有的直接啥也不提示,用户一脸懵逼。
  • 代码冗余: 每个请求都要写一堆 try…catch,重复的代码让人抓狂。
  • 状态难以追踪: 多个并发请求同时进行,很难知道哪个请求在 Loading,哪个请求出错了。

这些问题,就像一个个小虫子,啃噬着咱们的代码,降低开发效率,影响用户体验。所以,我们需要一套统一的解决方案,把这些虫子一网打尽。

二、解决方案:状态管理 + 拦截器

我们的解决方案可以概括为两个核心:

  1. 状态管理 (State Management): 使用 Vuex 或 Pinia 集中管理 Loading 状态和错误信息,实现全局共享。
  2. 拦截器 (Interceptors): 利用 Axios 或 Fetch API 的拦截器,统一处理请求前后的 Loading 状态和错误提示。

三、状态管理:Vuex 的妙用

这里我们以 Vuex 为例,如果你的项目是新的,也可以考虑使用 Pinia,它更轻量级。

首先,我们需要定义一个 Vuex 模块,专门用于管理网络请求的状态:

// store/modules/request.js
const state = {
  loading: false, // 全局 Loading 状态
  error: null,   // 全局错误信息
  activeRequests: 0 // 正在进行的请求数量
};

const mutations = {
  SET_LOADING(state, value) {
    state.loading = value;
  },
  SET_ERROR(state, error) {
    state.error = error;
  },
  INCREMENT_REQUESTS(state) {
    state.activeRequests++;
  },
  DECREMENT_REQUESTS(state) {
    state.activeRequests--;
  }
};

const actions = {
  startRequest({ commit }) {
    commit('INCREMENT_REQUESTS');
    if (state.activeRequests === 1) {
        commit('SET_LOADING', true); // 只有第一个请求开始时才显示 Loading
    }
    commit('SET_ERROR', null); // 开始请求时清除之前的错误
  },
  endRequest({ commit }) {
    commit('DECREMENT_REQUESTS');
    if (state.activeRequests === 0) {
      commit('SET_LOADING', false); // 当所有请求都结束时才隐藏 Loading
    }
  },
  setError({ commit }, error) {
    commit('SET_ERROR', error);
  }
};

const getters = {
  isLoading: state => state.loading,
  error: state => state.error,
  hasError: state => state.error !== null
};

export default {
  namespaced: true,
  state,
  mutations,
  actions,
  getters
};

解释一下:

  • loading: 全局 Loading 状态,控制 Loading 组件的显示与隐藏。
  • error: 全局错误信息,用于显示错误提示。
  • activeRequests: 记录当前正在进行的请求数量,这样可以避免在并发请求时 Loading 状态的闪烁。
  • SET_LOADINGSET_ERRORINCREMENT_REQUESTSDECREMENT_REQUESTS: mutations,用于修改 state。
  • startRequestendRequestsetError: actions,用于触发 mutations。
  • isLoadingerrorhasError: getters,用于获取 state。

然后在你的 Vuex store 中注册这个模块:

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import request from './modules/request'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    request
  }
})

四、拦截器:Axios 的助攻

接下来,我们需要使用 Axios 拦截器,在请求发起前和请求完成后,更新 Vuex 中的状态。

// api/axios.js
import axios from 'axios'
import store from '../store'

const instance = axios.create({
  baseURL: '/api', // 你的 API 地址
  timeout: 10000 // 超时时间
})

// 请求拦截器
instance.interceptors.request.use(
  config => {
    // 在请求发起前,触发 startRequest action
    store.dispatch('request/startRequest')
    return config
  },
  error => {
    // 请求错误时,触发 setError action
    store.dispatch('request/setError', error.message)
    store.dispatch('request/endRequest')
    return Promise.reject(error)
  }
)

// 响应拦截器
instance.interceptors.response.use(
  response => {
    // 请求成功后,触发 endRequest action
    store.dispatch('request/endRequest')
    return response
  },
  error => {
    // 请求失败时,触发 setError action
    let errorMessage = '网络错误,请稍后重试'
    if (error.response) {
      // 服务器返回了错误信息
      errorMessage = `[${error.response.status}] ${error.response.data.message || '服务器错误'}`
    } else if (error.request) {
      // 请求发送成功,但是没有收到响应
      errorMessage = '服务器未响应'
    } else {
      // 其他错误
      errorMessage = error.message
    }

    store.dispatch('request/setError', errorMessage)
    store.dispatch('request/endRequest')
    return Promise.reject(error)
  }
)

export default instance

解释一下:

  • baseURL: 你的 API 地址,方便统一管理。
  • timeout: 超时时间,防止请求卡死。
  • request interceptor: 在请求发起前,触发 request/startRequest action,显示 Loading,并清除之前的错误信息。
  • response interceptor: 在请求成功后,触发 request/endRequest action,隐藏 Loading。如果请求失败,触发 request/setError action,显示错误提示。
  • 根据不同的错误类型,设置不同的错误信息,提高用户体验。

五、组件中使用:轻松掌控状态

现在,我们可以在组件中使用 Vuex 的 getters 和 actions,轻松掌控 Loading 状态和错误提示。

<template>
  <div>
    <div v-if="isLoading">
      Loading...
    </div>
    <div v-if="hasError" class="error-message">
      {{ error }}
    </div>
    <button @click="fetchData">获取数据</button>
    <ul>
      <li v-for="item in data" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
import api from '../api/axios'

export default {
  data() {
    return {
      data: []
    }
  },
  computed: {
    ...mapGetters('request', ['isLoading', 'error', 'hasError'])
  },
  methods: {
    async fetchData() {
      try {
        const response = await api.get('/data')
        this.data = response.data
      } catch (error) {
        // 错误已经在拦截器中处理,这里只需要做一些组件级别的操作
        console.error('组件级别错误处理:', error)
      }
    }
  }
}
</script>

<style scoped>
.error-message {
  color: red;
}
</style>

解释一下:

  • 使用 mapGetters 将 Vuex 中的 isLoadingerrorhasError 映射到组件的 computed 属性中。
  • 根据 isLoadinghasError 的值,显示 Loading 组件和错误提示。
  • fetchData 方法中,调用 API 获取数据,并处理组件级别的错误。注意,这里的错误已经在 Axios 拦截器中处理过了,所以我们只需要做一些组件级别的操作,比如记录日志。

六、更进一步:定制化 Loading 和错误提示

上面的代码只是一个简单的例子,你可以根据自己的需求,定制化 Loading 组件和错误提示。

  • Loading 组件: 可以使用现成的 UI 库,比如 Element UI、Ant Design Vue,或者自己写一个。
  • 错误提示: 可以使用 vue-toastedvue-notification 等库,显示更加友好的提示信息。
  • 错误类型: 可以根据不同的错误类型,显示不同的错误提示信息。比如,401 错误提示“未授权”,404 错误提示“资源不存在”。

七、进阶技巧:Fetch API 的替代方案

如果你不想使用 Axios,也可以使用 Fetch API,配合 async/await 和拦截器,实现类似的功能。

// api/fetch.js
import store from '../store'

const fetchWithInterceptor = async (url, options = {}) => {
  store.dispatch('request/startRequest')

  try {
    const response = await fetch(url, options)

    if (!response.ok) {
      const message = await response.text() || `HTTP error! status: ${response.status}`;
      throw new Error(message);
    }

    const data = await response.json();
    store.dispatch('request/endRequest');
    return data;

  } catch (error) {
    store.dispatch('request/setError', error.message);
    store.dispatch('request/endRequest');
    throw error; // 抛出错误,让组件可以捕获
  }
};

export default fetchWithInterceptor;

// 组件中使用
import fetchWithInterceptor from '../api/fetch';

export default {
  methods: {
    async fetchData() {
      try {
        const data = await fetchWithInterceptor('/api/data');
        this.data = data;
      } catch (error) {
        console.error('Component level error:', error);
      }
    }
  }
}

八、总结:统一的力量

通过 Vuex 和拦截器,我们实现了一套统一的 Loading 状态管理和错误提示机制。

  • 统一管理: 将 Loading 状态和错误信息集中管理,避免了状态散乱和代码冗余。
  • 全局共享: 可以在任何组件中使用,方便快捷。
  • 可定制化: 可以根据自己的需求,定制化 Loading 组件和错误提示。
  • 易于维护: 代码结构清晰,易于维护和扩展。

这套方案,就像一个强大的指挥官,统一指挥着所有的网络请求,让你的 Vue 应用更加健壮、稳定、易用。

九、Q & A 环节

好了,今天的讲座就到这里,现在是 Q & A 环节,大家有什么问题,可以提出来,我会尽力解答。

问题 回答
如何处理多个并发请求的 Loading 状态? activeRequests 记录当前正在进行的请求数量,只有当所有请求都结束时才隐藏 Loading。 startRequest 时递增 activeRequestsendRequest 时递减 activeRequests。 当 activeRequests 为 0 时,才设置 loadingfalse。 这样可以避免在并发请求时 Loading 状态的闪烁。
如何处理不同类型的错误? 在 Axios 拦截器中,根据 error.response.statuserror.code 判断错误类型,并设置不同的错误信息。例如,401 错误提示“未授权”,404 错误提示“资源不存在”。 你也可以定义一个错误码和错误信息的映射表,方便统一管理。
如果我想在某些请求中禁用 Loading 状态,怎么办? 可以在 Axios 请求配置中添加一个 disableLoading 选项,然后在请求拦截器中判断该选项的值,如果为 true,则不触发 startRequestendRequest action。 例如: javascript // api/axios.js instance.interceptors.request.use( config => { if (!config.disableLoading) { store.dispatch('request/startRequest') } return config }, ... ) // 组件中使用 api.get('/data', { disableLoading: true })
如何处理请求取消的情况? Axios 提供了 CancelToken API,可以用于取消请求。 在请求拦截器中,创建一个 CancelToken,并将其传递给 Axios 请求配置。 在组件卸载时,调用 CancelTokencancel 方法,取消请求。 javascript // api/axios.js import axios from 'axios'; const CancelToken = axios.CancelToken; const source = CancelToken.source(); instance.interceptors.request.use( config => { config.cancelToken = source.token; return config; }, error => { return Promise.reject(error); } ); // 组件中使用 import api from '../api/axios'; export default { beforeDestroy() { source.cancel('组件卸载,取消请求'); }, async fetchData() { try { const response = await api.get('/data'); this.data = response.data; } catch (error) { if (axios.isCancel(error)) { console.log('请求已取消', error.message); } else { console.error(error); } } } };
如何在 TypeScript 项目中使用? 在 TypeScript 项目中,需要为 Vuex 模块和 Axios 拦截器定义类型,以确保类型安全。 例如,可以为 Vuex state 定义一个接口,为 actions 和 mutations 定义类型,为 Axios 请求配置定义类型。 typescript // store/modules/request.ts interface RequestState { loading: boolean; error: string | null; } const state: RequestState = { loading: false, error: null, }; // api/axios.ts import { AxiosRequestConfig } from 'axios'; interface CustomAxiosRequestConfig extends AxiosRequestConfig { disableLoading?: boolean; } instance.interceptors.request.use( (config: CustomAxiosRequestConfig) => { if (!config.disableLoading) { store.dispatch('request/startRequest'); } return config; }, ... );

希望今天的讲座对大家有所帮助,咱们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注