狠狠色丁香婷婷综合尤物/久久精品综合一区二区三区/中国有色金属学报/国产日韩欧美在线观看 - 国产一区二区三区四区五区tv

LOGO OA教程 ERP教程 模切知識(shí)交流 PMS教程 CRM教程 開(kāi)發(fā)文檔 其他文檔  
 
網(wǎng)站管理員

【W(wǎng)eb前端開(kāi)發(fā)】偷偷告訴你,我們項(xiàng)目里的進(jìn)度條,全都是假的!

admin
2025年1月10日 15:8 本文熱度 61

導(dǎo)讀

這篇文章主要探討了項(xiàng)目中假進(jìn)度條的實(shí)現(xiàn)。先介紹需求背景,然后調(diào)研了 NProgress 和 fake-progress 兩個(gè)方案,分析其源碼和特點(diǎn)。接著提到因現(xiàn)有方案的不足,作者萌生封裝自己的 hook(useFakeProgress)的想法,確定方案和入?yún)ⅲo出實(shí)現(xiàn)細(xì)節(jié)和示例,強(qiáng)調(diào)可根據(jù)業(yè)務(wù)定制。

扯皮

最近接到了一個(gè)需求:前端點(diǎn)擊按鈕觸發(fā)某個(gè)任務(wù)并開(kāi)啟輪詢獲取任務(wù)進(jìn)度,直至 100% 任務(wù)完成后給予用戶提示

這個(gè)業(yè)務(wù)場(chǎng)景還挺常見(jiàn)的,但是突然上周后端聯(lián)系到我說(shuō)現(xiàn)在的效果有點(diǎn)差,之前都是小任務(wù)那進(jìn)度條展示還挺不錯(cuò)的,現(xiàn)在有了一些大任務(wù)且會(huì)存在排隊(duì)阻塞的情況,就導(dǎo)致視圖上經(jīng)常卡 0% 排隊(duì),用戶體驗(yàn)太差了,問(wèn)能不能在剛開(kāi)始的時(shí)候做個(gè)假進(jìn)度先讓進(jìn)度條跑起來(lái)??

因此就有了這篇文章,簡(jiǎn)單做一下技術(shù)調(diào)研以及在項(xiàng)目中的應(yīng)用

正文

其實(shí)假進(jìn)度條也不難做,無(wú)非是輪詢的時(shí)候我們自己做一個(gè)隨機(jī)的自增,讓它卡到 99% 等待后端真實(shí)進(jìn)度完成后再結(jié)束

只不過(guò)還是想調(diào)研一下看看市面上有沒(méi)有一些成熟的方案并去扒一下它們的源碼??

NProgress

首先當(dāng)我聽(tīng)到這里的需求后第一時(shí)間想到的就是它:rstacruz/nprogress: For slim progress bars like on YouTube, Medium, etc

記得大學(xué)期間做的一些中后臺(tái)系統(tǒng)基本都少不了路由跳轉(zhuǎn)時(shí)的頂部進(jìn)度條加載,那時(shí)候就有了解到 NProgress,它的使用方式也很簡(jiǎn)單,完全手控:NProgress: slim progress bars in JavaScript,去文檔里玩一下就知道了

視圖呈現(xiàn)的效果就是如果你不手動(dòng)結(jié)束那它就會(huì)一直緩慢前進(jìn)卡死 99% ,挺符合我們這里的需求,可以去扒一下它內(nèi)部進(jìn)度計(jì)算相關(guān)的邏輯

NProgress 的內(nèi)容實(shí)際上比較少,源碼拉下來(lái)可以看到主要都在這一個(gè) JS 文件里了:

需要注意的是我們看的是這個(gè)版本:rstacruz/nprogress at v0.2.0,master 分支與 npm 安裝的 0.2.0 內(nèi)部實(shí)現(xiàn)還是有些差別的

我們這里不關(guān)注它的樣式相關(guān)計(jì)算,主要來(lái)看看對(duì)進(jìn)度的控制,直奔 start 方法:

還是比較清晰的,這里的 status 就是內(nèi)部維護(hù)的進(jìn)度值,默認(rèn)為 null,所以會(huì)執(zhí)行 NProgress.set,我們?cè)賮?lái)看看 set 方法:

set 方法里有一大堆設(shè)置動(dòng)畫(huà)樣式邏輯都被我剪掉了,關(guān)于進(jìn)度相關(guān)的只有這些。相當(dāng)于利用 clamp 來(lái)做一個(gè)夾層,因?yàn)槌跏歼M(jìn)來(lái)的 n 為 null,所以經(jīng)過(guò)處理后進(jìn)度變?yōu)?0.08

再回到 start 的邏輯,其中 work 就是內(nèi)部輪詢控制進(jìn)度自增的方法了,初始配置 trickle 為 true 代表自動(dòng)開(kāi)啟進(jìn)度自增,由于進(jìn)度條在 set 方法中已經(jīng)設(shè)置為 0.08,所以走到后面的 NProgress.trickle 邏輯

看來(lái)這里就是進(jìn)度控制的核心邏輯了, trickle 里主要調(diào)用了 inc,在 trickle 中給 inc 傳遞了一個(gè)參數(shù):Math.random() * Settings.trickleRate,顯然這里范圍是:0 <= n < 0.02

而在 inc 中,如果傳遞的 amount 有值的話那就每次以該值進(jìn)行自增,同時(shí)又使用 clamp 將最大進(jìn)度卡在 0.994

最后再調(diào)用 set 方法,set 里才是更新進(jìn)度和視圖進(jìn)度條的方法,涉及到進(jìn)度更新時(shí)都需要回到這里

當(dāng)然 NProgress.inc 也可以手動(dòng)調(diào)用,還對(duì)未傳參做了兼容處理:

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

即根據(jù)當(dāng)前進(jìn)度 n 計(jì)算剩余進(jìn)度,再隨機(jī)生成自增值

再來(lái)看 done 方法,它就比較詭異了:

按理來(lái)說(shuō)直接將進(jìn)度設(shè)置為 1 就行,但它以鏈?zhǔn)秸{(diào)用 inc 再調(diào)用 set,相當(dāng)于調(diào)用了兩次 set

而這里 inc 傳參又沒(méi)什么規(guī)律性,推測(cè)是為了 set 中的樣式處理,感興趣的可以去看看那部分邏輯,還挺多的...??

一句話總結(jié)一下 NProgress 的進(jìn)度計(jì)算邏輯:隨機(jī)值自增,最大值限制

但是因?yàn)?NProgress 與進(jìn)度條樣式強(qiáng)綁定,我們肯定是沒(méi)法直接用的

fake-progress

至于 fake-progress 則是我在調(diào)研期間直接搜關(guān)鍵詞搜出來(lái)的??:piercus/fake-progress: Fake a progress bar using an exponential progress function

很明顯看介紹就是干這個(gè)事的,而且還十分專業(yè),引入數(shù)學(xué)函數(shù)展示假進(jìn)度條效果,具有說(shuō)服力:

所以我們項(xiàng)目中其實(shí)就是用的這個(gè)包,只不過(guò)和 NProgress 類似,兩個(gè)包都比較老了,瞟一眼源碼發(fā)現(xiàn)都是老 ES5 了??

因?yàn)槲覀冺?xiàng)目中用的是 React,這里給出 React 的 demo 吧,為了編寫(xiě)方便使用了幾個(gè) ahooks 里的 hook:

其實(shí)使用方法上與 NProgress 都類似,不過(guò)兩者都屬于通用的工具庫(kù)不依賴其他框架,所以像視圖渲染都需要自己手動(dòng)來(lái)做

注意實(shí)例化中的傳參 timeConstant,某種意義上來(lái)講這個(gè)值就相當(dāng)于“進(jìn)度增長(zhǎng)的速率”,但也不完全等價(jià),我們來(lái)看看源碼

因?yàn)椴簧婕暗綐邮剑琭ake-progress 源碼更簡(jiǎn)單,核心就在這里:

下方的數(shù)學(xué)公式就是介紹圖中展示的,只能說(shuō)剛看到這部分內(nèi)容是真的是死去的數(shù)學(xué)只是突然又開(kāi)始攻擊我??,寫(xiě)了那么多函數(shù),數(shù)學(xué)函數(shù)是啥都快忘了

我們來(lái)簡(jiǎn)單分析一下這個(gè)函數(shù) 1 - Math.exp(-1 * x),exp(x)= exex 的圖像長(zhǎng)這樣,高中的時(shí)候見(jiàn)的太多了:

那假如這里改成 exp(-x) 呢?有那味了,以前應(yīng)該是有一個(gè)類似的公式 f(x) 與 f(?x) 圖像效果是關(guān)于 y 軸對(duì)稱,好像是有些特殊的不符合這個(gè)規(guī)律???反正大部分都是滿足的

OK,那我們繼續(xù)進(jìn)行轉(zhuǎn)換,看看 -exp(-x) 的效果

同樣有個(gè)公式 f(x) 與 ?f(x) 圖像效果是關(guān)于 x 軸對(duì)稱:

初見(jiàn)端倪,不知道你們有沒(méi)有注意 -exp(-x) 最終呈現(xiàn)的圖像是無(wú)限接近于 x 軸的,也就是 0:

那有了??,假如我再給它加個(gè) 1 呢?它不就無(wú)限接近于 1 了,即 -exp(-x) + 1,這其實(shí)就是 fake-progress  里公式的由來(lái):

但你會(huì)發(fā)現(xiàn)如果 x 按 1 遞增就很快進(jìn)度就接近于 1 了,所以有了 timeConstant 配置項(xiàng)來(lái)控制 x 的增長(zhǎng),回看這個(gè)公式:1 - Math.exp(-1 * this._time / this.timeConstant)

this._time 是一直在增長(zhǎng)的,而 this.timeConstant 作為分母如果被設(shè)置為一個(gè)較大的值,那可想而知進(jìn)度增長(zhǎng)會(huì)巨慢

所以 fake-progress 的核心原理是借助數(shù)學(xué)函數(shù),以函數(shù)值無(wú)限接近于 1 來(lái)實(shí)現(xiàn)假進(jìn)度條,但是這種實(shí)現(xiàn)有一個(gè) bug,可以看我提的這個(gè) issues,不過(guò)看這個(gè)包的更新時(shí)間感覺(jué)作者也不會(huì)管了??:

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

useFakeProgress

雖然我們現(xiàn)在項(xiàng)目中使用的是 fake-progress,但是個(gè)人感覺(jué)用起來(lái)十分雞肋,而且上面的 bug 也需要自己手動(dòng)兼容,因此萌生出自己封裝一個(gè) hook 的想法,讓它更符合業(yè)務(wù)場(chǎng)景

首先我們確定一下進(jìn)度計(jì)算方案,這里我毫不猶豫選擇的是 NProgress 隨機(jī)值增長(zhǎng)方案,為什么?因?yàn)榉奖阌脩糇远x

而且 NProgress 相比于 fake-progress 有一個(gè)巨大優(yōu)勢(shì):手動(dòng) set 進(jìn)度后仍然保持進(jìn)度正常自動(dòng)遞增

這點(diǎn)在 fake-progress 中實(shí)現(xiàn)是比較困難的,因?yàn)槟銦o(wú)法保證手動(dòng) set 的進(jìn)度是在這個(gè)函數(shù)曲線上,相當(dāng)于給出函數(shù) y 值反推 x 值,根據(jù)反推的 x 值再進(jìn)行遞增,想想都麻煩

確定好方案后我們來(lái)看下入?yún)桑瑓⒖?NProgress 我定義了這幾個(gè)配置項(xiàng):

這里我簡(jiǎn)單解釋一下 rerender 和 amount 配置:

實(shí)際上在封裝這個(gè) hook 的時(shí)候我一直在糾結(jié)這里的 progress 到底是 state 還是 ref,因?yàn)榇蠖鄶?shù)場(chǎng)景下 hook 內(nèi)部通過(guò)輪詢定時(shí)器更新進(jìn)度,而真實(shí)業(yè)務(wù)代碼中也會(huì)開(kāi)啟定時(shí)器去輪詢監(jiān)聽(tīng)業(yè)務(wù)接口的

所以如果寫(xiě)死為 state,那這個(gè)場(chǎng)景 hook 內(nèi)部的每次更新 render 是沒(méi)必要的,但是假如用戶又想只是使用假進(jìn)度展示,沒(méi)有后端業(yè)務(wù)接口呢?

思來(lái)想去其實(shí)完全可以放權(quán)給用戶進(jìn)行配置,因?yàn)?nbsp;state = ref + update,統(tǒng)一使用 ref,用戶配置 rerender 時(shí)我們?cè)诿看胃聲r(shí) update 即可

至于 amount 我是希望放權(quán)給用戶進(jìn)行自定義遞增值,你可以配置成一個(gè)固定值也可以配置成隨機(jī)值,更可以像 NProgress master 分支下這樣根據(jù)當(dāng)前進(jìn)度來(lái)控制自增,反正以函數(shù)參數(shù)的形式能夠拿到當(dāng)前的 progress:

至于實(shí)現(xiàn)細(xì)節(jié)就不再講述了,實(shí)際上就是輪詢定時(shí)器沒(méi)什么復(fù)雜的東西,直接上源碼了:

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 后結(jié)果

  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,

    },

  ];

}

這里需要補(bǔ)充一個(gè)細(xì)節(jié),在返回值里使用的是 progressDataRef 是 formatter 后的結(jié)果為 string 類型,如果用戶想要獲取原 number 的 progress,可以使用最下面提供的 get 方法拿 progressRef 值

一個(gè) demo 看看效果,感覺(jué)還可以:

當(dāng)然由于直接返回了 ref,為了防止用戶篡改可以再上一層代理劫持,我們就省略了

這也算一個(gè)工具偏業(yè)務(wù)的 hook,可以根據(jù)自己的業(yè)務(wù)來(lái)進(jìn)行定制,這里很多細(xì)節(jié)都沒(méi)有補(bǔ)充只是一個(gè)示例罷了??

End

以上就是這篇文章的內(nèi)容,記得上班之前還在想哪有那么多業(yè)務(wù)場(chǎng)景需要封裝自定義 hook,現(xiàn)在發(fā)現(xiàn)真的是各種奇葩需求都可以封裝,也算是豐富自己武器庫(kù)了...


作者:討厭吃香菜
鏈接:https://juejin.cn/post/7449307011710894080
來(lái)源:稀土掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。

該文章在 2025/1/10 15:08:28 編輯過(guò)
關(guān)鍵字查詢
相關(guān)文章
正在查詢...
點(diǎn)晴ERP是一款針對(duì)中小制造業(yè)的專業(yè)生產(chǎn)管理軟件系統(tǒng),系統(tǒng)成熟度和易用性得到了國(guó)內(nèi)大量中小企業(yè)的青睞。
點(diǎn)晴PMS碼頭管理系統(tǒng)主要針對(duì)港口碼頭集裝箱與散貨日常運(yùn)作、調(diào)度、堆場(chǎng)、車隊(duì)、財(cái)務(wù)費(fèi)用、相關(guān)報(bào)表等業(yè)務(wù)管理,結(jié)合碼頭的業(yè)務(wù)特點(diǎn),圍繞調(diào)度、堆場(chǎng)作業(yè)而開(kāi)發(fā)的。集技術(shù)的先進(jìn)性、管理的有效性于一體,是物流碼頭及其他港口類企業(yè)的高效ERP管理信息系統(tǒng)。
點(diǎn)晴WMS倉(cāng)儲(chǔ)管理系統(tǒng)提供了貨物產(chǎn)品管理,銷售管理,采購(gòu)管理,倉(cāng)儲(chǔ)管理,倉(cāng)庫(kù)管理,保質(zhì)期管理,貨位管理,庫(kù)位管理,生產(chǎn)管理,WMS管理系統(tǒng),標(biāo)簽打印,條形碼,二維碼管理,批號(hào)管理軟件。
點(diǎn)晴免費(fèi)OA是一款軟件和通用服務(wù)都免費(fèi),不限功能、不限時(shí)間、不限用戶的免費(fèi)OA協(xié)同辦公管理系統(tǒng)。
Copyright 2010-2025 ClickSun All Rights Reserved