导语: 你是否有在使用 NProgress 时,遇到过进度条被某些元素遮挡的情况?尝试调整 z-index 无效?本文带你深入CSS 层叠上下文原理,直击问题根源并提供快速解决方案!
一、问题现象:进度条层级突然失效
问题描述: 当调用NProgress.done()时,进度条淡出动画部分被活跃 Tab 遮挡。
(为了方便观察,我通过一个 Demo 来演示问题现象,如下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%,我们再深入看看其中具体执行了什么逻辑?
深入 inc 和 set 方法,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 实现,问题开始浮现了。
接下来,我们先看看 NProgress 的 HTML 结构和部分 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时,会创建层叠上下文。
也就是说此时无论 bar 的 z-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; /* 提升整个容器层级 */
}
原理:在父级建立高优先级层叠上下文,提升整个容器的层级。
三、防坑指南
-
类似属性: 以下属性都会创建层叠上下文。
opacity < 1 /* 透明度 */ transform: ... /* 变换 */ filter: blur() /* 滤镜 */ position: fixed; z-index: 0 /* 定位+非auto的z-index */ -
层级检测
Chrome DevTools→More Tools→Layers面板,可三维可视化层级结构。 -
调试方法
当z-index失效时,立即检查父级是否有以下特征:
opacity < 1transform / filter属性position+z-index组合
四、原理延伸:CSS 层叠上下文
定义
层叠上下文(Stacking Context) 是 CSS 中用于管理元素在 Z 轴上堆叠顺序的三维概念。浏览器将页面元素划分为不同的“层级集团”,同一集团内的元素通过 z-index 决定堆叠顺序,不同集团则根据父级层叠上下文的层级关系排序。
简单来说,层叠上下文是一个独立的渲染层级,内部的子元素受限于该层级的规则,无法直接与其他层叠上下文中的元素比较层级高低。
特性
-
独立性 每个层叠上下文与其兄弟元素完全独立,内部子元素的堆叠顺序仅在该上下文中有效。例如:
.parent { position: relative; z-index: 1; } /* 创建层叠上下文 */ .child { z-index: 9999; } /* 仅在 .parent 内部有效 */ -
自包含性 层叠上下文内的所有子元素被视为一个整体,在父级上下文中按层级顺序堆叠。
-
层级继承性 子元素的堆叠等级受父级层叠上下文限制。例如,即使子元素的
z-index很高,若父级层级低,子元素也无法覆盖其他高层级父级中的元素。
触发条件
以下属性会触发新的层叠上下文:
-
根元素:
<html>默认创建根层叠上下文。 -
定位元素:
position值为absolute/relative/fixed/sticky且z-index不为auto。 -
CSS3 属性:
opacity < 1transform不为nonefilter不为noneflex或grid容器的子元素(z-index非auto时)isolation: isolatewill-change指定了相关属性(如opacity、transform)
五、写在最后
在使用 NProgress 时如果出现遮挡的问题时,需先确认被遮挡元素的层叠上下文是否合理:
- 如果不合理,优先解决被遮挡元素本身的层叠问题;
- 如果合理,再按照本文中的解决方案来处理;