重塑你的CSS世界观——浮动魔鬼float
为什么要写《重塑你的CSS世界观》系列文章
由于从工作到现在,我的主要工作都是写JavaScript,几乎没怎么碰CSS,通常都是别人写好界面,然后我来开发JavaScript逻辑代码,这导致了严重的偏科,CSS弱得很,所以我决定要重新学习CSS,从打好CSS2的基础开始。最近我开始在看一本关于CSS的书,这本书叫《CSS世界》,是张鑫旭这个CSS大神写的,质量十分有保障。
这本书几乎完全颠覆了我对于CSS的认知,几乎等于重塑我的CSS世界观。
子曰:学而不思则罔,思而不学则殆。
所以我决定把从这本书学习到的知识结合我看到的一些文档来总结一下,以便以后复习使用,所以文章中的见解也有可能是错的,不追求完全正确,而是希望能够作为方法论来指导之后的CSS开发。如有错误,请诸君指正。
浮动的本质是什么?
浮动的本质就是为了实现文字环绕效果 ——《CSS世界》第六章 流的破坏和保护
熟悉张鑫旭老师的人,应该在他的博客和一些视频教程中都有听到上面那句话,可能和我一样,咋一看觉得懂,然后就不以为然就过去了,然后使用在float
的时候还是错误百出。
撇开我们是前端开发者这个身份,让我们作为一个普通用户的角度来思考有哪些场景我们会用到文字环绕效果。相信很多人都是使用过Word
这个软件,在我们编辑图文信息的时候,希望文字可以围绕图片来排列,也就是文字环绕,也就是从图1变为图2:
<center>图1</center>
<center>图2</center>
普通用户视角,做到文字环绕的的步骤
还是一样,让我们作为一个普通用户来思考如何实现这一效果。为了方便,我在这里定义一些词汇,将文本、段落称之为跟随内容
,将图片等需要被环绕的称之为目标元素
。
首先然后我们将这两者简化一下,分成两个区块,如图:
而所谓的环绕,那就是把两个区块重叠在一起,目标元素
悬浮于重叠区上面,而且重叠区不能有跟随内容
的内容,如图:
所以要使得跟随内容
能够环绕目标元素
我们需要做到以下步骤:
- 使
跟随内容
的顶部能够上升到与目标元素
顶部同一水平线上; 跟随内容
与目标元素
的重叠区域不能有文字,不重叠区域按照原来的排版方式。
如何浮动?
脱离文档流
浮动最初是为了实现文字环绕效果,经过上面的讨论我们意识到了需要让浮动元素和跟随元素的顶部上升到同一水平线上。而在文档流中,如果浮动元素和跟随元素都是Div元素,它们两在默认情况下都将占据一行。所以我们要让浮动元素脱离文档流,这个时候跟随元素才能根据文档流的要求进行上移,从而达到视觉上的重叠效果。
而由于浮动元素脱离了文档流,如果父元素没有指定高度或者其他元素撑起,也就出现了所谓的浮动元素的父元素高度塌陷。
确定“包含块”,在“当前行”进行浮动
在进行原理分析前,让我们先来看一段代码,并想象它的渲染结果
<style> .box { border:1px solid red; width: 200px; margin-left:50px; } .text { background-color:gray; color:white; } .float { float:left; color:blue; } </style> <div class='box'> 我就是 <span class='text'>一段文本<em class='float'>浮动</em></span> </div>
此时,你脑海里想象的结果可能如下图:
但实际运行效果却是这样的:
为什么会出现这种情况呢?让我们从CSS的标准中找寻答案。W3C的CSS标准文档关于浮动的描述中有这样一段话:
A floated box is shifted to the left or right until its outer edge touches the containing block
edge or the outer edge of another float
翻译成中文就是:
一个浮动盒会向左或向右移动,直到其外边界挨到包含块
边界或者另一个浮动盒的外边界。
OK,先撇开有多个浮动元素的情况,让我们只考虑一个浮动元素的情况。注意到上述引用文本中的高亮词没有?包含块(containing block)
,没错,之所以刚才我们对代码运行的渲染效果的想象与实际浏览器运行的效果产生偏差,就是因为我们没有意识到包含块(containing block)
,或者识别错了浮动元素的包含块
,而这个包含块
就是元素浮动的参考位置!
什么是包含块?如果判断一个元素的包含块?
在W3C的CSS标准文档中关于包含块的描述中有这样一段话:
The position and size of an element's box(es) are sometimes calculated relative to a certain rectangle,called the containing block of the element
翻译成中文就是:
元素(生成的)盒的位置和大小有时是根据一个特定矩形计算的,叫做该元素的包含块(containing block)
关于如何确定一个元素的包含块,标准也给出了定义,由于英文的较长,阅读起来比较吃力,在此我放一段中文的定义:
- 根元素所在的包含块是一个被称为初始包含块的矩形。对于连续媒体,尺寸取自视口的尺寸,并且被固定在画布开始的位置;对于分页媒体就是页区(page area)。初始包含块的'direction'属性与根元素的相同
- 对于其它元素,如果该元素的position是'relative'或者'static',包含块由其最近的块容器祖先盒的内容边界(
the content edge
)形成
- 如果元素具有'position: fixed',包含块由连续媒体的视口或者分页媒体的页区建立
- 如果元素具有'position: absolute',包含块由最近的'position'为'absolute','relative'或者'fixed'的祖先建立,按照如下方式:
- 如果该祖先是一个行内元素,包含块就是环绕着为该元素生成的第一个和最后一个行内盒的内边距框的边界框(bounding box)。在CSS 2.1中,如果该行内元素被跨行分割了,那么包含块是未定义的
- 否则,包含块由该祖先的内边距边界形成
如果没有这样的祖先,包含块就是初始包含块
根据上面关于包含块的定义,我们可以发现.float
元素符合第2条,那么它的包容块就是.box
这个div
的内容边界
,注意是内容边界(the content edge
),而不是整个盒子,所以不包含padding、border、margin。
让我们先来看一张关于盒模型图:
W3C的CSS规范关于content edge
的定义是:
The content edge surrounds the rectangle given by the width and height of the box, which often depend on the element's rendered content The four content edges define the box's content box.
翻译成中文就是:
内容边界环绕着盒的width和height指出的矩形,通常取决于元素的呈现(rendered)内容。4条内容边界定义了盒的内容框(content box)
所以如果我们给.box
类添加一个padding
为10px的话,
.box { padding: 10px; }
浮动元素由于是贴着包容块在浮动,而此时的包容块仅仅是.box
元素的内容区,不包含padding
,所以就出现了,浮动元素与.box
元素盒子之间的间距,效果如下:
OK,那如何通过只调整CSS的方式就达到我们一开始脑海里想象的渲染效果呢?答案就是让.text
span元素成为.float
元素的包含块
就行了,让inline level element
变成block level element
,只需添加一句dispaly:inline-block
或dispaly:block
即可,这里我们还想保持.text
元素的内联性,所以我们添加:
.text { display:inline-block; }
此时的效果就是我们一开始想要的了
在“当前行”浮动
W3C的CSS标准规范中关于浮动的定义,一开头就有这么一句:
A float is a box that is shifted to the left or right on the current line
.
翻译成中文就是:
浮动(盒)就是一个在当前行
向左或向右移动的盒。
大家注意高亮词当前行(the current line)
,一定要理解这个当前行
,如果说包含块(containing block)
决定了浮动元素浮动的范围的话,那这个当前行(the current line)
就决定了在浮动范围的哪个垂直位置进行浮动。
同样,我们通过一个小场景来了解这个当前行
的重要性。
想象一下,你接到了一个任务,要求实现一个三列布局,无论左右两列的宽度怎么变化,中间列的宽度都要自适应,效果大概如下图(为了突出效果,给三列都添加了背景颜色):
聪明的你可能已经想到了办法,让左右两列浮动,中间列的左右margin
值为auto
,这样就可以实现中间列自适应了。没错你觉得十分机智,然后按照设计图的视觉你从左到右写了三个div
:
<style> .container { width: 600px; color:white; text-align:center; font-size:30px; } .left { float:left; background-color:yellow; width:200px; height:200px; } .right { float:right; background-color:red; width:200px; height:200px; } .middle { height: 200px; margin: 0 auto; background-color:blue; } </style> <div class='container'> <div class='left'>Left</div> <div class='middle'>Middle</div> <div class='right'>Right</div> </div>
然而你运行代码,你看到的效果却是这样的:
然后你的心情奔溃了:
遇事不慌,首先让我们分析一下,浮动元素.right
的包含块是什么?没错,就是.container
的内容边界(content edge)
,所以虽然有点差错,但好歹还在里面浮动着。
那接下来就是确定当前行(current line)
了。
让我们还原到最开始的时候的布局,去除.left
和.right
的浮动属性,此时会有"三行"
首先我们让.left
浮动起来,在.container
的内容边界范围中、“第一行”中向左浮动,由于其脱离了文档流,.middle
开始上移到“第一行”。
此时由于.middle
的上移,.right
元素也向上移动到了“第二行”,然后让它浮动,此时.right
的浮动范围是.container
的内容边界范围中,而浮动的当前行
是“第二行”,所以就出现了.right
元素掉下去浮动
的效果了,因为它的“当前行”就是“第二行”,它其实没有掉下去过!
那怎么样才能解决,很简单,让.left
和.right
“同一行”不就好了。什么意思呢?
.left
浮动的当前行是“第一行”,然后由于脱离了文档流,后续的元素会往上移动一行,那我们让.right
紧跟在.left
之后不就行了,.left
浮动后,.right
就会到“第一行”,此时浮动不就到了“第一行”的右边了么。然后由于.right
也脱离文档流,.middle
也上移到了“第一行”,具体代码如下:
<div class='container'> <div class='left'>Left</div> <div class='right'>Right</div> <div class='middle'>Middle</div> </div>
然后就是运行之后就是我们一开始想要的效果了:
看到这里相信你已经理解了许多,让我们通过相关下面这段代码的运行效果,来检验你是否真的理解了吧,提示:标题中的文字已经超出了.title
的宽度了
<style> .title { width:160px; } .more { float:right } </style> <h3 class='title'>我就是一个三级标题<a href='#' class='more'>更多</a></h3>
如果你能脑补出下图的效果,说明你已经懂了,如果还不能,请回到前文继续阅读。
让我们用上面用过的套路来分析这一现象,首先确认.more
元素的包含块为.title
元素的内容边界,然后由于内容过长,宽度不够,此时就变为了两行显示,而.more
元素就在“第二行”,所以它的当前行就是“第二行”,添加浮动属性之后,在.title
元素的内容边界范围内的“第二行”向右浮动,就形成了我们看到的样子。
如果希望无论标题内容无论多长,.more
元素都要固定浮动在右边,那么只需要将.more
元素的代码放置带.title
元素的最前方即可,也就是:
<h3 class='title'><a href='#' class='more'>更多</a>我就是一个三级标题</h3>
此时的效果就是我们想要的了
重叠区不允许渲染“内容”,行框盒子“卡位”,实现文字环绕
这一部分其实是浮动的本质,却经常被我们所遗忘,主要是因为这个文字环绕的效果主要影响的是浮动元素的跟随元素。
由于这一部分规范中的定义比较晦涩,所以以下内容是我在阅读《CSS世界》结合自身的理解来阐述的,如果你希望查看规范的原文,可以访问这个链接。
首先由于浮动,浮动元素和跟随元素出现了一定的重叠区,这一点大家都知道了,而要想内容环绕,则重叠区不能有内容,注意是内容,文本、svg、图片等替换元素都被视为“内容”,基本都是inline或line-block的元素。
而无论是inline还是inline-block元素,都会产生“行框盒子”,在重叠区中不允许出现“内容”,也就可以转化成重叠区的“行框盒子”需要“贴在”浮动元素浮动方向的反方向的侧边即可,就是说如果一个元素向左浮动,则跟随元素在重叠区的“行框盒子”需要“贴在”浮动元素的右侧边,反之亦然。
让我们看一段代码:
<style> .container{ width:250px; border:1px solid black; } .float { float:left; width: 100px; height:100px; border:2px solid red; } .paragraph { background-color:green; color:white; } .paragraph:first-line { background-color:blue; } </style> <div class='container'> <div class='float'></div> <div class='paragraph'> 我是匿名内联盒子的文本 <span>,而我是内联盒子的文本,如果文本很长,那就会出现balabalabala的效果。 </span> 充点字数哈兄弟。 </div> </div>
我们用蓝色背景高亮文本的第一行,就可以看得出来“行框盒子”卡浮动元素侧边的效果了,然后注意,重叠区是不能渲染“内容”,背景颜色不属于“内容”,所以我们可以看到绿色背景色连重叠区也铺满。
这时候可能有大兄弟要问,那重叠区到底是浮动元素在上还是跟随元素在上啊?答案就是:浮动元素在上。
让我们加一段代码验证以下:
.float { background-color:yellow; }
效果如下:
总结
当一个元素要进行浮动时,需要以下步骤:
- 脱离文档流;
- 在“包含块”范围内及“当前行”上根据浮动方向进行浮动;
- 受浮动影响的元素在与浮动元素的重叠区中不允许渲染“内容”,受浮动影响的元素内的行框盒子进行“卡位”,实现文字环绕;