欧美成人精品手机在线观看_69视频国产_动漫精品第一页_日韩中文字幕网 - 日本欧美一区二区

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發文檔 其他文檔  
 
網站管理員

【Web前端開發】偷偷告訴你,我們項目里的進度條,全都是假的!

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

導讀

這篇文章主要探討了項目中假進度條的實現。先介紹需求背景,然后調研了 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