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

背景

在现代 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('所有请求失败');
        });
}
相关推荐
gnip3 分钟前
做个交通信号灯特效
前端·javascript
小小小小宇4 分钟前
Webpack optimization
前端
尝尝你的优乐美6 分钟前
前端查缺补漏系列(二)JS数组及其扩展
前端·javascript·面试
咕噜签名分发可爱多8 分钟前
苹果iOS应用ipa文件安装之前?为什么需要签名?不签名能用么?
前端
她说人狗殊途22 分钟前
Ajax笔记
前端·笔记·ajax
yqcoder31 分钟前
33. css 如何实现一条 0.5 像素的线
前端·css
excel1 小时前
Nuxt 3 + PWA 通知完整实现指南(Web Push)
前端·后端
yuanmenglxb20041 小时前
构建工具和脚手架:从源码到dist
前端·webpack
rit84324991 小时前
Web学习:SQL注入之联合查询注入
前端·sql·学习
啃火龙果的兔子1 小时前
Parcel 使用详解:零配置的前端打包工具
前端