• 作者:老汪软件技巧
  • 发表时间:2024-08-22 10:09
  • 浏览量:

前言

相信大家在自己的项目实际应用中都会遇到慢网的场景。

在慢网环境中,应用的稳定性和交互是用户体验的关键,所以我们需要从前端的角度提出来一些方式来尽可能的避免慢网对于接口数据的影响,尽可能满足实际的应用场景。

那么我先给大家提出三个问题,大家先行思考,再逐步解决:

问题接口资源......模拟

import React, { useState } from "react";
import { Button } from "antd-mobile";
let nnIndex = 0; // 用于模拟接口请求的index
const Demo = () => {
  const [demoMsg, setDemoMsg] = useState<string>(''); // 接口返回的数据
  /** 模拟接口 */
  const fetchDataApi = (index: number) => {
    const timeRound = Math.round(Math.random() * 10000); // 响应时间在0-10s之间
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(`接口返回数据--api${index}, 响应时间${timeRound}ms`);
      }, timeRound);
    });
  }
  /** 模拟调用接口 */
  const fetchData = async (index: number) => {
    try {
      const res = await fetchDataApi(index);
      console.log('res: ', res);
      setDemoMsg(res);
    } catch (error) {
      console.log('error: ', error)
    }   
  }
  return <div>
      <Button color="success" onClick={() => fetchData(nnIndex++)}>点击模拟慢网请求Button>
      <h1>{demoMsg}h1>
  div>
}
export default Demo;

慢网chatGPT 对与慢网的定义

网络速度的快慢通常根据特定使用场景的需求来定义。

以下是一些常见的网络速度分类及其对应的体验:

1. 慢网

2. 普通网

3. 快网

4. 超快网

常见应用场景的需求

测试慢网

Download(下载速度):指从服务器到客户端的数据传输速度,通常以 Mbps(兆比特每秒)或 Kbps(千比特每秒)为单位。例如,3G 网络的下载速度通常比 4G 网络慢很多。Upload(上传速度):指从客户端到服务器的数据传输速度,通常也以 Mbps 或 Kbps 为单位。上传速度对一些应用(如视频通话、上传文件等)非常重要。Latency(延迟):指数据包从客户端发送到服务器并返回的时间延迟,通常以毫秒(ms)为单位。高延迟会导致明显的网络延迟,影响用户体验。例如,卫星互联网的延迟通常比光纤连接高很多。Packet Loss(数据包丢失):指在网络传输过程中丢失的数据包百分比。数据包丢失可能导致连接不稳定、数据传输中断或质量下降,尤其在实时应用(如视频通话或在线游戏)中。Packet Queue Length(数据包队列长度):指在网络传输过程中等待传输的数据包数量。较长的队列长度可能导致延迟增加,因为数据包需要在队列中等待传输。Packet Reordering(数据包重新排序)是指在计算机网络中,数据包到达的顺序与发送时的顺序不同。实践1、防抖+loading


2、对同一个接口串行使用场景

轮询,需要对每一次轮询的数据都做处理

思路

  let flag = false; // 全局标志位
  /** 模拟调用接口 */
  const fetchData = async (index: number) => {
    if(flag) return; // 如果正在请求中,则不再请求
    try {
      flag = true;
      const res = await fetchDataApi(index);
      flag = false;
      console.log('res: ', res);
      setDemoMsg(res);
    } catch (error) {
      flag = false;
      console.log('error: ', error)
    }   
  }

3、重复接口请求取消使用场景

不能使用防抖等技巧,在某一种情况下确实需要短时间多次请求数据

注意事项思路AbortController

abortController接口表示一个控制器对象,允许你根据需要中止一个或多个 Web 请求;

主要使用它提供的一个构造函数AbortController()、一个实例属性AbortController.signal、一个实例方法AbortController.abort()

代码实现

1、在调用接口的地方给接口传参,判断该接口是否需要走这个配置

export function api(data) {
  return http({
    url: `自己的url`,
    method: "post",
    data,
    cancelDuplicateRequests: true, // true为是,false为不是
  });
}

2、将map管理逻辑、生成请求唯一标识、检查是否存在重复请求、取消请求统一封装到一个文件,方便管理

相信大家在自己的项目实际应用中都会遇到__相信大家在自己的项目实际应用中都会遇到

import { AxiosRequestConfig } from "axios";
/** 获取请求唯一标识 */
export const getRequestIdentify = (config: AxiosRequestConfig) => {
    const { method, url, params = {}, data = {} } = config || {};
    return [method, url, JSON.stringify(params), JSON.stringify(data)].join("&");
}
/** 键值对存储当前请求信息 */
const pendingRequestMap = new Map();
/** 删除对应的key值 */
export const deletePendingRequestMap = (config: AxiosRequestConfig) => {
    const requestKey = getRequestIdentify(config);
    pendingRequestMap.delete(requestKey);
}
/** 用于把当前的请求信息添加到请求对象中 */
export const addRequest = (config: AxiosRequestConfig) => {
    const requestKey = getRequestIdentify(config);
    if(!config.signal) {
        const controller  = new AbortController();
        config.signal = controller.signal;
        if(!pendingRequestMap.has(requestKey)) {
            pendingRequestMap.set(requestKey, controller);
        }
    }
}
/** 检查是否存在重复请求,若存在则取消已发的请求 */
export const removeRequest = (config: AxiosRequestConfig) => {
    const requestKey = getRequestIdentify(config);
    if(pendingRequestMap.has(requestKey)) {
        const controller = pendingRequestMap.get(requestKey);
        controller.abort();
        deletePendingRequestMap(config);
    }
}

3、在请求和响应拦截器中调用这些功能函数

service.interceptors.request.use(
  (config) => {
    const { cancelDuplicateRequests = false } = config;
    if(cancelDuplicateRequests) {
      removeRequest(config); // 取消重复请求
      addRequest(config); // 添加请求信息
    }
    ...别的逻辑
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

service.interceptors.response.use(
  (response) => {
    const { config } = response;
    removeRequest(config);  // 移除该请求key
    ...其它逻辑
  },
  (error: any) => {
  
    if(!axios.isCancel(error)) {
      removeRequest(config);  // 移除该请求key
    }
    if(axios.isCancel(error)) { // 针对取消异常做处理
      console.log('error111: ', error);
    }
    ...其它逻辑
    return Promise.reject(error);
  }
);

CancelToken

Axios 的 cancel token API 是基于被撤销cancelable promises proposal

此 API 从v0.22.0开始已被弃用,不应在新项目中使用。

更改关键代码:

/** 用于把当前的请求信息添加到请求对象中 */
export const addRequest = (config: AxiosRequestConfig) => {
    const requestKey = getRequestIdentify(config);
    if(!config.cancelToken) { // 区别
        const CancelToken = axios.CancelToken; // 区别
        const source = CancelToken.source(); // 区别
        config.cancelToken = source.token; // 区别
        if(!pendingRequestMap.has(requestKey)) {
            pendingRequestMap.set(requestKey, source); // 区别
        }
    }
}
/** 检查是否存在重复请求,若存在则取消已发的请求 */
export const removeRequest = (config: AxiosRequestConfig) => {
    const requestKey = getRequestIdentify(config);
    if(pendingRequestMap.has(requestKey)) {
        const source = pendingRequestMap.get(requestKey); // 区别
        source.cancel(); // 区别
        deletePendingRequestMap(config);
    }
}

4、接口重试机制使用场景

由于网络或者服务等原因,导致接口超时,或产生未知错误,降低用户刷新页面的操作成本,自动重新请求接口;

思路

利用第三方依赖axios-retry,给所有接口,或者以接口为颗粒度去进行配置,让其在对应的错误下,进行对应次数的重新请求;

全局设置

axiosRetry(service, {//传入axios实例
  retries: 3,              // 设置自动发送请求次数
  retryDelay: (retryCount) => {
    return retryCount * 1500;      // 重复请求延迟(毫秒)
  },
  shouldResetTimeout: true,       //  重置超时时间
  retryCondition: (error) => {
    //true为打开自动发送请求,false为关闭自动发送请求
    if (error.message.includes('timeout') || error.message.includes("status code")) {
      return true;
    } else {
      return false;
    };
  }
});

单个接口设置

export function api(data) {
  return http({
    url: `自己的url`,
    method: "post",
    data,
    isNeedRetry: true, // 是否需要重试,否则不需要重试
  });
}
axiosRetry(service, {//传入axios实例
  retries: 3,              // 设置自动发送请求次数
  retryDelay: (retryCount) => {
    return retryCount * 1500;      // 重复请求延迟(毫秒)
  },
  shouldResetTimeout: true,       //  重置超时时间
  retryCondition: (error) => {
    const { config,  message} = error; 
    const { isNeedRetry } = config;
    if(!isNeedRetry) return false; // 该接口不需要重试
    //true为打开自动发送请求,false为关闭自动发送请求
    if (message.includes('timeout') || message.includes("status code")) {
      return true;
    } else {
      return false;
    };
  }
});

5、图片失败自动加载+点击重新加载使用场景

由于网络问题,图片资源加载报错

思路

import { useEffect } from "react";
interface IUseImgReload {
  count?: number;
  imgQuerySelector?: string;
  qid?: string;
}
/**
 * @param count 重新加载图片的次数
 */
export const useImgReload = (config: IUseImgReload) => {
  const { count, imgQuerySelector = 'img', qid } = config || {}
  useEffect(() => {
    setTimeout(() => {
      const images = document.querySelectorAll(imgQuerySelector);
      if (images.length === 0) return;
      images.forEach((img) => {
        img.onerror = () => {
          img.dataset.errCount = img.dataset.errCount ? `${parseInt(img.dataset.errCount) + 1}` : '1';
          if (parseInt(img.dataset.errCount) <= count) {
            setTimeout(() => {
              // 重新加载图片
              img.src = img.src;
            }, 1500);
          } else {
            // 错误次数超过限制,添加点击加载图片能力
            img.style.fontSize = '12px';
            img.alt = '图片加载失败, 点击重新加载';
            img.onclick = () => {
              img.src = img.src;
            };
          }
        }
      });
    }, 0);
  }, [qid]);
}

6、网络异常自动刷新页面使用场景

对于mqtt网慢情况下连接失败,mqtt多次自动重连不上的场景,减少用户自己手动刷新页面这一步,自动重新刷新页面;

思路

clientSelf.on('error', (err: Error) => {
      Toast.show('页面出了点问题,请稍后刷新页面!');
      console.error('Connection error: ', err);
      mqttErrorLog(err)
      setConnectStatus('error')
      errorToReload('error')
    });

const getErrorCount = () => {
  const matches = document.cookie.match(/errorCount=(\d+)/);
  return matches ? Number(matches[1]) : 0;
};
const setErrorCount = (count: number) => {
  const maxAge = 3 * 60 * 60; // 3 hours in seconds
  document.cookie = `errorCount=${count}; Max-Age=${maxAge}; path=/`;
};
const MAX_ERROR_COUNT = 6; // 设置最大错误次数
let reloadTimeout: string | number | NodeJS.Timeout | null | undefined = null; // 用于存储 setTimeout 返回的 timeout ID
const errorToReload = (type: string) => {
  if (reloadTimeout !== null) {
    // 如果已经存在一个待执行的刷新操作,取消它
    clearTimeout(reloadTimeout);
  }
  // 设置一个新的刷新操作,在用户确认后 1 秒执行
  reloadTimeout = setTimeout(() => {
    let errorCount = getErrorCount();
    errorCount += 1;
    setErrorCount(errorCount);
    
    if (errorCount <= MAX_ERROR_COUNT) {
      // 如果错误次数小于最大错误次数,刷新页面
      window.location.reload();
    } else {
      // 如果错误次数达到最大错误次数,停止刷新页面,埋点上报
      mqttMaxErrorLog(type)
    }
  }, 1000)
}

7、用小图判断网速,弹出网速提示使用场景

用于一些判断网速的场景

思路

  const testDownloadSpeed = ({ url = "https://m.xiwang.com/resource/-AoxAKO9BfzlhTcwuXiso-1700028922889.png", size }) => {
    return new Promise((resolve, reject) => {
      const img = new Image()
      img.src = `${url}?_t=${Math.random()}` // 加个时间戳以避免浏览器只发起一次请求
      const startTime = new Date()
  
      img.onload = function () {
        const fileSize = size // 单位是 kb
        const endTime = new Date()
        const costTime = endTime - startTime
        const speed = fileSize / (endTime - startTime) * 1000 // 单位是 kb/s
        console.log('speed: ', speed);
        console.log('costTime: ', costTime);
        resolve({ speed, costTime })
      }
  
      img.onerror = reject
    })
  }

......

未完待续