TL;DR: 如果一个元素的 flex-basis属性 的值为百分数,且它父级元素(flex容器)在主轴方向上的尺寸没有被显式设置,此时 flex-basis 的值会被解析为 content。即此种情况下,0% 呈现的结果会与 0px 不同。
正文:
一天测试同学跑来反馈说页面侧边菜单导航栏有个bug——“菜单项过多时无法滚动查看下方视口外的菜单项”。
嗯?我回,“不可能,我写代码的时候有考虑到,用上了 overflow-y: auto 哇。”
“不信?”,测试把ta的电脑屏幕怼到我眼前,边划拉边说:“看,是不是有问题?”
“emmm……嗯!……嗯?”,我疑惑又哑口无言,于是默默开始了排查修复。
问题复现
这个问题最初我是在业务开发时遇到的,为了便于清晰描述,我这里写了一个能复现上述问题的简化demo:codepen.io/shadow-mike…
下面将和大家讨论下这个flex布局问题以及分享下我如何探索这个问题的过程。
先描述下这个侧边栏结构的demo。
最外层#sidebar使用flex布局,其子元素除了.main-container外还有其他兄弟元素块(demo里省略),.main-container(浅黄色)也使用flex布局,子元素由LOGO容器(绿色)和菜单容器(深黄色)组成。
需求上希望侧边栏顶天立地,菜单内容过多时不能导致整个页面滚动,只能在菜单容器内部滚动。
基于上述要求,我初次写出了如下样式代码,这里仅罗列出关键的几行布局样式:
/* CSS style */
#sidebar { /* 最外层元素 */
display: flex;
flex-direction: column; /* 定义主轴的方向。column使多个flex子项排列方向与块轴(block axis,多个块元素排列方向)排列方向一致 */
height: 100vh; /* 与视口等高的高度值 */
}
.main-container { /* 包含.logo和.menu-container的容器 */
display: flex;
flex-direction: column;
flex: 1; /* 等价 flex-flow: 1; flex-shrink: 1; flex-basis: 0%; */
}
.logo {
flex-basis: 100px;
}
.menu-container {
flex: 1;
overflow-y: auto; /* 预期:在子元素高度超过父容器时出现滚动条,使子元素在其内部滚动 */
}
上面写的样式代码我的预期是,.main-container 容器高度应该撑满 #sidebar 的高度。假设当前浏览器视口高度是 900px,那 .main-container 高度即为 900px,接着内部的 .logo 占去 100px,剩下 800px 被 .menu-container 撑满。overflow: auto 的预期是,如果内部菜单项子元素高度超过 800px 时,.menu-container 内应该会出现滚动条,子元素在其内部滚动。
然而实际运行在浏览器中,会观察到这里的overflow: auto并没起作用,当子元素(菜单项)过长时,.menu-container 并没出现滚动条,而是也被撑开了,致使整个 #sidebar 高度也被撑开超过了 900px。
你可以点击开头的demo来观察。我使用Chrome<版本 98.0.4758.102>和Safari<版本 14.1.2 (14611.3.10.1.7)>观察得出的表现结果与上述相符。
问题排查
能复现bug后,下一步就是定位和修改问题代码。
有时候CSS的问题并不单纯只靠逻辑就能解决,有时或许还是得靠见多识广。
那刻当下的我很认同这个想法。因为反复检视了代码,排除掉粗心的可能性后,我就想不出其它的排查思路了。有一刻我都怀疑失效了的 overflow-y: auto 是不是和flex属性“八字不合”。看来靠自己掌握的半吊子flex知识根本解决不了。
于是我开始借助搜索引擎。在stackoverflow上翻阅片刻后看到了这个问答:CSS flex-basis: 0% has different behaviour to flex-basis: 0px。问题中贴出的代码示例和自己遇到的情况极其相似。哈?flex-basis 值为 0% 和值为 0px 结果会有差?
将信将疑,于是在样式表里加了个 flex-basis: 0px(因为观察到之前已经设置的 flex: 1 中的 flex-basis 在浏览器里被解析为了 0%),结果还真有变化,侧边栏顶天立地了!!
仍然是开头的demo,你可以通过解开 .menu-container 中的这行注释: // flex-basis: 0px; // key line 来观察到变化。
那刻我又懵了……明明大家都是0,咋还能整出不一样的效果来了呢?肿么肥四??
看了些stackoverflow的回答以及翻阅了些标准文档后,我貌似是能把这个问题理解得七七八八了。下面就开始和大家分享下我的探索过程。
解惑
首先确认第一个问题。
我们应该去哪里寻找最准确、最可靠的答案?
W3C.
简单说,它是一个制定Web标准规范、促进Web长期发展的国际组织。
官网中的各式技术说明文档(specification)里主要描述的是Web设备应当按哪些规则去实现各Web技术,文档的受众主要是诸如浏览器开发者一类。因此如果只是单纯想查阅某个属性有哪些选项值、如何用的话,大可不必翻W3C文档,通常检索MDN文档就能找到答案。
那W3C文档对于我们多数普通前端开发者而言有哪些帮助哪些作用呢?
我感觉我无法断言出答案。反正对我来说,本文问题的答案是在这些文档里找到头绪的。
再说说其它的,
W3C网站上发布的技术报告文档主要分为3大类:
Recommendations: 正式的Web标准规范说明文档;Notes: 记录技术规范以外的信息,比如用例之类;Registries: 记录数据集;
一篇文档从初建到最终完全完成这整个过程中还存在着很多文档状态(这里能查到所有的文档类型及其状态的说明),这儿就只大致介绍下我们现在关注的重点——CSS工作组用到的一些文档状态:
一篇
Recommendation 类文档从草稿阶段开始,此时内容随时会变,对多数开发者来说没太大参考价值。进入 CR 阶段后,文档已得到广泛的审查,内容基本固定,所以如果文档被标记为从 CR 起往后的这些状态,这类文档已具备参考价值,大概率我们可以将其作为依据来查阅使用(为啥说大概率,因为后面我有踩到坑);SPSD 是指过时的标准文档。
以上内容是我读完部分文档后按自己理解写的,不一定精确完备,仅作参考哈。
在翻阅的过程中我还找到一篇讲如何阅读W3C文档的文章,有兴趣的可以阅读下,这里不展开了。
在这里能检索到所有的Web标准和草案文档;
在这里可以概览当前CSS标准的最新进展;
接着回到主题,继续探索 flex-basis 相关问题。
flex-basis 属性的含义?
先简述下这个属性的基本含义。
flex布局一般由flex容器(display 属性值为 flex 或 inline-flex)和其子元素(后文亦称flex子项或flex item)构成。flex-basis 属性一般作用在flex子项上,它定义了:在flex容器分配剩余空间前flex子项在主轴方向上的初始尺寸。flex子项在主轴方向上的实际尺寸是根据元素自身尺寸、flex-basis、flex-grow、flex-shrink 等属性共同决定的。如何计算实际尺寸不是本文重点,不继续展开。
关于 flex、flex-basis 属性的更多使用细节,非常推荐阅读张含韵的头号粉丝张老师的这2篇文章:CSS flex属性深入理解、Oh My God,CSS flex-basis原来有这么多细节。
接下来正式疏解本文的重点问题。
flex-basis 值为 0px、 0% 的差别在哪里?
以下答案的依据均来自这篇被标记为 CR 的文档:CSS Flexible Box Layout Module Level 1
在文档里的第 7.2.3. 章节中提到,
flex-basis 除了能接受 auto、content等值外,还能接受 与width、height 属性相同的值,并且解析方式也相同。就是说 height 怎么解析 0px、0%,flex-basis 也这么解析。所以一般情况下,这2个值对 flex-basis 而言,在浏览器上的表现效果应该都是一样的。
但是读到第二句红色下划线处,它描述了这样一种特殊情况:如果 flex-basis 的值为百分数,且它flex容器的尺寸没有被显式设置,此时 flex-basis 的值会被解析为 content。
content 又是什么意思呢?在W3C文档里也找到了解释。
在第7.1章节中提到,content 值会根据flex子项的内容(指flex子项的子元素尺寸)来计算实际尺寸,多数情况下效果与 max-content 值一致,就是说flex子项的子元素有多长其主轴初始值就有多长。
对于想了解 content 其他几个值的细微差异的同学,这里仍是推荐大家阅读张老师的文章——理解CSS3 max/min-content及fit-content等width值。
小结
小结一下,对于最开始的那个demo,为什么菜单项过多时 .menu-container 内没出现滚动轴,整个过程是这样:
元素 .menu-container(它是flex子项)的样式 flex: 1 是个简写,对应的完整值为 flex: 1 1 0%,即它其中的 flex-basis 为 0%,又因为父元素 .main-container(它是flex容器)没有设置 height 属性,所以 0% 这个百分比数值被解析为了 content,导致(在主轴方向上).menu-container 里的子元素有多长,它就会被撑开多长。
修复的方式是再往 .menu-container 上设置 flex-basis: 0px(或者将 flex 属性重写为 flex: 1 1 0px;)。使其主轴方向上的初始尺寸变为固定的 0px。此时 .main-container flex容器内若还有未分配的剩余空间,再加上 flex-grow: 1 (flex: 1 1 0% 中的第一个值)这个条件, 那它最终的实际尺寸就是:.main-container 的尺寸 - 100px(.logo的flex-basis: 100px)后得到的剩余空间。此时内部菜单项再怎么增多变长也不会把 .menu-container 的肚子撑大。
完美!终于搞清楚了整个问题的来龙去脉。哈哈!准备愉快地给文章收个尾。
……
等等……我好像又发现了个新问题。
在浏览器中,flex: 1 的 flex-basis 值为什么不是W3C文档中提到的 0 ?
看前面那张截图里的最后一句话。
When omitted from the 'flex' shorthand, its specified value is '0'.
当使用 flex 属性但又省略不写它其中的 flex-basis 值时,会将 flex-basis 设置为 0.
这里的 0 单位是 px 还是 % 我们在Chrome浏览器里就能验证:
答案是 px.
再看文档中7.1.1中提到的,
flex 属性只接收一个正数时等价于flex: 正数 1 0;。这里对 flex-basis 的表述与上面开头那句是一致的。
不过当我在浏览器里验证这个规则时,又发现了蹊跷,下面截图是Chrome浏览器<版本 99.0.4844.51>对 flex: 1、flex: 1 1 0 的解析:
浏览器把 flex: 1 中的 flex-basis 解析为了 0%,与W3C文档描述的 flex: 1 1 0; 并不一致。
**!那刻我又又懵了……明明大家都是(打住
说好的W3C是权威是标准规范呢?咋这时候Chrome不听它的了呢。哦不对,测试后发现 Safari<版本 14.1.2 (14611.3.10.1.7)>、Firefox<97.0.1>、Edge等都没听,它们都解析成了 0%。这是浏览器们的集体背刺??
大脑断片片刻后,我又开始进行了搜索。还是同一篇W3C文档,的确从中又找到了些新线索:
这段是2015年修正日志里的,意思是将之前 flex 简写语法里的 flex-basis 的解析值从 0% 恢复为原来的 0.
嚯,好家伙,标准文档里曾经的确是有将其解析成 0% 的描述。
再往前捯,最终找到了2012年修正日志里的这一段:
Historically(突然想用这个词), 这个值的解析规则经历了 0px -> 0% -> 0px 的变化,这是W3C flex 标准文档的视角。
那浏览器们为什么只遵守了 0px -> 0% 的规则变化呢?
这次从浏览器的视角出发,我检索了chromium bugs。喜出望外,还真找到了相关的issue。
根据这个issue里提供的信息了解到,这个“bug”的确在2015年时就有人提交给了mozilla,但是浏览器们都不约而同的保留着这个“bug”一直到现在。
最终在csswg-drafts项目里的这个issue里找到了可能的答案:
简而言之就是有兼容性问题。现在已有很多网页都利用了 flex 属性的这个特性来开发。如果简写语法里 flex-basis 的解析从 0% 变为 0px 会导致部分情况下flex容器塌陷,致使很多网页异常。
我又写了一个demo,大家能更直观的看到 flex-basis: 0px、flex-basis: 0% 的差异:
好啦,对于上面这些问题,我现在就摸索到了这儿,已经全部分享出来了。如果有兴趣的话大家可以自行再去做更多的深入。
朴素收尾
通过对一个业务bug的探索,我自己有如下收获:
- 搞清楚了
flex-basis值为0px和0%的差异; - 大致搞清楚了如何利用、阅读W3C文档;
- 反手在stackoverflow上答了一个类似问题;
如果读完本文你或多或少也有收获的话,麻烦给点个赞👍鼓励下!