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

背景

在现代 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('所有请求失败');
        });
}
相关推荐
炫饭第一名1 小时前
速通Canvas指北🦮——基础入门篇
前端·javascript·程序员
NineData2 小时前
数据库迁移总踩坑?用 NineData 迁移评估,提前识别所有兼容性风险
数据库·程序员·云计算
王晓枫2 小时前
flutter接入三方库运行报错:Error running pod install
前端·flutter
符方昊2 小时前
React 19 对比 React 16 新特性解析
前端·react.js
ssshooter2 小时前
又被 Safari 差异坑了:textContent 拿到的值居然没换行?
前端
曲折2 小时前
Cesium-气象要素PNG色斑图叠加
前端·cesium
Forever7_2 小时前
Electron 淘汰!新的桌面端框架 更强大、更轻量化
前端·vue.js
Angelial2 小时前
Vue3 嵌套路由 KeepAlive:动态缓存与反向配置方案
前端·vue.js
BugShare2 小时前
写一个你自己的Agent Skills
人工智能·程序员
jiayu3 小时前
Angular学习笔记24:Angular 响应式表单 FormArray 与 FormGroup 相互嵌套
前端