本篇文章介绍了一个在开发中遇到的诡异的问题,排查问题过程颇为艰辛,不过最终结果还是值得的,因为巩固了一些基础知识和好的调试方法,它们是:
- fixed定位的特点
- 样式优先级的判定
- 如何使用浏览器的DOM还原一闪即逝的错误效果
1. 问题说明
在猎萝卜-猎头端,测试提出这样一个bug(禅道编号12828):
在猎萝卜的 订单 - 我的职位 - 已接单 入口,点击右侧职位列表的任意职位,在弹出职位预览之前(图2),会先看到职位详情的底部串到左下角位置(图1)
图1 异常状态 图2 回到正常的状态2. 调用过程
线框说明图1 线框说明图1首先介绍下触发整个动作的逻辑,为了方便理解,借用上面的线框图来说明:
-
projectList.vue
是职位列表,其中每个职位对应的条目封装成了order-project-item.vue
,点击order-project-item.vue
后,会触发事件全局的expandOrderPreview
-
order-project-preview.vue
作为一个常驻在界面右侧的组件,默认是收起在屏幕右侧的,在侦听到了expandOrderPreview
事件以后,会从右侧弹出 -
next-bar-affix.vue
作为order-project-preview.vue
底部的子组件,会一起出现和消失
3. 具体的代码逻辑
在下图中的逻辑代码,整理成如下描述:
order-project-item.vue
触发点击事件expandOrderPreview
,被order-project-preview.vue
侦听到,然后进行如下处理:
-
第一步
emit showOrderPreview
事件- 父级
app-frame.vue
收到showOrderPreview
事件,给引用的order-project-preview.vue
添加一个.previewing
的样式 -
.previewing
会使order-project-preview.vue
的宽度从0变到特定宽度从而在右侧展示
- 父级
-
第二步 调用
refresh
方法- 将
loading
设置为2,order-project-preview.vue
中详情面板受到loading==0
的v-if
控制,因此会隐藏;loading组件发现loading>0
因此会有加载效果 - 异步调用
getProjectDetail
和getOrders
这两个方法,直到结果返回,loading
值会递减至0,详情才开始正常展示,加载效果才会去除
- 将
4. 一些基础知识
解决问题之前,通过查看样式代码,我们巩固了一些基础知识。
如下是抽取出来的DOM结构和样式设置:
<div class="app-frame">
<div class="order-project-preview">
<div class="next-bar-affix">
<div class="h-affix" style="bottom:0"></div>
</div>
</div>
</div>
.order-project-preview {
position: fixed;
top: 0;
bottom:0;
right: 0;
width: 300px;
}
.next-bar-affix {
position: fixed;
bottom: 0;
}
.next-bar-affix .h-affix {
position: fixed;
height: 60px;
}
-
.order-project-preview
进行position: fixed
设置后,DOM的位置会相对视口固定,通过left
,right
,top
,bottom
来设置定位 - 在上述示例代码中,
.order-project-preview
宽度300px
,高度撑满屏幕,底部栏.next-bar-affix
底部内容区的高度为30px
- 其中
.next-bar-affix
部分的代码是由heyui
的affix
组件生成的,一个有意思的启发是,在.h-affix
中也添加了一个bottom:0
的属性。可以尝试将该属性去除,会发现底部的内容区域会跑到最底部之下,变得不可见。 - 综上,可以从上述效果图推测的一个结论是:
如果一个子容器设置了
fixed
定位,它的父容器也是fixed
,那么在不指定子容器的left
,right
,top
,bottom
时,子容器的边界会以父容的top
left
作为边界。进一步可以推断,在我们的问题中,必然是有什么因素使得作为父容器的.order-project-preview
失去了作为子容器定位基准的条件**,从而跑到了视口的左下角。
5. 具体推测
做过的一些不靠谱的推测:一个DOM样式的变化 A->A+B
实际上是 A -> 无 -> A+B
,即order-project-preview.vue
增加 .previewing
的过程中,容器样式到无
的中间过程暂时使得next-bar-affix.vue
丢失了left
的基准,从而导致在中过程中串到了视口左下角。
this.$emit('showOrderPreview');
this.refresh();
基于上述推测,为了解决这个问题,考虑将上述两行代码对调:
- 先让
refresh
的逻辑去触发order-project-preview.vue
的隐藏,自然也就隐藏了next-bar-affix.vue
- 再触发
showOrderPreview
事件来order-project-preview.vue
的展开 - 等
refresh
触发的异步调用结束以后,order-project-preview.vue
中的具体内容才连同next-bar-affix.vue
被展示出来
但结果不如预期的那样,猜想也许是refresh
成功隐藏next-bar-affix.vue
之前,this.$emit('showOrderPreview');
的逻辑引起的弹出生效了,于是添加了如下代码(事实上纯属没有依据的HardCode):
this.$emit('showOrderPreview');
this.$nextTick(() => {
this.refresh();
})
运行,没在出现如上所示的问题,似乎是解决了。
6. 问题又重现了
然后,提交代码后,测试同学告知,该问题又重现了,重现步骤姑且略过,这时候联想到之前排错的过程中,就猜想可能跟浏览器的渲染顺序有关系,然后把浏览器的工作原理的基础介绍翻阅了一遍,但并没有解答这个问题。
又想起团队的caoq同学分享过如何监听DOM上的属性变化。问题难点无非在于无法让浏览器在某个异常的时刻停下来,如果能停下来观测DOM的结构和样式,差不多就可以排查到问题根源。于是给order-project-preview.vue
的根节点设置了attribute modification
的断点(设置方法如下图所示):
这时候意外地发现在异常发生时,主界面order-proejct-preview.vue
的内容已经加载出来了(但却没显示),即不存在next-bar-affix.vue
丢失默认的left基准的问题。
倒是发现了该容器上添加了一个 .h-loading-parent
的样式,这个样式定位为 relative
。由此基本能确定了在heyui
的loading
组件的渲染中,会修改所在父容器order-project-preview.vue
的样式,给它添加上.h-loading-parent
。
因为 order-project-preview.vue
最外围默认的样式是 fixed
, 被.h-loading-parent
覆盖之后变成了 relative
(它们具有相同的优先级,后出现的relative
覆盖前出现的fixed
),从而导致了next-bar-affix.vue
的left
基准丢失。我们只要把给弹出的面板order-project-preview.vue
加上更高优先级的fixed
定位即可。
//loaing组件提供 样式
.h-loading-parent {
position: relative;
}
// 自书写的样式
.order-project-preview-page {
// 两个class累积的优先级为2,大于.h-loading-parent的1:
&.previewing {
position: fixed;
}
}
7. 再深入一些
问题:断点过程已经渲染好的面板却看不到内容,只看到一个错位的底部栏
通过查看heyui
的<loading>
组件源码的样式可以发现:
-
heyui
组件在loading
完毕后,会设置500ms
的timeout
来清除.h-loading-parent
的样式(参加下图1),即内容渲染出来了,但是样式500ms
内还没去除,也就导致了relative
引起的错位 -
relative
的属性同样影响到了.order-project-preview-page.previewing
的高度,因为它的各个子元素无一例外都absolute
的,因此这样的文档流无高度的,而被覆盖的fixed
,通过了top',
bottom`赋予了它和视口相同的高度。
问题2 我们在本地代码上无法重现该问题
在本地代码运行在浏览器的样式发现:.order-project-preview-page
上设置的fixed
的优先级高于 h-loading-parent
的relative
。
因此可以做一个猜测
- 在开发环境中库的样式被先加载,自己组件内地样式后加载,如果计算的优先级的值一样的话,自己组件的样式自然就失效了。
- 在线上环境,样式的代码经过压缩以后,并不能保留开发环境中的出现顺序,因此需要人为的设置来保证所期待的优先级。
网友评论