LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

【Web前端开发】偷偷告诉你,我们项目里的进度条,全都是假的!

admin
2025年1月10日 15:8 本文热度 72

导读

这篇文章主要探讨了项目中假进度条的实现。先介绍需求背景,然后调研了 NProgress 和 fake-progress 两个方案,分析其源码和特点。接着提到因现有方案的不足,作者萌生封装自己的 hook(useFakeProgress)的想法,确定方案和入参,给出实现细节和示例,强调可根据业务定制。

扯皮

最近接到了一个需求:前端点击按钮触发某个任务并开启轮询获取任务进度,直至 100% 任务完成后给予用户提示

这个业务场景还挺常见的,但是突然上周后端联系到我说现在的效果有点差,之前都是小任务那进度条展示还挺不错的,现在有了一些大任务且会存在排队阻塞的情况,就导致视图上经常卡 0% 排队,用户体验太差了,问能不能在刚开始的时候做个假进度先让进度条跑起来😮

因此就有了这篇文章,简单做一下技术调研以及在项目中的应用

正文

其实假进度条也不难做,无非是轮询的时候我们自己做一个随机的自增,让它卡到 99% 等待后端真实进度完成后再结束

只不过还是想调研一下看看市面上有没有一些成熟的方案并去扒一下它们的源码🤓

NProgress

首先当我听到这里的需求后第一时间想到的就是它:rstacruz/nprogress: For slim progress bars like on YouTube, Medium, etc

记得大学期间做的一些中后台系统基本都少不了路由跳转时的顶部进度条加载,那时候就有了解到 NProgress,它的使用方式也很简单,完全手控:NProgress: slim progress bars in JavaScript,去文档里玩一下就知道了

视图呈现的效果就是如果你不手动结束那它就会一直缓慢前进卡死 99% ,挺符合我们这里的需求,可以去扒一下它内部进度计算相关的逻辑

NProgress 的内容实际上比较少,源码拉下来可以看到主要都在这一个 JS 文件里了:

需要注意的是我们看的是这个版本:rstacruz/nprogress at v0.2.0,master 分支与 npm 安装的 0.2.0 内部实现还是有些差别的

我们这里不关注它的样式相关计算,主要来看看对进度的控制,直奔 start 方法:

还是比较清晰的,这里的 status 就是内部维护的进度值,默认为 null,所以会执行 NProgress.set,我们再来看看 set 方法:

set 方法里有一大堆设置动画样式逻辑都被我剪掉了,关于进度相关的只有这些。相当于利用 clamp 来做一个夹层,因为初始进来的 n 为 null,所以经过处理后进度变为 0.08

再回到 start 的逻辑,其中 work 就是内部轮询控制进度自增的方法了,初始配置 trickle 为 true 代表自动开启进度自增,由于进度条在 set 方法中已经设置为 0.08,所以走到后面的 NProgress.trickle 逻辑

看来这里就是进度控制的核心逻辑了, trickle 里主要调用了 inc,在 trickle 中给 inc 传递了一个参数:Math.random() * Settings.trickleRate,显然这里范围是:0 <= n < 0.02

而在 inc 中,如果传递的 amount 有值的话那就每次以该值进行自增,同时又使用 clamp 将最大进度卡在 0.994

最后再调用 set 方法,set 里才是更新进度和视图进度条的方法,涉及到进度更新时都需要回到这里

当然 NProgress.inc 也可以手动调用,还对未传参做了兼容处理:

amount = (1 - n) * clamp(Math.random() * n, 0.1, 0.95)

即根据当前进度 n 计算剩余进度,再随机生成自增值

再来看 done 方法,它就比较诡异了:

按理来说直接将进度设置为 1 就行,但它以链式调用 inc 再调用 set,相当于调用了两次 set

而这里 inc 传参又没什么规律性,推测是为了 set 中的样式处理,感兴趣的可以去看看那部分逻辑,还挺多的...😶

一句话总结一下 NProgress 的进度计算逻辑:随机值自增,最大值限制

但是因为 NProgress 与进度条样式强绑定,我们肯定是没法直接用的

fake-progress

至于 fake-progress 则是我在调研期间直接搜关键词搜出来的😶:piercus/fake-progress: Fake a progress bar using an exponential progress function

很明显看介绍就是干这个事的,而且还十分专业,引入数学函数展示假进度条效果,具有说服力:

所以我们项目中其实就是用的这个包,只不过和 NProgress 类似,两个包都比较老了,瞟一眼源码发现都是老 ES5 了🤐

因为我们项目中用的是 React,这里给出 React 的 demo 吧,为了编写方便使用了几个 ahooks 里的 hook:

其实使用方法上与 NProgress 都类似,不过两者都属于通用的工具库不依赖其他框架,所以像视图渲染都需要自己手动来做

注意实例化中的传参 timeConstant,某种意义上来讲这个值就相当于“进度增长的速率”,但也不完全等价,我们来看看源码

因为不涉及到样式,fake-progress 源码更简单,核心就在这里:

下方的数学公式就是介绍图中展示的,只能说刚看到这部分内容是真的是死去的数学只是突然又开始攻击我😅,写了那么多函数,数学函数是啥都快忘了

我们来简单分析一下这个函数 1 - Math.exp(-1 * x),exp(x)= exex 的图像长这样,高中的时候见的太多了:

那假如这里改成 exp(-x) 呢?有那味了,以前应该是有一个类似的公式 f(x) 与 f(x) 图像效果是关于 y 轴对称,好像是有些特殊的不符合这个规律?🤔反正大部分都是满足的

OK,那我们继续进行转换,看看 -exp(-x) 的效果

同样有个公式 f(x) 与 f(x) 图像效果是关于 x 轴对称:

初见端倪,不知道你们有没有注意 -exp(-x) 最终呈现的图像是无限接近于 x 轴的,也就是 0:

那有了🤓,假如我再给它加个 1 呢?它不就无限接近于 1 了,即 -exp(-x) + 1,这其实就是 fake-progress  里公式的由来:

但你会发现如果 x 按 1 递增就很快进度就接近于 1 了,所以有了 timeConstant 配置项来控制 x 的增长,回看这个公式:1 - Math.exp(-1 * this._time / this.timeConstant)

this._time 是一直在增长的,而 this.timeConstant 作为分母如果被设置为一个较大的值,那可想而知进度增长会巨慢

所以 fake-progress 的核心原理是借助数学函数,以函数值无限接近于 1 来实现假进度条,但是这种实现有一个 bug,可以看我提的这个 issues,不过看这个包的更新时间感觉作者也不会管了😅:

bug: progress may reach 100% · Issue #7 · piercus/fake-progress

useFakeProgress

虽然我们现在项目中使用的是 fake-progress,但是个人感觉用起来十分鸡肋,而且上面的 bug 也需要自己手动兼容,因此萌生出自己封装一个 hook 的想法,让它更符合业务场景

首先我们确定一下进度计算方案,这里我毫不犹豫选择的是 NProgress 随机值增长方案,为什么?因为方便用户自定义

而且 NProgress 相比于 fake-progress 有一个巨大优势:手动 set 进度后仍然保持进度正常自动递增

这点在 fake-progress 中实现是比较困难的,因为你无法保证手动 set 的进度是在这个函数曲线上,相当于给出函数 y 值反推 x 值,根据反推的 x 值再进行递增,想想都麻烦

确定好方案后我们来看下入参吧,参考 NProgress 我定义了这几个配置项:

这里我简单解释一下 rerender 和 amount 配置:

实际上在封装这个 hook 的时候我一直在纠结这里的 progress 到底是 state 还是 ref,因为大多数场景下 hook 内部通过轮询定时器更新进度,而真实业务代码中也会开启定时器去轮询监听业务接口的

所以如果写死为 state,那这个场景 hook 内部的每次更新 render 是没必要的,但是假如用户又想只是使用假进度展示,没有后端业务接口呢?

思来想去其实完全可以放权给用户进行配置,因为 state = ref + update,统一使用 ref,用户配置 rerender 时我们在每次更新时 update 即可

至于 amount 我是希望放权给用户进行自定义递增值,你可以配置成一个固定值也可以配置成随机值,更可以像 NProgress master 分支下这样根据当前进度来控制自增,反正以函数参数的形式能够拿到当前的 progress:

至于实现细节就不再讲述了,实际上就是轮询定时器没什么复杂的东西,直接上源码了:

import { useRef, useState } from "react";


interface Options {

  minimun?: number;

  maximum?: number;

  speed?: number;

  rerender?: boolean;

  amount?: (progress: number) => number;

  formatter?: (progress: number) => string;

  onProgress?: (progress: number) => void;

  onFinish?: () => void;

}


export function useFakeProgress(options?: Options): [

  { current: string },

  {

    inc: (amount?: number) => void;

    set: (progress: number) => void;

    start: () => void;

    stop: () => void;

    done: () => void;

    reset: () => void;

    get: () => number;

  }

] {

  const {

    minimun = 0.08,

    maximum = 0.99,

    speed = 800,

    rerender = false,

    amount = (p: number) => (1 - p) * clamp(Math.random() * p, minimun, maximum),

    formatter = (p: number) => `${p}`,

    onProgress,

    onFinish,

  } = options || {};


  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  const progressRef = useRef(0);

  const progressDataRef = useRef(""); // formatter 后结果

  const [, update] = useState({});


  const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);


  const setProgress = (p: number) => {

    progressRef.current = p;

    progressDataRef.current = formatter(p);

    onProgress?.(p);

    if (rerender) update({});

  };


  const work = () => {

    const p = clamp(progressRef.current + amount(progressRef.current), minimun, maximum);

    setProgress(p);

  };


  const start = () => {

    function pollingWork() {

      work();

      timerRef.current = setTimeout(pollingWork, speed);

    }


    if (!timerRef.current) pollingWork();

  };


  const set = (p: number) => {

    setProgress(clamp(p, minimun, maximum));

  };


  const inc = (add?: number) => {

    set(progressRef.current + (add || amount(progressRef.current)));

  };


  const stop = () => {

    if (timerRef.current) clearInterval(timerRef.current);

    timerRef.current = null;

  };


  const reset = () => {

    stop();

    setProgress(0);

  };


  const done = () => {

    stop();

    setProgress(1);

    onFinish?.();

  };


  return [

    progressDataRef,

    {

      start,

      stop,

      set,

      inc,

      done,

      reset,

      get: () => progressRef.current,

    },

  ];

}

这里需要补充一个细节,在返回值里使用的是 progressDataRef 是 formatter 后的结果为 string 类型,如果用户想要获取原 number 的 progress,可以使用最下面提供的 get 方法拿 progressRef 值

一个 demo 看看效果,感觉还可以:

当然由于直接返回了 ref,为了防止用户篡改可以再上一层代理劫持,我们就省略了

这也算一个工具偏业务的 hook,可以根据自己的业务来进行定制,这里很多细节都没有补充只是一个示例罢了🤪

End

以上就是这篇文章的内容,记得上班之前还在想哪有那么多业务场景需要封装自定义 hook,现在发现真的是各种奇葩需求都可以封装,也算是丰富自己武器库了...


作者:討厭吃香菜
链接:https://juejin.cn/post/7449307011710894080
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

该文章在 2025/1/10 15:08:28 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2025 ClickSun All Rights Reserved