# 结合拦截器实现全局加载提示

URL: https://caijiao.org/axios/wrapper/global-loading
Source: docs/axios/wrapper/global-loading.md
Description: 本章节将讲解如何在 Axios 中结合请求拦截器与响应拦截器实现全局加载提示的完整方案，包括请求计数管理、展示与隐藏 loading、按需屏蔽加载提示、与常见 UI 库（如 Element Plus、Ant Design）集成示例，以及在并发请求、错误场景下的稳定处理方案，帮助开发者提升用户体验并保持代码简洁可维护。

在实际开发中，请求发出与响应返回之间常有延迟。为了提升用户体验，可以在 Axios 拦截器中实现**全局加载提示（loading）**。下面给出思路与多种实现方式（Vue3 + Element Plus 示例、React + 全局状态示例），并说明常见注意点。

## 思路概述

1. 请求开始时显示加载动画：请求拦截器中维护活跃请求计数，当计数由 0 → 1 时显示 loading。
2. 请求结束时隐藏加载动画：响应拦截器或错误处理时递减计数，当计数为 0 时关闭 loading。
3. 支持按请求级别关闭 loading：通过 config.hideLoading 控制单次请求不触发全局 loading。
4. 支持防抖显示（可选）：对短请求避免闪烁，设置最小显示时长或延迟显示。

## Vue3 + Element Plus 示例（单文件封装）

```js
// src/utils/request.js
import axios from 'axios';
import { ElLoading, ElMessage } from 'element-plus';

let loadingInstance = null;
let requestCount = 0;
const MIN_SHOW_TIME = 300; // 最短展示时间，避免闪烁
let showTimer = null;
let shownAt = 0;

function showLoading() {
  if (requestCount === 0) {
    // 延迟显示以避免短请求闪烁
    showTimer = setTimeout(() => {
      loadingInstance = ElLoading.service({
        lock: true,
        text: '加载中...',
        background: 'rgba(0,0,0,0.3)',
      });
      shownAt = Date.now();
    }, 150); // 150ms 延迟显示
  }
  requestCount++;
}

function hideLoading() {
  requestCount = Math.max(0, requestCount - 1);
  if (requestCount === 0) {
    // 若已经显示，保证至少显示 MIN_SHOW_TIME 毫秒
    clearTimeout(showTimer);
    const elapsed = Date.now() - shownAt;
    const delay = elapsed < MIN_SHOW_TIME ? MIN_SHOW_TIME - elapsed : 0;
    setTimeout(() => {
      loadingInstance?.close?.();
      loadingInstance = null;
      shownAt = 0;
    }, delay);
  }
}

const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
  timeout: 10000,
});

// 请求拦截器
service.interceptors.request.use(
  (config) => {
    if (!config.hideLoading) showLoading();
    const token = localStorage.getItem('token');
    if (token) config.headers.Authorization = `Bearer ${token}`;
    return config;
  },
  (error) => {
    hideLoading();
    return Promise.reject(error);
  }
);

// 响应拦截器
service.interceptors.response.use(
  (response) => {
    hideLoading();
    return response;
  },
  (error) => {
    hideLoading();
    // 可在此做统一错误提示
    ElMessage.error(error.response?.data?.message || error.message || '请求失败');
    return Promise.reject(error);
  }
);

export default service;
```

## 使用方式（在组件中）

```js
import request from '@/utils/request';

// 默认会触发全局 loading
request.get('/users').then(res => console.log(res.data));

// 某些请求不想显示 loading，可设置 hideLoading
request.get('/status', { hideLoading: true }).then(res => console.log(res.data));
```

## React + 全局状态（Redux / Context）示例

```js
// store/loadingSlice.js （Redux Toolkit 示例）
import { createSlice } from '@reduxjs/toolkit';

const loadingSlice = createSlice({
  name: 'loading',
  initialState: { count: 0, visible: false },
  reducers: {
    start(state) {
      state.count++;
      state.visible = state.count > 0;
    },
    end(state) {
      state.count = Math.max(0, state.count - 1);
      state.visible = state.count > 0;
    },
    reset(state) {
      state.count = 0;
      state.visible = false;
    }
  }
});

export const { start, end, reset } = loadingSlice.actions;
export default loadingSlice.reducer;
```

```js
// utils/request.js
import axios from 'axios';
import store from '@/store';
import { start, end } from '@/store/loadingSlice';

let requestCount = 0;

function show() {
  if (requestCount === 0) store.dispatch(start());
  requestCount++;
}
function hide() {
  requestCount = Math.max(0, requestCount - 1);
  if (requestCount === 0) store.dispatch(end());
}

const api = axios.create({ baseURL: '/api' });

api.interceptors.request.use(config => {
  if (!config.hideLoading) show();
  const token = localStorage.getItem('token');
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
}, err => { hide(); return Promise.reject(err); });

api.interceptors.response.use(res => { hide(); return res; }, err => { hide(); return Promise.reject(err); });

export default api;
```

在 React 顶层渲染一个 LoadingOverlay，当 `loading.visible` 为 true 时显示全局加载组件（如 `` 或自定义遮罩）。

## 常见注意点与优化建议

1. **避免闪烁**：对短请求使用延迟显示或最短显示时间（如上示例）。
2. **并发请求**：使用计数器避免多个并发请求导致重复 hide/show。
3. **按需关闭**：支持 `config.hideLoading` 或 `config.showLoading = false` 控制单次请求不触发全局 loading。
4. **错误场景**：在请求异常或拦截器内部抛错时务必调用 hide 减少加载残留。
5. **SSR 考虑**：在服务端渲染场景避免直接调用浏览器 UI API（如 ElLoading），需在客户端渲染时初始化。
6. **可复用性**：将 show/hide/计数器封装为独立模块，方便在不同项目中复用。

## 小结

通过在 Axios 拦截器中维护活跃请求计数，并结合 UI 提示组件或全局状态，可以实现可靠且用户友好的全局加载提示。合理处理延迟显示、最短显示时长与单次请求控制，能显著提升用户体验并避免 UI 闪烁或残留问题。
