NProgress 解析

1,068 阅读5分钟

NProgress

Slim progress bars for Ajax'y applications. Inspired by Google, YouTube, and Medium.

在很多项目中,都需要用到NProgress
它的主要用途是在顶部渲染一个不断前进的进度条,可以让用户更好的使用体验

类似于下图
image.png

npm地址
git地址

使用

官方提供的方法一共的有 start,set,done,inc,其中常用是startdone
很明显,start 就是初始化执行,done是执行完毕

NProgress.start();
NProgress.done();
$(document).on('page:fetch',   function() { NProgress.start(); });
$(document).on('page:change',  function() { NProgress.done(); });
$(document).on('page:restore'function() { NProgress.remove(); });

今天主要是startdone这两个常用方法的原理

屏幕顶部一个移动元素 bar,屏幕顶部右侧一个 spin
可以通过 start 开始移动这个 bar,同时 spin 旋转
也可以通过 set 设置一个初始值 -- 即bar 的初始位移距离
到最后的 done 都是 bar 的 宽度为 屏幕宽度,spin 消失,bar 消失

Api

start

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

    // NProgress.trickle = function() {

    // return NProgress.inc();

    // };

    var work = function() {
      setTimeout(function() {

        if (!NProgress.status) return;

        NProgress.trickle();

        work();

      }, Settings.trickleSpeed);

    };

    // 初始执行
    if (Settings.trickle) work();

     return this;

  };

主要聚焦于 work这个函数,如果有Settings.trickle 这个属性的话,执行work,在work中开启一个定时器,不断的执行NProgress.tricklework这个函数,直到NProgress.statusfalse
刚开始statusnull,执行了NProgress.set(0)
set 在这个时候做了什么

 NProgress.set = function(n) {
     ...
     // Settings.minimum  = 0.08
      n = clamp(n, Settings.minimum, 1);
     NProgress.status = (n === 1 ? null : n);
     ...
 }

可以看出,在 set 函数中 NProgress.status被置成了数字0.08
从上述可以得出三点结论

  1. NProgress.status是判断是否停止执行的依据,为 truthyset0,为falsy就 停止执行 work
  2. NProgress.trickle是不断推进NProgress中的bar前进的入口函数
  3. NProgress.status 被设置为数字 0.08
    接下来看NProgress.trickle做了什么
NProgress.trickle = function() {
    return NProgress.inc();
  };

返回了一个inc函数,看来inc不止在这个地方用到了,否则也不会再次封装一层了

NProgress.inc

NProgress.inc = function(amount) {
    var n = NProgress.status;
    if (!n) {
      return NProgress.start();
    } else if(n > 1) {
      return;
    } else {
      // n 越大越慢 
      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);
    }
  };

可以看出在start的时候,没有传递amountamountundefined,NProgress.statusset的时候,被置换成0.08
NProgress.status越小,amount被设置的越大,n + amount 越大,增长的越快

如果传递了amount,那么就用固定的amount

到最后把amount + status 的和传递给函数NProgress.set

NProgress.set 😨

在函数start的时候,并且statusnull的时候,执行过一次set函数,现在需要再次执行一次,(其实每次只要执行work函数,都要执行set)

set 是主要作用是通过传入的参数n设置bar位移的距离,如果参数n已经是 1 了,就把原来的barspin删除

NProgress.set = function(n) {
// NProgress.isStarted = function() {
//    return typeof NProgress.status === 'number';
// }; 
     var started = NProgress.isStarted();
    n = clamp(n, Settings.minimum, 1);
    
      NProgress.status = (n === 1 ? null : n);
      
      // 生成了一个元素
      var progress = NProgress.render(!started),
    // barSelector: '[role="bar"]',
        bar      = progress.querySelector(Settings.barSelector),
        speed    = Settings.speed,
        ease     = Settings.easing;

    // 为了获取最新的DOM
    progress.offsetWidth; /* Repaint */

    queue(function(next) {
      // 判断是用 `translate` 还是 `margin` 控制
      if (Settings.positionUsing === '') Settings.positionUsing = NProgress.getPositioningCSS();

      // 根据上文的 Settings.positionUsing 是 translate 还是 marginLeft,来添加对应的数值
      // barCSS = { transform: 'translate('+toBarPerc(n)+'%,0)' };或者
      //  barCSS = { 'margin-left': toBarPerc(n)+'%' };
      
      // function toBarPerc(n) {
     //    return (-1 + n) * 100;
     // }
      css(bar, barPositionCSS(n, speed, ease));

      if (n === 1) {
        // Fade out
        css(progress, {
          transition: 'none',
          opacity: 1
        });
        
        progress.offsetWidth; /* Repaint */
        setTimeout(function() {
          css(progress, {
            transition: 'all ' + speed + 'ms linear',
            opacity: 0
          });
          
          setTimeout(function() {
            NProgress.remove();
            next();
          }, speed);

        }, speed);
      } else {
        setTimeout(next, speed);
      }
    });

    return this;
}
  1. NProgress.status改为数字
  2. 使用NProgress.render生成dom元素 progress
  3. 使用函数queue进行顺序调用
  4. 根据浏览器的支持程度来判断 Settings.positionUsingtranslate3dtranslate还是margin-left
  5. barPositionCSS 根据上文的Settings.positionUsing 得出css属性,并交由css函数执行
  6. 如果n 已经是1了,需要把元素隐藏掉,如果没有执行 next 函数 其他的都容易理解,主要是这个 queue 函数

queue

接收一个函数,通过内部的 pending数组和next函数,不断地调用

var queue = (function() {
    var pending = [];

    function next() {
      var fn = pending.shift();
      if (fn) {
        fn(next);
      }
    }

    return function(fn) {
      pending.push(fn);
      if (pending.length == 1) next();
    };
  })();

简化版

// set 函数中的 setTimeout(next, speed);
let count = 0
    function a(next) {
      setTimeout(() => {
        console.log("count",count++)
      }, 2000)
    }
    
    // 相当于 work函数 内部不断的递归调用自己
    setTimeout(() => {
      queue(a)
      setTimeout(() => {
        queue(a)
        setTimeout(() => {
          queue(a)
          setTimeout(() => {
            queue(a)
          }, 1000)
        }, 1000)
      }, 1000)
    }, 1000)

不过经过试验setTimeout(next, speed); 这句代码有没有都一样,暂时不知道有什么特别的用处
甚至于说,没有queue 也能很好地运行
说一下 NProgress.render,生成dom的函数

NProgress.render 🏊‍

生成真实DOM元素,根据用户配置动态调节元素的css值,通过模板自动生成需要的元素

NProgress.render = function(fromStart) {
    addClass(document.documentElement, 'nprogress-busy');
    // 创建 progress 元素
    var progress = document.createElement('div');
    progress.id = 'nprogress';
    
    // Settings.template = 
    
    // '<div class="bar" role="bar">
    //       <div class="peg"></div>
    //     </div>

    //    <div class="spinner" role="spinner">
    //      <div class="spinner-icon"></div>
    //    </div>'
    progress.innerHTML = Settings.template;
    
     // barSelector: '[role="bar"]',
    var bar = progress.querySelector(Settings.barSelector),
    
    // function toBarPerc(n) {
    //   return (-1 + n) * 100;
    // }
    // 判断是否是从最开始执行,status 也就是执行比例,比如 0.4,或者0.6
     perc = fromStart ? "-100" : toBarPerc(NProgress.status || 0),
     
     //  Settings.parent: "body",
      parent = isDOM(Settings.parent)
        ? Settings.parent
        : document.querySelector(Settings.parent),
        // 定义 右上角 不断旋转的小元素
      spinner;
      
      // 给元素`bar` 添加 属性
      css(bar, {
          transition: "all 0 linear",
          transform: "translate3d(" + perc + "%,0,0)",
    });
    
    // spinnerSelector: '[role="spinner"]',因为已经 progress.innerHTML 已经生成了 spinner
    // 如果配置了 showSpinner 为 false,需要移除元素
      if (!Settings.showSpinner) {
          spinner = progress.querySelector(Settings.spinnerSelector);
          spinner && removeElement(spinner);
    }
    
    /* 如果用户更改了 父元素,那么用户传进来的这个元素要加上 css
    .nprogress-custom-parent {
      overflow: hidden;
       position: relative;
    }

     .nprogress-custom-parent #nprogress .spinner,
     .nprogress-custom-parent #nprogress .bar {
         position: absolute;
     }
     */
    
    /* 如果是父元素是 body 的话,使用 fixed 相对于视口
         #nprogress .spinner {
              display: block;
              position: fixed;
              z-index: 1031;
              top: 15px;
              right: 15px;
        }

        #nprogress .bar {
          background: blue;

          position: fixed;
          z-index: 1031;
          top: 0;
          left: 0;

          width: 100%;
          height: 2px;
        }
        */
    
     if (parent != document.body) {
       addClass(parent, "nprogress-custom-parent");
     }
    
    parent.appendChild(progress);
    return progress;
}

done 😊

结束函数,先把 比例提到0.3 - 0.8之间,在把set 设置成 1,set 有一个定时器,在一个定时器时间内只执行一次,并且销毁 dom

 NProgress.done = function (force) {
    if (!force && !NProgress.status) return this;

    return NProgress.inc(0.3 + 0.5 * Math.random()).set(1);
  };

set传入 参数为 n =1 的部分

NProgress.set = function (n) {
 // NProgress.isStarted = function() {
 //   return typeof NProgress.status === 'number';
 // };
    var started = NProgress.isStarted();
    NProgress.status = n === 1 ? null : n;
     if (n === 1) {
         css(progress, {
            transition: 'all ' + speed + 'ms linear',
            opacity: 0
          });
          setTimeout(function() {
            NProgress.remove();
            next();
          }, speed);
    }
}

工具函数

clamp

参数 n 限制在 minmax 之间

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

css

给元素添加属性
example: css(div,{ transition: 'all ' + speed + 'ms linear', opacity: 0 })

var css = (function(){
cssProps    = {};
// 第一步:给element 添加getStyleProp(prop) 返回的 prop 为 value
function applyCss(element, prop, value) {
     prop = getStyleProp(prop);
      element.style[prop] = value;
    }
    
     function getStyleProp(name) {
      name = camelCase(name);
      // 做一层属性缓存,如果 cssProp存在,直接使用cssProp
      return cssProps[name] || (cssProps[name] = getVendorProp(name));
    }
    
    
    function getVendorProp(name) {
    // 不使用原来存在 body 上的 属性
      var style = document.body.style;
      if (name in style) return name;

      var i = cssPrefixes.length,
          capName = name.charAt(0).toUpperCase() + name.slice(1),
          vendorName;
          
      while (i--) {
        vendorName = cssPrefixes[i] + capName;
        if (vendorName in style) return vendorName;
      }

      return name;
    }
    
    return function(element, properties) {
      var args = arguments,
          prop,
          value;
          
    // 判断实参个数是否有二个
      if (args.length == 2) {
        for (prop in properties) {
          value = properties[prop];
          if (value !== undefined && properties.hasOwnProperty(prop)){
              applyCss(element, prop, value);
          } 
        }
      } else {
        applyCss(element, args[1], args[2]);
      }
    }
})()

遍历传入的参数properties,给element添加上属性, 通过var style = document.body.style,如果属性在 body上已经存在,即在sytle上,就不添加,如果不存在就添加

ts

nprogress是js,没有类型提示,所以需要 给 nprogress 添加 ts 类型 git地址


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;
export = nProgress;

最后

从这个NProgress中学习到了

  1. NProgress 的其他用法
  2. 使用自执行函数保护变量
  3. 使用html模板配置dom元素
  4. 使用 ts 给 js 添加类型

time:2022/10/14 3:41