当部分请求失败时,前端如何保证用户体验不崩溃?

背景

在现代 Web 开发中,前端通常需要向服务器发送多个异步请求(如批量数据获取、并行 API 调用等)。然而,由于网络波动、服务器错误或接口限制,部分或全部请求可能会失败。

常见问题

  1. 批量请求失败时,如何避免重复弹窗?

    • 如果每个失败的请求都单独弹出一个错误提示(如 Toast),会导致短时间内出现多个弹窗,影响用户体验。
    • 例如:用户点击"批量删除"按钮,发送 10 个删除请求,其中 5 个失败。如果每个错误都弹窗,用户会看到多个重复的提示。
  2. 如何优化错误提示策略?

    • 单次提示:无论有多少个请求失败,只弹出一个 Toast(如"部分操作失败"或"网络异常")。
    • 智能合并错误:如果多个请求失败,可以合并错误信息(如"3/10 个操作失败")。

适用场景

  • 批量操作(如批量删除、批量上传)
  • 并行数据加载(如同时加载多个模块的数据)
  • 接口轮询/重试(如多个接口轮询时统一处理错误)

目标

设计一个方案,确保在批量请求失败时: ✅ 只弹出一个 Toast (避免重复提示) ✅ 合理合并错误信息 (如统计失败数量) ✅ 不影响正常业务逻辑(错误仍能被捕获和处理)

介绍几种实现方案

1. 使用全局标志位

通过设置一个布尔变量 isToastShown 来标记是否已弹出 Toast。在所有请求完成后,检查该变量,如果未弹出,则弹出一个 Toast 并设置标志位为 true。这种方法适用于简单的单次通知场景。

示例代码:

ini 复制代码
let isToastShown = false;
​
function fetchJSON(url) {
    return fetch(url)
        .then(response => {
            if (!response.ok) {
                throw new Error('Network response was not ok');
            }
            return response.json();
        })
        .catch(error => {
            if (!isToastShown) {
                showToast('请求失败');
                isToastShown = true;
            }
            throw error;
        });
}
​
function makeRequests(urls) {
    Promise.all(urls.map(fetchJSON))
        .catch(() => {
            if (!isToastShown) {
                showToast('所有请求失败');
                isToastShown = true;
            }
        });
}

2. 使用防抖函数

通过创建一个防抖函数 debounce,限制通知函数的执行频率,确保在一定时间内只执行一次通知。这种方法适用于需要限制通知频率的场景。

示例代码:

javascript 复制代码
function debounce(fn, delay) {
    let timer;
    return function(...args) {
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => fn.apply(this, args), delay);
    };
}
​
const showToastDebounced = debounce(showToast, 3000);
​
function fetchJSON(url) {
    return fetch(url)
        .then(response => {
            if (!response.ok) {
                throw new Error('Network response was not ok');
            }
            return response.json();
        })
        .catch(error => {
            showToastDebounced('请求失败');
            throw error;
        });
}
​
function makeRequests(urls) {
    Promise.all(urls.map(fetchJSON))
        .catch(() => {
            showToastDebounced('所有请求失败');
        });
}

3. 使用计数器

通过维护一个计数器来跟踪未处理的请求数量。每当发起一个请求,计数器加一;请求完成时,无论成功或失败,计数器减一。在请求失败时,先检查计数器值,如果仍大于零,则不弹出新的 Toast;只有当计数器归零时,才弹出一个 Toast 提示请求失败。这种方法适用于批量请求的错误统一管理。

示例代码:

ini 复制代码
let pendingRequests = 0;
​
function fetchJSON(url) {
    pendingRequests++;
    return fetch(url)
        .then(response => {
            if (!response.ok) {
                throw new Error('Network response was not ok');
            }
            return response.json();
        })
        .catch(error => {
            pendingRequests--;
            if (pendingRequests === 0) {
                showToast('所有请求失败');
            }
            throw error;
        })
        .finally(() => {
            pendingRequests--;
            if (pendingRequests === 0) {
                showToast('所有请求失败');
            }
        });
}
​
function makeRequests(urls) {
    urls.forEach(url => fetchJSON(url));
}

4. 使用 Promise.allSettled

使用 Promise.allSettled 来处理批量请求,然后检查结果中是否有错误。如果有错误,只弹出第一个错误的 Toast。这种方法适用于批量请求的错误统一管理。

示例代码:

javascript 复制代码
function fetchJSON(url) {
    return fetch(url)
        .then(response => {
            if (!response.ok) {
                throw new Error('Network response was not ok');
            }
            return response.json();
        });
}
​
function makeRequests(urls) {
    Promise.allSettled(urls.map(fetchJSON))
        .then(results => {
            const hasError = results.some(result => result.status === 'rejected');
            if (hasError) {
                showToast('所有请求失败');
            }
        });
}

5. 使用 Toast 组件的单例模式

许多前端框架和库中的 Toast 组件默认采用单例模式,即同一时间只会存在一个 Toast。如果需要在同一时间弹出多个 Toast,可以参考相应文档进行配置。这种方法适用于需要避免重复弹出 Toast 的场景。

示例代码(Vue.js 中使用 Vant Toast 组件):

javascript 复制代码
import { showToast } from 'vant';
​
function fetchJSON(url) {
    return fetch(url)
        .then(response => {
            if (!response.ok) {
                throw new Error('Network response was not ok');
            }
            return response.json();
        })
        .catch(error => {
            showToast('请求失败');
            throw error;
        });
}
​
function makeRequests(urls) {
    Promise.all(urls.map(fetchJSON))
        .catch(() => {
            showToast('所有请求失败');
        });
}
相关推荐
白兰地空瓶几秒前
JavaScript 列表转树(List to Tree)详解:前端面试中如何从递归 O(n²) 优化到一次遍历 O(n)
javascript·算法·面试
大布布将军3 分钟前
⚡️ 后端工程师的护甲:TypeScript 进阶与数据建模
前端·javascript·程序人生·typescript·前端框架·node.js·改行学it
chilavert31840 分钟前
技术演进中的开发沉思-260 Ajax:核心动画
开发语言·javascript·ajax
程序员小易42 分钟前
前端轮子(1)--前端部署后-判断页面是否为最新
前端·vue.js·node.js
xiaoxue..43 分钟前
列表转树结构:从扁平列表到层级森林
前端·javascript·算法·面试
小oo呆1 小时前
【自然语言处理与大模型】LangChainV1.0入门指南:核心组件Agent
前端·javascript·easyui
BD_Marathon1 小时前
关于JS和TS选择的问题
开发语言·javascript·ecmascript
Hao_Harrision1 小时前
50天50个小项目 (React19 + Tailwindcss V4) ✨ | DrawingApp(画板组件)
前端·react.js·typescript·tailwindcss·vite7
dly_blog1 小时前
Vite 原理与 Vue 项目实践
前端·javascript·vue.js
仅此,2 小时前
前端接收了id字段,发送给后端就变了
java·前端·javascript·spring·typescript