5.2 三栏-固定宽度布局
本节,我来教你创建三栏布局。只要掌握了创建三栏布局的技术,你想搞多少栏就能搞多少栏。由于本章主要关注页面结构,为了让大家直观地看到整个过程到底发生了什么,咱们要给每个栏加上深浅不同的背景色。
就从一个简单的居中的单栏固定布局开始吧。为了让布局好看一点,我写了一些简单的样式。为节省版面,我们没有给出这些样式,实际上它们对本节要讲的技术没有任何影响。主要标记的ID是wrapper
(外包装),这个容器里包含了一栏。
<div id="wrapper">
<article>
<! -- 这里是一些文本元素 -->
</article>
</div>
布局相关的CSS如下:
#wrapper {width:960px; margin:0 auto; border:1px solid;}
article {background:#ffed53;}
图5-1 为article
添加背景色和边框,以看清这个居中的栏
如图5-1所示,通过给整个外包装设定宽度值,并将其水平外边距设定为auto
,这个单栏布局在页面上居中了。随着向里添加内容,这一栏的高度会相应增加。外包装中的article
元素本质上就是一个没有宽度的块级盒子(关于“没有宽度的盒子”,请参见3.2节),它水平扩展填满了外包装。下面,我们再向外包装里添加一个导航元素,让它作为第二栏。
- <div id="wrapper">
- <nav>
- <!-- 无序列表 -->
- </nav>
- <article>
- <! -- 文本 -->
- </article>
- </div>
我们得浮动作为两栏的容器元素,让它们并排显示,这是我们在3.3节讲过的。
- #wrapper {width:960px; margin:0 auto; border:1px solid;}
- nav {
- width:150px;
- float:left;
- }
- nav li {
- /*去掉列表项目符号*/
- list-style-type:none;
- }
- article {
- width:810px;
- float:left;
- background:#ffed53;
- }
图5-2 在布局中添加了第二栏
如图5-2所示,把两栏容器元素的总宽度设定为外包装的宽度(150 + 810 = 960),并浮动它们,就可以创造出并肩排列的两栏来。每一栏的长度取决于内容多少。采用同样的方法,可以添加第三栏(或任意多个栏)。
要了解如何让所有栏看起来都同布局一样高,请参考5.3.1节介绍的“人造栏技术”。
- <div id="wrapper">
- <nav>
- <!-- 无序列表 -->
- </nav>
- <article>
- <! -- 文本 -->
- </article>
- <aside>
- <! -- 文本 -->
- </aside>
- </div>
接下来,我们要调整article
这一栏的宽度,为第三栏腾出空间。
- #wrapper {width:960px; margin:0 auto; border:1px solid;}
- nav {
- width:150px;
- float:left;
- background:#dcd9c0;
- }
- nav li {
- list-style-type:none;
- }
- article {
- width:600px;
- float:left;
- background:#ffed53;
- }
- aside {
- width:210px;
- float:left;
- background:#3f7ccf;
- }
图5-3 布局中有了三个浮动栏
如图5-3所示,通过把三个浮动容器的总宽度设定为恰好等于外包装的宽度(150 + 600 + 210 = 960),就有了三栏布局的框架。就用这种办法,我可以想加多少栏就加多少栏,只要它们的总宽度等于外包装的宽度即可。当然,多栏布局通常都有与布局同宽的页眉和页脚,下面我们就来添加。
- <div id="wrapper">
- <header>
- <!-- 标题 -->
- </header>
- <nav>
- <!-- 无序列表 -->
- </nav>
- <article>
- <! -- 文本 -->
- </article>
- <aside>
- <! -- 文本 -->
- </aside>
- <footer>
- <!-- 文本 -->
- </footer>
- </div>
我们希望页眉和页脚与布局同宽,而且它们默认就与布局同宽,所以如图5-4所示,在此我们只简单地为它们设定了背景色,以便能看到它们在哪儿。
header {background:#f00;}
footer {background:#000;}
图5-4 页眉还不错,但页脚却移动到了浮动栏的后面
此时的页眉与布局同宽,其内容高度也比较合适。但是,页脚位于浮动元素后面,所以就会尽量往上移动。解决这个问题也很简单,代码如下,效果如图5-5所示。
footer {clear:both;}
图5-5 应用清除之后,页脚把自己定位到了最长一栏的下方
为页脚应用clear:both
(clear:left
效果也一样,因为这里只有左浮动元素),就可以阻止它向上移动,不让它超过浮动元素的下方边界。这么一条简单的规则,就可以保证页脚始终都位于最长栏的下方。
到目前为止,我们为HTML标记应用的CSS如下(没多少文本样式)。
* {margin:0; padding:0;}
#wrapper {width:960px; margin:0 auto; border:1px solid;}
header {background:#f00;}
nav {
width:150px;
float:left;
background:#dcd9c0;
}
nav li {
list-style-type:none;
}
article {
width:600px;
float:left;
background:#ffed53;
}
aside {
width:210px;
float:left;
background:#3f7ccf;
}
footer {clear:both; background:#000;}
接下来,我们主要解决布局中的两个主要问题。首先,内容与各栏边界紧挨在一起,太拥挤;其次,每栏高度由文本多少决定,而如果每栏都与布局一样高则更好。我们先来为内容周围添加一些空白。别以为这是件简单的事儿,一会儿你就知道了。
为栏设定内边距和边框
只要一调整各栏中的内容,布局就可能超过容器宽度,而右边的栏就可能滑到左边的栏下方。一般来说,两种情况下可能会发生这种问题。
- 为了让内容与栏边界空开距离,为栏添加水平外边距和内边距,或者为了增加栏间距,为栏添加外边距(只要开始给布局添加样式,就一定会采用这里说的一种做法,甚至双管齐下),导致布局宽度增大,进而浮动栏下滑。换句话说,右边浮动的栏因为没有足够的空间与其他栏并列,就会滑到左边栏的下方。
- 在栏中添加大图片,或者没有空格的长字符串(如长URL),也会导致栏宽超过布局宽度。同样,这种情况下右边的栏也会滑到左边的栏下方。
好了,下面我们就来试试添加内边距,增大内容与栏边界的距离。这次从中间一栏开始。
- article {
- width:600px;
- float:left;
- background:#ffed53;
- padding:10px 20px;
- }
图5-6 中间栏中的内容与栏边界之间有了空间,但由于这个栏占据的空间增大,导致右边的栏滑到了左边的栏下方
如图5-6所示,中间栏的文本四周都增加了间距,与栏边界保持了一定距离。可是,这样一来由于中间栏的总宽度增加,导致右边的栏不能再与前两栏并排在一起,结果就跑到了左边栏的下方。还记得吗,第3章在讲盒模型的时候我们说过,为固定宽度的元素添加水平外边距、边框和内边距,会导致元素盒子变宽。像这样增大浮动栏的宽度,几乎总会造成图5-6所示的“浮动滑移”问题。好在,我们也有三种方法来预防该问题发生。
- 从设定的元素宽度中减去添加的水平外边距、边框和内边距的宽度和。
- 在容器内部的元素上添加内边距或外边距。
- 使用CSS3的
box-sizing
属性切换盒子缩放方式,比如section {box-sizing:border-box;}
。 应用box-sizing
属性后,给section
添加边框和内边距都不会增大盒子,相反会导致内容变窄。
我们分别试验一下这几种手段。
1. 重设宽度以抵消内边距和边框
比如我们给600像素宽的栏又添加了20像素的内边距,为了抵消增加的内边距,可以把栏宽减少40像素而设定为560像素。这样,右边的栏就能归位。问题在于,每次只要调整内、外边距就要重设布局宽度,有点烦人。因此这个办法虽然可行,却不够理想。说不定哪一回调整内边距或边框就会导致布局错乱。
2. 给容器内部的元素应用内边距和边框
把外边距和内边距应用到内容元素上确实奏效。前提是这些元素没有明确地设定宽度,这样它们的内容才会随着内、外边距的增加而缩小。就像盒模型定义所说的,没有宽度的元素在水平方向上会适应其父元素,其内容会随着外边距、边框和内边距的增加而减少。
然而,一栏之中可能会包含大量不同内容的元素。假如将来又决定调整内容与容器边界的距离,就必须每个元素都要进行调整,这样不仅麻烦,而且容易出错。况且,给栏添加边框同样会增大栏宽,不可能通过为其包含的内容元素逐个应用样式来做到。
所以说,与其为容器中的元素添加外边距,不如在栏中再添加一个没有宽度的div
,让它包含所有内容元素,然后再给这个div
应用边框和内边距。如此一来,只要为内部div
设定一次样式,就可以把让所有内容元素与栏边界保持一致的距离。而且,将来再需要调整时也会很方便。任何新增内容元素的宽度都由这个内部div
决定。
采用这种方法除了标记中多了一个div
元素外,唯一的问题就是那些反对把标记用于表现用途的纯粹论者会跟你叫嚣。关于我对这个问题的看法,请参考附注栏“关于表现性标记的思考”,另外一个附注栏“子-星选择符”也给出了用代码替换内部div
的方案。
关于表现性标记的思考
HTML的目的是语义,也就是给内容赋予含义。而CSS呢,是为了把表现性的样式分离出来才发明的。不过,有些表现性标记是有害的,而有些则没有副作用。使用表格来创建多栏布局,或者使用
标签,这种做法的确不值得提倡,因为这会造成内容难以移植。比如说吧,用三个表格单元作为三栏,这种布局到哪都会显示成表格,就算是在完全不合适的智能手机里也一样。如果表现性标记无法用CSS修改,或者在CSS不可用时也要迫使用户接受,那就是滥用HTML。可是,标签在段间换行,却不使用
div
或span
这种中性的元素,对默认样式没有影响,除非你给它们应用样式,否则它们就跟不存在一样。所以,我认为添加这种元素达到表现性的目的是完全可以接受的。
下面我就为大家示范怎么用内部div
修复图5-6中存在的问题。
- <article>
- <div class="inner">
- <!-- 文本 -->
- </div>
- </article>
现在,把造成问题的内边距从栏上去掉。为了示范这个技术有多好用,接下来我们不仅要给内部div
应用内边距,还要给它应用外边距和边框。
- article {
- width:6oopx;
- float:left;
- padding:10px 20px;
- background:#ffed53;
- }
- article .inner {
- margin:10px;
- border:2px solid red;
- padding:20px;
- }
图5-7 给没有宽度的内部div
应用外边距、边框和内边距后,中间栏的宽度没有变化,右边的栏仍然还在原来的位置上
如图5-7所示,中间栏的宽度并未因此有什么变化,因为内容区减少的宽度抵消了应用到内部div
上的外边距、边框和内边距的总宽度。总之,由此可以得出一个结论:如果布局中的栏是浮动的,而且都设定了宽度,你就根本不要去动它!要动,就把内容放在内部div
里,动这个div
。
好了,解决问题的关键已经讲清楚了。接下来我们去掉中间栏的外边距、边框和内边距,给其他两栏也添加内部div
,然后只给这三栏加上内边距。
- <div id="wrapper">
- <header>
- <!-- header text -->
- </header>
- <nav>
- <div class="inner">
- <ul>
- <!-- 链接 -->
- </ul>
- </div>
- </nav>
- <article>
- <div class="inner">
- <!-- 文本 -->
- </div>
- </article>
- <aside>
- <div class="inner">
- <!-- 文本 -->
- </div>
- </aside>
- <footer>
- <!-- 文本 -->
- </footer>
- </div>
接下来我们就利用这个div
为三个栏中的内容创造间距。此外,还居中了页脚中的内容。
nav .inner {padding:10px;}
article .inner {padding:10px 20px;}
aside .inner {padding:10px;}
footer {text-align:center}
图5-8 通过为内部div
应用内边距,布局的宽度并没有改变
如图5-8所示,三栏文本与栏边界之间有了内边距生成的必要距离,而footer
元素中的文本也居中了。
子-星选择符
所谓“子-星选择符”就是一个组合选择符,利用它可以不使用内部
div
就能设定一栏中所有元素的外边距。星号选择符可以选择“所有元素”,故而,在一个选择符后面加个星号,比如
someSelector *
就可以选择someSelector
所代表元素的所有后代元素。子选择符可以选择“某元素的子元素”,故而,把子选择符放到星号前面,比如someSelector > *
就会只选择someSelector
所代表元素的所有子元素,而非后代元素。这正好适用于选择容器内部的所有顶部元素,然后设定它们的外边距。比如,对于section
栏,设定section > * {margin:0 10px;}
,就能为栏中所有子元素,不包括其他后代元素,各应用10像素的左、右外边距。 使用“子-星选择符”要注意两点。第一,在为子元素设定垂直外边距时,只能使用margin-top
和margin-bottom
,不能使用简写的margin
,否则会抵消用“子-星选择符”应用给这些元素的水平外边距。如果你想进一步缩进某个子元素的内容,就应该给该子元素应用内边距。第二,“子-星选择符”有潜在性能问题,因为它会导致浏览器遍历整个DOM结构去查找所有匹配的元素。但我也发现这一点性能影响几乎可以忽略不计。假如你的页面真的包含几千上万个元素,那倒确实该考虑用ySlow或其他性能度量工具测一测这个选择符的影响。
以上措施使图5-5所示的布局有了明显改观。就这么简单的几下,布局就显得更专业了。处理栏及其内部div
的关键在于,浮动栏并设定栏宽,但不给任何内容元素设定宽度。要让内容元素扩展以填充它们的父元素——内部div
。这样,只要简单地设定内部div
的外边距和内边距,就可以让它们以及它们包含的内容与栏边界保持一定距离。
注意,如果容器的上、下边框不可见,内部div
的上、下外边距会叠加。要是你遇到了这个问题,可以只为容器设定垂直内边距。但要小心一点,别一块儿也添加水平内边距,比如article {padding:20px 0;}
,就只设定了上、下内边距。
3.使用box-sizing:border-box
这是最简单的一个办法。只要在三个浮动的栏的CSS规则中分别加上box-sizing:border-box
声明,再给栏添加内边距(和边框)就不会导致盒子的宽度变化了。此时,既不用调整栏宽去抵消增加的内边距,也不用使用内部div
。添加内边距的结果就是内容收缩。以下就是简洁清晰的没有内部div
的标记。
<div id="wrapper">
<header>
<!-- 标题 -->
</header>
<nav>
<ul>
<!-- 链接 -->
</ul>
</nav>
<article>
<!-- 文本 -->
</article>
<aside>
<!-- 文本 -->
</aside>
<footer>
<!-- 文本 -->
</footer>
</div>
而以下就是CSS规则。
- * {margin:0; padding:0;}
- #wrapper {width:960px; margin:0 auto; border:1px solid #000;
- overflow:hidden;}
- header {background:#f00;}
- nav {
- box-sizing:border-box;
- width:150px;
- float:left;
- background:#dcd9c0;
- padding:10px 10px;
- }
- /*去掉列表项目符号*/
- nav li {list-style-type:none;}
- article {
- box-sizing:border-box;
- width:600px;
- float:left;
- background:#ffed53;
- padding:10px 20px;
- }
- aside {
- box-sizing:border-box;
- width:210px;
- float:left;
- background:#3f7ccf;
- padding:10px 10px;
- }
- footer {clear:both; background:#000;}
图5-9 使用box-sizing
属性后,可以直接给栏应用内边距
如图5-9所示,直接给栏应用内边距会导致内容变窄,但不会影响布局。听起来容易的办法总会有一个“但是”,这里的“但是”要说的是IE6和IE7不支持box-sizing
属性。不过,有一个专门解决这个问题的腻子脚本(polyfill),名叫borderBoxModel.js。你可以使用条件注释(以便只有IE6和IE7加载)把它添加到HTML标记之后、结束的标签之前,以保证在加载DOM之后再执行该脚本:
本书附录介绍了什么是腻子脚本,以及为什么需要它们。
<body>
<!-- HTML标记 -->
<!-- 只让IE8之前的IE加载它 -->
<!--[if lt IE 8 ]>
<script src="helpers/borderBoxModel.js"></script>
<![endif]-->
</body>
最新版本的borderBoxModel.js腻子脚本以及它的用途和局限性,可以参考这里:https://github.com/albertogasparin/borderBoxModel。另外,也可以读一读本人的这篇谈
box-sizing
的文章:http://albertogasparin.it/articles/2012/02/start-using-css-box-sizing-today。
这样,IE6和IE7就可以根据box-sizing
属性的设定正确地调整栏的大小了。在依靠内部div
几年之后,经过试验我发现,不仅给浮动的栏,甚至给所有元素都应用这个不同的盒缩放模型都是没有问题的,我在CSS里会添加这一条规则:
* {box-sizing:border-box}
如此一来,页面中的盒模型就全都符合逻辑了。换句话说,每个盒子的宽度并不是内容区的宽度,而是一经设定就不可变的真正的盒子宽度。在此期间,我经常关注一些Web大牛,比如Paul Irish。建议大家读一读他的相关文章http://paulirish.com/2012/box-sizing-border-box-ftw。另外,别忘了看看文章底下的评论,因为有人提出了一些疑问,也不是所有人都同意他的观点。
好了,下一节我们就讲一讲实现流动布局的几种方式。
预防过大的元素
设计一个将来可能由他人维护的动态网站时,需要考虑得更长远一些。比如,应该预见到可能出现一些过大的元素。如果将来有一张比浮动栏更宽的图片被放到栏中,就会导致布局变宽,而右边的栏又会滑到下方。为此,一个简单的预防措施就是添加一条
.inner img {max-width:100%;}
声明,以便限制图片的宽度不超过其父元素(在此就是内部div
)。另一个办法是给每个栏(或者内部
div
,如果你用了的话)添加overflow:hidden
声明。这条声明不会缩小图片以适应父元素,而会将它(以及任何过大元素)超出容器边界的部分剪切掉。动态网站中另一个潜在的问题是换行。HTML只会在单词间空格的地方换行。一些长URL,甚至一些长单词,在栏比较窄的情况下,都会导致栏宽过大。因此,还应该给所有栏的外包装元素应用
word-wrap:break-word
声明,以便所有栏及其内容继承这个设定。有了这条声明,浏览器会把过长的词断开显示在不同行上。只是word-wrap
没有定义在哪里断开,因此结果完全是随机的,而且没有连字符。不过,这条规则只在需要时才会起作用,而且能保护布局不会被长URL顶得支离破碎。建议你在每一栏中都用长URL、大图片,以及包含内容过多的元素测试一下布局,看看这些声明到底会不会起作用,并发现更多需要事先考虑保护措施的其他漏洞。