【源码分析】NProgress 进度条遮挡 —— Opacity 新建层叠上下文

169 阅读5分钟

导语: 你是否有在使用 NProgress 时,遇到过进度条被某些元素遮挡的情况?尝试调整 z-index 无效?本文带你深入CSS 层叠上下文原理,直击问题根源并提供快速解决方案!

一、问题现象:进度条层级突然失效

问题描述: 当调用NProgress.done()时,进度条淡出动画部分被活跃 Tab 遮挡。

(为了方便观察,我通过一个 Demo 来演示问题现象,如下GIF图) ezgif-4f17f1030f1f98.gif

问题分析过程:

首先,当前激活的Tab(Tab 3)被遮挡,是不是激活状态下层级过高导致?

通过快速验证发现,不是。之所以只有当前激活 Tab 被遮挡只是因为 Tab 默认状态下,其背景颜色是透明的;如果背景是非透明颜色,同样存在遮挡问题。

继续分析:进度条是在 NProgress.done 之后失效,是否与 NProgress.done 逻辑相关?

先抛出结论:是的。具体原因我们继续往下分析;

首先,我们需要深入 NProgress 源码,看看 NProgress.done 都干了些什么?

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

NProgress.done 将进度设置为 100%,我们再深入看看其中具体执行了什么逻辑?

深入 incset 方法,inc 方法可以让动画过渡更自然,但这显然不会产生层叠的问题;

  NProgress.inc = function(amount) {
    var n = NProgress.status;
    if (!n) {
        return NProgress.start();
    } else {
      if (typeof amount !== 'number') {
        amount = (1 - n) * clamp(Math.random() * n, 0.1, 0.95);
      }
      n = clamp(n + amount, 0, 0.994);
      return NProgress.set(n);
    }
  };

我们再深入看看 set 方法,选取关键片段(省略部分无关代码):

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

根据以上代码片段,当 n === 1 时,也就是执行 Nprogress.done 时,设置进度条的淡出效果。

通过 opacity: 1 -> opacity: 0 实现,问题开始浮现了。

接下来,我们先看看 NProgressHTML 结构和部分 CSS 样式。

上面的progress 就是 NProgress 的根元素, HTML 模板的结构如下:

<div id="nprogress">
  <div class="bar" role="bar">
    <div class="peg"></div>
  </div>
  <div class="spinner" role="spinner">
    <div class="spinner-icon"></div>
  </div>
</div>

下面是部分 CSS 样式,其中元素的 z-index 的设置在 bar 元素上;

#nprogress {
  pointer-events: none;
}
#nprogress .bar {
  background: #29d;
  position: fixed;
  z-index: 1031;
  top: 0;
  left: 0;
  width: 100%;
  height: 2px;
}

❌ 看似合理的代码,却暗藏玄机!

二、层叠上下文原理

1. 什么是层叠上下文?

浏览器将元素划分到不同的"层级集团"(层叠上下文),同一集团内通过z-index排序,不同集团则按父级层级排序。

2. Opacity的隐藏特性

熟读 W3C 规范的同学都知道:W3C 规范明确指出,当元素 opacity < 1时,会创建层叠上下文。

也就是说此时无论 barz-index 多高,当opacity < 1时,其层级都只在 progress 中有效,无法突破到更高层级的上下文集团。

二、解决方案:

方案一:使用 rgba 替代 opacity (源码修改)
#nprogress .bar {
    background: rgba(34, 153, 221, 1)
}
  NProgress.set = function(n) {
    ...
    queue(function(next) {
      ...
      if (n === 1) {
        css(progress, {  transition: 'none',  background: 'rgba(34, 153, 221, 1)' });
        progress.offsetWidth; /* Repaint */
        setTimeout(function() {
          css(progress, { transition: 'all ' + speed + 'ms linear',  background: 'rgba(34, 153, 221, 0)'  });
          ...
        }, speed);
      }
      ...
    });
    return this;
  };

原理: 通过rgba实现透明度,避免触发新的层叠上下文。

方案二:层级升级(样式覆盖)
#nprogress {
    position: relative;
    z-index: 1031; /* 提升整个容器层级 */
}

原理:在父级建立高优先级层叠上下文,提升整个容器的层级。

三、防坑指南

  1. 类似属性: 以下属性都会创建层叠上下文。

    opacity < 1      /* 透明度 */
    transform: ...   /* 变换 */
    filter: blur()   /* 滤镜 */
    position: fixed; z-index: 0 /* 定位+非auto的z-index */
    
  2. 层级检测

    Chrome DevToolsMore ToolsLayers 面板,可三维可视化层级结构。

  3. 调试方法

    当z-index失效时,立即检查父级是否有以下特征:

    • opacity < 1
    • transform / filter 属性
    • position + z-index 组合

四、原理延伸:CSS 层叠上下文

定义

层叠上下文(Stacking Context) 是 CSS 中用于管理元素在 Z 轴上堆叠顺序的三维概念。浏览器将页面元素划分为不同的“层级集团”,同一集团内的元素通过 z-index 决定堆叠顺序,不同集团则根据父级层叠上下文的层级关系排序。

简单来说,层叠上下文是一个独立的渲染层级,内部的子元素受限于该层级的规则,无法直接与其他层叠上下文中的元素比较层级高低。

特性

  1. 独立性 每个层叠上下文与其兄弟元素完全独立,内部子元素的堆叠顺序仅在该上下文中有效。例如:

    .parent { position: relative; z-index: 1; } /* 创建层叠上下文 */
    .child { z-index: 9999; } /* 仅在 .parent 内部有效 */
    
  2. 自包含性 层叠上下文内的所有子元素被视为一个整体,在父级上下文中按层级顺序堆叠。

  3. 层级继承性 子元素的堆叠等级受父级层叠上下文限制。例如,即使子元素的 z-index 很高,若父级层级低,子元素也无法覆盖其他高层级父级中的元素。

触发条件

以下属性会触发新的层叠上下文:

  1. 根元素<html> 默认创建根层叠上下文。

  2. 定位元素position 值为 absolute/relative/fixed/sticky  z-index 不为 auto

  3. CSS3 属性

    • opacity < 1
    • transform 不为 none
    • filter 不为 none
    • flex 或 grid 容器的子元素(z-index 非 auto 时)
    • isolation: isolate
    • will-change 指定了相关属性(如 opacitytransform

五、写在最后

在使用 NProgress 时如果出现遮挡的问题时,需先确认被遮挡元素的层叠上下文是否合理:

  1. 如果不合理,优先解决被遮挡元素本身的层叠问题;
  2. 如果合理,再按照本文中的解决方案来处理;