十年老库 - NProgress 轻量级的进度条

8,501 阅读2分钟

介绍

NProgress 相信作为前端,大家应该都不陌生这个小玩意,它出现在页面的顶部,作为一个进度条,它默认长这样

image.png
基本上每个项目都会用到它,大大优化了用户体验,毕竟有个进度条在滚动,说明这个网页并没有停止运行,特别是对于一些需要加载大文件的项目,更需要这种进度条来不断提示用户,延长用户的耐心

原理

NProgress 最常用的 只有 2 个Api,分别是NProgress.startNProgress.done
当然还有其他Api,详细使用请戳👉NProgress Npm

在看源码之前,其实心里已经对于如何制作进度条的有想法了

  1. 简单来说,创建一个 元素fixed 在顶部
  2. 然后不断的更改 元素位置 / 宽度,这样就形成了一个不断滚动的效果
  3. 当 执行done的时候,把这个 元素 删除
    但是它作为一个经久不衰的一个库,肯定有他过人之处,看到最后,确实从这个十年前的老库中学到了不少东西

为了方便理解,一些配置量直接替换成默认值,想看完整代码,请戳👉NProgress gitHub

NProgress.start

// 默认 NProgress.status = null
  NProgress.start = function() {
    if (!NProgress.status) NProgress.set(0);

    var work = function() {
      setTimeout(function() {
        if (!NProgress.status) return;
        NProgress.trickle();
        work();
      }, 200);
    };

   work();

   return this;
  };

由于 NProgress.status = null,先执行NProgress.set

1. NProgress.set (部分)

  NProgress.set = function(n) {
      // ...
      // 保证 n 在 0.08 和 1 之间
    n = clamp(n, 0.08, 1);
    NProgress.status = (n === 1 ? null : n);
    // ...

    return this;
  };

工具函数 clamp

 function clamp(n, min, max) {
    if (n < min) return min;
    if (n > max) return max;
    return n;
 }

此时 NProgress.status 被设置成 0.08 ;
ok, 再去看执行work 函数
work 函数中 一个定时器不断的执行NProgress.trickle 函数

trickle翻译成中文是细流的意思

2. NProgress.trickle

NProgress.trickle = function() {
    return NProgress.inc();
};

得,再去找 NProgress.inc 函数

🚀 3. NProgress.inc(重要)

NProgress.inc = function(amount){
     // ...
    // NProgress.status = 0.08
     var n = NProgress.status;
     if (typeof amount !== 'number') {
        if (n >= 0 && n < 0.2) { amount = 0.1; }
        else if (n >= 0.2 && n < 0.5) { amount = 0.04; }
        else if (n >= 0.5 && n < 0.8) { amount = 0.02; }
        else if (n >= 0.8 && n < 0.99) { amount = 0.005; }
        else { amount = 0; }
      }
   
    n = clamp(n + amount, 0, 0.994);
    return NProgress.set(n);
}

这个是nProgress不断缓慢前进的关键
由于没有传入amount,typeof amount == undefined,又由于n == 0.08,所以是amount = 0.1,此时速度较快
不过随着n数值越来越大,amount 也就越来越小,增长的速度也会逐渐的慢下来 🐌
然后继续调用 NProgress.set

4. NProgress.set(补充)

NProgress.set = function(n) {
    var started = typeof NProgress.status === 'number'
    var progress = NProgress.render(!started),
       
        /* 🔥 Repaint 这一步很关键,就是为了保证 dom 渲染完毕*/
        progress.offsetWidth; 
        
        if (n === 1){
         setTimeout(function() {
             progress.style = {
                transition: 'all 200ms linear'
                opacity:0
             }
          // 移除元素
             setTimeout(function() {
               progress.parentNode.removeChild(progress)
             }, 200);

         }, 200);
       } 
   }

工具方法render

NProgress.render = function(fromStart){
// 如果已经存在nprogress,就不需要重复创建了
if (!!document.getElementById('nprogress')) return document.getElementById('nprogress');

// 如果没有创建 元素
var progress = document.createElement('div');
    progress.id = 'nprogress';
    progress.innerHTML = `<div class="bar" role="bar">
                            <div class="peg"></div>
                          </div>
                          <div class="spinner" role="spinner">
                           <div class="spinner-icon"></div>
                          </div>`;
                          
     // 设置 bar 的 translateX 距离,刚开始是 -100                    
    var bar = progress.querySelector("[role='bar']");
    var parent = document.querySelector("body");
    var perc = fromStart ? '-100' :  ((NProgress.status || 0) -1)*100;
    
    bar.style = {
      transition: 'all 0 linear',
      transform: 'translate3d(' + perc + '%,0,0)'
    }
    
    parent.appendChild(progress);
    return progress;
  }

总体流程

image.png

NProgress.done

NProgress.done = function(force) {
    if (!force && !NProgress.status) return this;
    return NProgress.inc(0.3 + 0.5 * Math.random()).set(1);
  };

可以直接调用NProgress.set(1)的,就是为了走一下过渡效果

ts

declare namespace nProgress {
  interface NProgressOptions {
      minimum: number;
      template: string;
      easing: string;
      speed: number;
      trickle: boolean;
      trickleSpeed: number;
      showSpinner: boolean;
      parent: string;
      positionUsing: string;
      barSelector: string;
      spinnerSelector: string;
  }

  interface NProgress {
      version: string;
      settings: NProgressOptions;
      status: number | null;

      configure(options: Partial<NProgressOptions>): NProgress;
      set(number: number): NProgress;
      isStarted(): boolean;
      start(): NProgress;
      done(force?: boolean): NProgress;
      inc(amount?: number): NProgress;
      trickle(): NProgress;

      /* Internal */

      render(fromStart?: boolean): HTMLDivElement;
      remove(): void;
      isRendered(): boolean;
      getPositioningCSS(): 'translate3d' | 'translate' | 'margin';
  }
}

declare const nProgress: nProgress.NProgress;

🍎总结

这个NProgress 库虽然比较老,但是他的函数式编程和对Dom 的操作依旧是很值得学习的,特别是它的函数方法的编写及使用,基本做到了一个方法只做一件事,代码结构相当清晰