《WebKit 技术内幕》之六(1):CSS解释器和样式布局
CSS解释器和规则匹配处于DOM树建立之后,RenderObject树之前,CSS解释器解释后的结果会保存起来,然后RenderObject树基于该结果来进行规范匹配和布局计算。当网页有用户交互或者动画等动作的时候,通过CSSOM等技术,JavaScript代码同样可以非常方便地修改CSS代码,WebKit此时需要重新解释样式并重复以上这个过程。
1 CSS基本功能
1.1 简介
先谈一谈HTML网页的开发者们所遭遇的痛苦和悲惨的经历。在CSS出现之前或者更早,HTML网页设计者们因为要设计不同风格和样式的元素,所以规范不停地加入很多新的元素来表示网页布局,例如p、span等元素。然而,问题依然存在,例如,使用表格(Table)元素来排列网页中的元素,这可能存在一些问题:其一,表格经常内嵌表格,导致网页内容较大,占用带宽;其二,被搜索引擎解析后,网页内容将会变得杂乱无章。所以这时候急需一种技术来解决这些问题。庆幸的是,此时CSS出现了。
CSS的全称是Cascading Style Sheet,中文名是级联样式表,主要是用来控制网页的显示风格。它被广泛地使用在网页中,绝大多数的现代浏览器都支持它。CSS的一个比较重要的特征就是将网页的内容和内容的展示方式分离,这对开发者提高开发效率非常有用。另一个重要的特征是它很强大,而且不是一般的强大,特别是新的CSS3标准,不仅能提供对页面任意元素的精准控制,同时还能提供丰富多彩的样式。简而言之,CSS是一种非常出色的文本展示语言。
Web开发者有两种方法可以使用CSS,第一种就是示例代码中将CSS的代码放入元素“style”中,这称为内部样式表;第二种就是形如代码<link rel="stylesheet"type="text/css"href="css-url.css">这样的用法,引用了一个外部的CSS文档,这称为外部样式表。
示例代码 6-1 使用CSS的HTML网页
为了便于读者对CSS有个直观的印象,示例代码6-1展示了一个使用CSS的简单例子,后面很多描述都是基于它来展开的。不过,该例子虽然简单,但却是一个展示了CSS众多特征的例子,CSS的主要部分包含在元素“Style”中,也就是例子中的第3行到第16行。同时,JavaScript代码部分也有对样式的操作,如第29行,后面的部分会对它们逐一加以解释。
样式的来源有三种类型,其一是网页开发者编写的样式信息,它被包含在网页或者外部样式文件中,这也是最常见的方式;其二是网页的读者设置的样式信息,读者可以设置一个样式,这个样式可以应用到其浏览的网页;其三是浏览器的内在默认样式。以上三种类型的优先级自然是递减的。
CSS语言主要定义了一系列作用在各个元素上的样式规则,如示例代码6-1中的第4到17行就是一个规则,该规则表示div所应用的部分样式设置。CSS3标准中还有很多新的功能,示例代码6-1中也描述了其中的一些功能,那就是3D变形(Transform)。这些样式给网页设计带来了非常震撼的视觉效果,读者可以尝试在浏览器中运行上面的网页。这些新功能笔者将在高级篇中的第8章来介绍。
- 选择器: 上面介绍的属性选择器就是CSS3新加入的,除此之外,还加入了控制精确的选择器用来选择特定位置的子女、特定元素标签的子女等。
- 样式: CSS3增加了一些比较实用的功能,例如自定义字体、圆角属性、边框颜色等。
- 变形、变换和动画(transform、transition和animation): CSS3提供了令人惊奇的变形、变换和动画功能,令其更加赏心悦目。规范的草案中仅定义了2D的变换,而WebKit却可以提供3D的变形。变形有四种类型,包括平移、旋转、缩放和扭曲。同2D变形不同的是,3D变形增加了绕Z轴的平移、旋转和缩放。有一点颇令人遗憾,那就是各个不同的浏览器对这些属性的名字定义不一致,例如标准的变换的定义属性名是“transform”,而Webkit的则是“-webkit-transform”,如例子中第15行所示。IE支持的是“-ms-transform”,Firefox支持的是“-moz-transform”,Opera支持的是“-o-transform”,这些不免令人心烦意乱。变换描述了属性从一个值过渡到另一个值的过程,定义了过程的时间、启动过程的延迟时间等。但是,这些规范草案中的定义还不足以描述更精确的变化过程,所以规范引入了更为灵活的方式,这就是CSS动画。Web开发者使用CSS动画能够定义不同的“keyframes”来控制动画中间的变化过程而不仅仅是动画的开始和结束。读者可以这么理解——变换是一种较为简单和常见的动画。
CSS2引入了一个概念,可以设置跟“media”相关的样式信息,例如用于屏幕显示、打印等。这样,网页可以为了达到不同的目的来设置CSS样式信息,其格式形如“@media screen{div{color:read}}”,它表明该CSS设置的属性仅仅作用于屏幕的显示。
1.2 样式规则
样式规则是CSS规范中最基本的组成,通常,CSS文档包含一系列的样式规则,如上所述,示例代码6-1中的第4行到第17行就是一个完整的样式规则。
下图描述了一个典型的CSS规则结构。一个规则包括两个部分——规则头和规则体。规则头由一个或者多个选择器组成,选择器随后会被介绍;规则体则由一个或者多个样式声明组成,每个样式声明由样式名和样式值构成,表示这个规则对哪些样式进行了规定和设置。
CSS的样式规则表示
当HTML中的某个元素经过后面的匹配算法使用了这条规则,那么就将这些样式设置成该元素的样式,除非有更高优先级的规则匹配上该元素。
1.3 选择器
CSS的选择器是一组模式,用来匹配相应的HTML元素。当选择器匹配相应元素的时候,该选择器包含的各种样式值就会作用于匹配的元素上。通过选择器,CSS能够精准地控制HTML页面中的任意一个或者多个元素的样式属性。查看示例代码6-1中的第4行,该行中的“div”就是一个选择器,这是元素选择器类型,其含义是选择该页面中的所有“div”元素。因为仅有第20行包含一个“div”元素,所以,该元素会使用该选择器的属性设置。那么,“div”元素下面所设置的样式等属性(花括号内)都会作用于该元素,从第6行到第16行。
示例代码中的选择器仅是众多选择器类型中的一种,从CSS1到CSS3,规范陆续地加入了多达42种选择器,极大地增强了选择的能力,下面介绍其中一些主要的选择器。
- 标签选择器: 根据标签元素的名称来匹配,例如示例代码6-1中的选择器。它可以选择一个或者多个元素。
- 类型选择器: 根据类型信息来选择目标元素,类型选择器可以选择一个或者多个元素,示例代码中选择div元素也可以使用类型选择器,方法是“.aclass”。
- lD选择器: 根据元素的ID来选择目标元素,一个选择器仅能选择一个元素,这是因为ID的唯一性。示例代码中选择div元素也可以使用ID选择器,方法是“#adiv”。
- 属性选择器: 根据属性来选择目标元素,可以选择一个或者多个,示例代码中选择div元素也可以使用属性选择器,方法是“div[id]”、“div[id=’adiv’]”、“div[id~=’di’]”、“div[id|=’ad’]”。
- 后代选择器: 选择某元素包含的后代元素,可以选择一个或者多个,示例代码中选择div元素也可以使用后代选择器,方法是“body div”。
- 子女选择器: 选择某元素包含的子女元素,可以选择一个或者多个,示例代码中选择div元素也可以使用子女选择器,方法是“body>div”。
- 相邻同胞选择器: 根据相邻同胞信息来确定选择的元素,可以选择一个或者多个,示例代码中选择div元素也可以使用相邻同胞选择器,方法是“p+div”。
还有很多其他类型的选择器,例如伪类选择器、通用选择器、群组选择器、根选择器等,这里不再一一介绍,请查阅CSS规范。
介绍完选择器之后,还有个非常重要的问题,那就是优先级。对于某个元素的一个属性,因为多个选择器可能都作用于该元素,并且它们可能对该属性设置了不同的属性值,这种情况下,应该怎么确定使用哪种选择器呢?
一般而言,选择器描述得越具体,它的优先级越高,也就是说选择器指向的越准确,它的优先级就越高。例如,如果用1表示标签选择器的优先级,那么类选择器优先级是10,ID选择器就是100,数值越大表示优先级越高。所以,尽量使用控制精确的选择器,使用优先级合理的选择器。假如对于元素的某一样式属性,两个匹配上的选择器都设置了该属性值,那么在此情况下,优先级高的选择器所设置的属性值将会应用到该元素上。
标准中还引入了两个新的JavaScript接口:QuerySelector和QuerySelectorAll。这两个接口让CSS定义的所有选择器都可以作为参数传给这两个接口,从而获取到相应的HTML页面中的DOM节点。Chrome、Safari和Firefox等浏览器都支持该接口。
1.4 框模型
框模型(Box model,或称箱子模型)是CSS标准中引入来表示HTML标签元素的布局结构。一个框模型大致包括了四个部分,它们从外到内分别是外边距(Margin)、边框(Border)、内边距(Padding)和内容(Content)。图描述的就是一个标准的框模型结构。在HTML网页中,每个可视元素(之所以强调可视是因为很多HTML元素其实不是用来显示的,例如用来表示语义的元素)的布局都是按照框模型来设计的。网页通过对元素设置这些样式属性,就可以达到特定的布局效果。
框模型中的最边缘部分分别是四个方向上的外边距(TM、RM、BM、LM),可以为这四个外边距设置不同的大小,图中特意将这些方向上的外边距绘制得不一样,也是表明了这个含义。图中外边距往内是四个方向上的边框(左边框、右边框、上边框、下边框),再次是四个方向上的内边距(同边框类似),最后是该元素显示自己的内容所使用的区域。
CSS标准的框模型结构
示例代码也包含了框模型的使用方法,但是不足以完全展示这一模型。所以笔者将它略作修改,变成示例代码所示的代码。笔者希望通过专门的框模型示例和显示结果来帮助读者更直观清晰地理解该模型。
示例代码 框模型的HTML网页代码片段
CSS代码:
#adiv/*ID选择器*/ { width:300px;/*宽度*/ background-color:#efefef;/*背景色*/ border:2px solid black;/*边框*/ } .aclass/*类选择器*/ { border:5px solid red;/*边框*/ margin:150px 100px 10px 40px;/*外边距*/ padding:15px 20px 25px 30px;/*内边距*/ color:green;/*颜色*/ }
HTML代码:
<p> This is a test to demonstrate the mechanism of layout!</p> <div id="adiv"> <div class="aclass">A B C D E F G H I J K L M N O P Q RS T</div> </div>
图是示例代码在Chrome浏览器中的显示结果。ID为“#adiv”的div元素被设置了边框是为了让读者了解它的内容区域。图中最大的区域就是该div元素的内容区域,最外边的黑色边框就是它的边框。该元素的内部就是“.aclass”的类选择器所选择的div元素的包含块,笔者后面会介绍包含块的概念。包括块内部就是类型为“.aclass”的div元素的框模型显示结果,这也是笔者希望描述的框模型结构。为了便于读者对框模型的理解,在显示区域旁边的标注表明了框模型的各个属性值,可以同图的框模型进行对照,看看实际的效果是怎么样的。
图示例代码的框模型显示结果
框模型是布局计算的基础,渲染引擎可以根据模型来理解该如何排版元素以及元素之间的位置关系。
1.5 包含块(Containing Block)模型
当WebKit计算元素的箱子的位置和大小时,WebKit需要计算该元素和另外一个矩形区域的相对位置,这个矩形区域称为该元素的包含块。上面介绍的框模型就是在包含块内计算和确定各个元素的,包含块的具体定义如下。
- 根元素的包含块称为初始包含块,通常它的大小就是可视区域(Viewport)的大小。
- 对于其他位置属性设置为“static”或者“relative”的元素,它的包含块就是最近祖先的箱子模型中的内容区域(Content)。
- 如果元素的位置属性为“fixed”,那么该元素的包含块脱离HTML文档,固定在可视区域的某个特定位置。
- 如果元素的位置属性为“absolute”,那么该元素的包含块由最近的含有属性“absolute”、“relative”或者“fixed”的祖先决定,具体规则如下:如果一个元素具有“inline”属性,那么元素的包含块是包含该祖先的第一个和最后一个inline框的内边距的区域;否则,包含块则是该祖先的内边距所包围的区域。
结合实例来讲,类型为“aclass”的div元素的包含块就是其父亲的内容区域,其框模型就是在该内容区域上进行计算生成得来的。
1.6 CSS样式属性
CSS标准中定义了各式各样的样式属性,用来描述元素的显示效果。示例代码6-1的第6行到第16行设置了选择的元素的样式属性值,笔者大致把这些属性分成以下类别。
- 背景: 通常有两种方式来设置元素的背景,一种是设置背景颜色(例子中的background-color),另外一种是设置背景图片。
- 文本: 设置文本缩进、对齐、单词间隔、字母间隔、字符转换、装饰和空白字符等。
- 字体: 设置字体属性,可以是内嵌的,也可以是自定义字体的方式,另外还可以设置加粗、变形等属性。
- 列表: 设置列表类型,可以以字母、希腊字母、数字等方式编号列表。
- 表格: 通过设置边框来达到显示表格的视觉效果的目的。设置是否把表格边框合并为单一的边框,设置分隔单元格边框的距离,设置表格标题的位置,设置是否显示表格中的空单元格,设置显示单元、行和列的算法等。
- 定位: CSS提供元素的相对、绝对定位和浮动定位。示例使用了绝对定位,参见示例代码中的第6到第8行。
1.7 CSSOM(CSS Object Model)
想象一下上面两个示例代码中关于CSS部分的代码,读者会发现它们都是静态的,那么CSS有没有提供一些方法可以让开发者自定义一些脚本去操作它们的状态呢?这就是CSSOM,称为CSS对象模型。它的思想是在DOM中的一些节点接口中,加入获取和操作CSS属性或者接口的JavaScript接口,因而JavaScript可以动态操作CSS样式。DOM提供了接口让JavaScript修改HTML文档,同理,CSSOM提供了接口让JavaScript获得和修改CSS代码设置的样式信息,这听起来非常酷吧,确实是这样的。
对于内部和外部样式表,CSSOM定义了样式表的接口,称为“CSSStyleSheet”,这是一个可以在JavaScript代码中访问的接口。借助于该接口,开发者可以在JavaScript中获取样式表的各种信息,例如CSS的“href”、样式表类型“type”、规则信息“cssRules”等,甚至可以获取样式表中的CSS规则列表。这个接口同DOM中的“Script”节点或者“Link”节点不一样,它是CSSOM定义的新接口。开发者可以通过document.stylesheets查看当前网页中包含的所有CSS样式表,这是因为CSSOM对DOM中的Document接口进行了扩展,下面是新加入的属性。
partial interface Document{ readonly attribute StyleSheetList styleSheets; attribute DOMString? selectedStyleSheetSet; readonly attribute DOMString? lastStyleSheetSet; readonly attribute DOMString? preferredStyleSheetSet; readonly attribute DOMString[] styleSheetSets; void enableStyleSheetsForSet(DOMString? name); };
通过上面这些属性,开发者甚至可以动态选择使用哪些CSS样式表。获取的样式表就是前面定义的CSSStyleSheet对象,JavaScript代码可以修改这些对象的属性,非常便于使用。
W3C还定义了另外一个规范,那就是CSSOM View,它的基本含义是增加一些新的属性到Window、Document、Element、HTMLElement和MouseEvent等接口,这些CSS的属性能够让JavaScript获取视图信息,用于表示跟视图相关的特征,例如窗口大小、网页滚动位移、元素的框位置、鼠标事件的坐标等信息。这些特征在很多浏览器中获得了支持,它们非常有用,下面以CSSOM View对Window的扩展为例,笔者省略了一些属性。
partial interface Window{ MediaQueryList matchMedia(DOMString media_query_list); readonly attribute Screen screen; //viewport readonly attribute long innerWidth; readonly attribute long innerHeight; //viewport scrolling readonly attribute long scrollX; readonly attribute long pageXOffset; readonly attribute long scrollY; readonly attribute long pageYOffset; … };
1.8 实践:理解CSSOM和选择器
本节中,笔者希望通过例子来理解CSSOM标准定义的内容,结合选择器的匹配方法来加深对CSS技术的认识。
图是一个代码示例图和在Chrome浏览器的控制台中的信息。下面按照步骤逐步分析这一例子。
图理解CSSOM和选择器的示例代码和控制台语句
- 同样是在Chrome浏览器中运行左边的网页,同时打开Chrome的开发者工具,切换到控制台这一功能。
- 读者会看到“Test CCSOM1”字符串的颜色是绿色的,而“Test CCSOM2”字符串的颜色是红色的。这是因为例子中第一个“div”元素只匹配到第二个样式表中的规则。而第二个“div”元素,虽然两个规则对它而言都可以匹配,但是类规则的优先级更高,因而它的结果是红色。
- 在控制台中输入JavaScript语句“document.styleSheets”,读者可以看到当前有两个CSSStyleSheet对象,单击查看它们的属性和属性值,跟前面的标准对比一下。
- 在控制台中输入JavaScript语句“document.styleSheets[0].disabled=true”,读者在浏览器中会发现“Test CCSOM2”变成绿色的了,这是因为将第一个样式表关闭了,所以它不再起作用了。
- 尝试在开发者工具的控制台中输入如下JavaScript代码“document. styleSheets[1].cssRules[0].style.color=‘gray’”,读者会看到所有字符都变成灰色的了,这是因为这条语句将第二个样式表中的第一个规则中的字体颜色设置成了灰色,浏览器即刻生效。
还可以在控制台中尝试一下其他的JavaScript语句,或者在更复杂一些的网页里面理解规则,这里只是描述基本原理。