各位观众老爷,大家好!我是今天的主讲人,江湖人称“Bug终结者”,今天咱们来聊聊 Vue 应用中网络请求的 Loading 状态管理和错误提示机制。这玩意儿,说简单也简单,说复杂也挺复杂,关键在于“统一”二字。咱们要让整个应用在面对网络请求的时候,表现得像一个人,而不是一群乌合之众。
一、问题分析:网络请求的痛点
在开始之前,咱们先来吐槽一下传统网络请求的痛点:
- Loading 状态散乱: 每个组件都自己搞一套 Loading,一会儿
isLoading
,一会儿isFetching
,看得人眼花缭乱,维护起来更是噩梦。 - 错误提示不统一: 有的用
alert
,有的用console.error
,有的直接啥也不提示,用户一脸懵逼。 - 代码冗余: 每个请求都要写一堆 try…catch,重复的代码让人抓狂。
- 状态难以追踪: 多个并发请求同时进行,很难知道哪个请求在 Loading,哪个请求出错了。
这些问题,就像一个个小虫子,啃噬着咱们的代码,降低开发效率,影响用户体验。所以,我们需要一套统一的解决方案,把这些虫子一网打尽。
二、解决方案:状态管理 + 拦截器
我们的解决方案可以概括为两个核心:
- 状态管理 (State Management): 使用 Vuex 或 Pinia 集中管理 Loading 状态和错误信息,实现全局共享。
- 拦截器 (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_LOADING
、SET_ERROR
、INCREMENT_REQUESTS
、DECREMENT_REQUESTS
: mutations,用于修改 state。startRequest
、endRequest
、setError
: actions,用于触发 mutations。isLoading
、error
、hasError
: 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 中的isLoading
、error
和hasError
映射到组件的 computed 属性中。 - 根据
isLoading
和hasError
的值,显示 Loading 组件和错误提示。 - 在
fetchData
方法中,调用 API 获取数据,并处理组件级别的错误。注意,这里的错误已经在 Axios 拦截器中处理过了,所以我们只需要做一些组件级别的操作,比如记录日志。
六、更进一步:定制化 Loading 和错误提示
上面的代码只是一个简单的例子,你可以根据自己的需求,定制化 Loading 组件和错误提示。
- Loading 组件: 可以使用现成的 UI 库,比如 Element UI、Ant Design Vue,或者自己写一个。
- 错误提示: 可以使用
vue-toasted
、vue-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 时递增 activeRequests , endRequest 时递减 activeRequests 。 当 activeRequests 为 0 时,才设置 loading 为 false 。 这样可以避免在并发请求时 Loading 状态的闪烁。 |
如何处理不同类型的错误? | 在 Axios 拦截器中,根据 error.response.status 或 error.code 判断错误类型,并设置不同的错误信息。例如,401 错误提示“未授权”,404 错误提示“资源不存在”。 你也可以定义一个错误码和错误信息的映射表,方便统一管理。 |
如果我想在某些请求中禁用 Loading 状态,怎么办? | 可以在 Axios 请求配置中添加一个 disableLoading 选项,然后在请求拦截器中判断该选项的值,如果为 true ,则不触发 startRequest 和 endRequest 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 请求配置。 在组件卸载时,调用 CancelToken 的 cancel 方法,取消请求。 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; }, ... ); |
希望今天的讲座对大家有所帮助,咱们下期再见!