作者:vivo 互联网前端团队 - Zhang Jiqi
本文主要讲述了CSS中的级联层(CSS@layer),讨论了级联以及级联层的创建、嵌套、排序和浏览器支持情况。级联层可以用于避免样式冲突,提高代码可读性和可维护性。
我们参看Cascading and Inheritance Level 5(13 January 2022) 中6.4节所述:
级联层提供了一种结构化的方式来组织和平衡单个起源中的问题。单个级联层内的规则级联在一起,不与层外的样式规则交错。
开发者可以创建层来表现元素默认值、第三方库、主题、组件、覆盖和其他样式问题,并且能够以显式方式重新排序层级,而无需更改每个层内的选择器或特异性,或依赖源顺序来解决跨层的冲突。
单纯看官方定义和概括可能比较晦涩,下面我们会结合案例来说清楚。
简而言之:级联层的出现就是为了使 CSS 开发者可以更简单直接地控制级联。
我们来假设日常开发中的一个场景,从场景去理解级联层在解决什么问题。
如下图:
我们原来的'display'文案是红色,当我们引入了一个第三方组件库,第三方库中有以下样式。
/* 开发者样式 */
.item {
color: red;
}
/* 第三方库 */
#app .item {
color: green;
border: 5px solid green;
font-size: 1.3em;
padding: 0.5em;
width: 120px;
}
就会导致'display'文案变成绿色。
我们想要将'display'文案的颜色由绿色改成红色一般的手段是增加选择器特异性(优先级)。比如在开发页面中对开发者样式进行修改:
/* 开发者样式 */
#app div.item {
color: red;
}
/* 第三方库 */
#app .item {
color: green;
border: 5px solid green;
font-size: 1.3em;
padding: 0.5em;
width: 120px;
}
或者借助级联中出场顺序对优先级的影响在用户页面中重写
/* 第三方库 */
#app .item {
color: green;
border: 5px solid green;
font-size: 1.3em;
padding: 0.5em;
width: 120px;
}
/* 开发者样式 */
#app .item {
color: red;
}
效果如下:
再举个例子:
比如有可能第三方组件写了
a { color: blue; }
那项目开发中由于引入这个第三方组件 就会导致样式污染,因为第三方库的样式往往都在项目设置的通用样式比如common.css后加载。
如果后面想在代码中覆盖某些属性,使用高特异性选择器的语句就可能会导致问题。然后因为有问题就会选择更高特异性的择器的语句或使用!important,这会使代码变得冗长也可能会带来副作用。低特异性选择器的语句很容易被后面出现在代码中的语句覆盖。在自己的代码之后加载第三方 CSS 时特别会出现这种问题。
所以级联层就是为了解决以上场景出现的,级联层在级联中的的位置是在内联样式和选择器特异性之间。当有些css声明就是设置要低优先级且不受选择器特异性影响那么使用级联层再合适不过。
运用级联层解决第一个日常开发场景痛点的css代码如下:
/* 排序层 */
@layer reset, lib;
/* 通用样式 */
@layer reset {
#app .item {
color: black;
width: 100px;
padding: 1em;
}
}
/* 第三方库样式 */
/*我们将第三方库的样式全部放到lib层*/
/*这里一般使用@import导入的方式*/
/*为了示例简单我们简化了操作*/
@layer lib {
#app .item {
color: green;
border: 5px solid green;
font-size: 1.3em;
width: 130px;
}
}
/* 开发者样式 */
.item {
color: red;
}
为了知道为什么上面的css代码能解决冲突问题,更好地理解级联层的作用,理解一些现象背后的根因,了解级联层和级联的关系,我们继续往下看。
CSS中有两个重要的基础规则,一个是继承,一个是级联。
指的是类似 color,font-family,font-size,line-height 等属性父元素设置后,子元素会继承的特性。
可以简单理解为是CSS 用来解决要应用于元素的具体样式的算法。也就是基于一些优先级排序输出给给定元素上属性值一个级联值。级联值是级联的结果。
我们参看Cascading and Inheritance Level 5(13 January 2022) 中6.1节,
相比于Cascading and Inheritance Level 4(14 January 2016) 中的定义有明显变化。
最重要的变化就是增加了级联层。由此级联排序变成:
起源和重要性(Origin and Importance)
上下文(Context)
样式属性(Element-Attached Styles)
层(Layers)
特异性(Specificity)
出场顺序(又名源代码顺序)(Order of Appearance)
浏览器在确定最终元素样式呈现的时候,会依据这些准则按照优先权从高到低排序,并且会一个一个的检查,直到确定最终样式。
css中每个样式规则有三个核心起源,它决定了它进入级联的位置,理解起源优先级是理解级联的关键。
用户代理来源(浏览器内置样式)
用户来源(浏览器的用户设置 )
作者来源(Web开发者)
Css声明的起源取决于它来自哪里,重要性在于它是否用!important声明。
各种起源的优先级按降序排列:
过渡
重要的用户代理
重要用户
重要作者
动画
普通作者
普通用户
普通用户代理
越靠前来源的声明优先级越高。过渡和动画我们可以暂时忽略。
通常作为开发者,!important会被我们视为一种增加特异性的方法,用以覆盖内联样式或特异性较高的选择器。
但是!important设计出来的初衷是作为整体级联中的一个特性,来平衡开发者、用户设置和浏览器内置之间对css优先级的影响能力。
默认情况下三者的优先级是:作者来源> 用户来源>用户代理来源(可以参看上文起源优先级中6-8的排序)。但是当css声明添加!important之后会使它们的优先顺序颠倒(参看上文起源优先级中2-4的排序)。
如果站在浏览器和用户的角度看!important提供了在必要时重获优先级控制权的能力,而非只是简单的增加特异性。
举个例子:
浏览器默认样式表包含我们开发者无法覆盖的重要样式。
浏览器对具有'hidden'类型的input输入框设置了默认的展示属性并且将其声明为重要。
input[type=hidden i] { display: none !important; }
看下面两张图例:
第一张可以看出我们对具有'hidden'类型的input输入框的展示属性设置成了显示并且声明为重要
第二张是样式最终计算结果:隐藏
从上面的浏览器表现可以看到我们作为开发者在作者样式表中设置的规则没能覆盖用户代理样式表中的相同规则。
这验证了上面说的:在级联中!important会颠倒三大核心起源默认优先顺序。
验证的过程中还发现了一个chrome控制台这边的bug,看上面的第一张图例:没生效的规则不划删除线,生效的反而划删除线了。
再看一个官方文档的例子加强一下理解:
font-size的值最终是‘12pt ’。
因为作者样式表中添加!important的规则优先权高于用户样式表中普通规则。
text-indent的值最终是‘1em’。
因为虽然两个样式表都标注了!important,但是标注!important用户声明优先级大于标注!important作者声明。
下图可以帮助我们直观的理解级联以及级联层在级联中的位置:
图片来源:css-tricks
我们会发现平时操作最多的选择器特异性(selector specificity)只是级联中的一小部分。也轻松地理解了为什么内联样式优先级天然高。同时我们会发现!important在级联中有特殊地位。他穿插在级联规则的各个阶段并能颠倒优先级。
我们来看MDN上的定义:
The @layer CSS at-rule is used to declare a cascade layer and can also be used to define the order of precedence in case of multiple cascade layers.
也就是说 @layer 这个at-rule(AT规则) 用于声明级联层(cascade layer),也能用于定义多个级联层的优先级。
At-rules 是什么?
At-rules是指导 CSS 如何表现的CSS 语句。它们以 at 符号 ' @' ( U+0040 COMMERCIAL AT) 开头,后跟一个标识符,包括下一个分号 ' ;' ( U+003B SEMICOLON) 或下一个CSS 块之前的所有内容。
我们开发常见的at-rule有@charset、@media、@font-face 、@keyframes 等。
@layer的句法如下:
@layer layer-name {rules}
@layer layer-name;
@layer layer-name, layer-name, layer-name;
@layer {rules}
可以通过多种方式创建级联层。
第一种方法是:创建命名的级联层,其中包含该层的 CSS 规则,如下所示:
@layer green {
.item {
color: green;
border: 5px solid green;
font-size: 1.3em;
padding: 0.5em;
width: 120px;
}
}
@layer special {
.item {
color: rebeccapurple;
}
}
第二种方法是:创建一个命名的级联层而不分配任何样式。这可以是单层,如下所示:
@layer green;
可以一次定义多个层,如下:
@layer green, special
一次定义多个层有什么好处呢?
因为声明层的初始顺序决定了层的优先级。与声明一样,如果在多个层中找到声明,最后定义的层声明将最终生效。看下面代码:
@layer green,special;
@layer green {
#app .item {
color: green;
border: 5px solid green;
font-size: 1.3em;
padding: 0.5em;
width: 120px;
}
}
@layer special {
.item {
color: rebeccapurple;
}
}
效果如下图:
special层中item样式规则将被应用即使它的特异性低于 green层中的规则。这是因为一旦层顺序定义完成,就会忽略选择器特异性。
同样也会忽略出现的顺序:
我们声明层名称并设置它们的顺序后,可以通过重新声明名称来将 CSS 规则添加到图层。然后将样式附加到层,并且层顺序不会更改。比如如下代码虽然@layer green重新声明了并在文件后方但是由于顺序一开始已经设置所以字体颜色还是紫色:
@layer green,special;
@layer special {
.item {
color: rebeccapurple;
}
}
@layer green {
.item {
color: green;
border: 5px solid green;
font-size: 1.3em;
padding: 0.5em;
width: 120px;
}
}
效果如下:
忽略选择器特异性和代码出现顺序可以让我们创建更简单的 CSS 选择器,并使代码优雅,因为不必确保选择器具有足够高的特异性来覆盖其他css规则,只需要确保它出现在后面的层中。
第三种方法是:创建一个没有名称的级联层。例如:
@layer {
.item {
color: black;
}
}
这将创建一个匿名级联层,该层功能与命名层相同。但是使用匿名层有如下缺点:
可读性较差:匿名层没有名称,会导致样式表不易阅读和理解。特别是在大型项目中,可能会出现样式不断增加,难以跟踪和管理的问题。
难以扩展:如果稍后想要更改特定样式或组合,也会很难找到特定的代码块。
不可复用性:匿名层中的样式不能在其他地方重复使用或引用。这样会使样式表更难以管理和维护。
平时我们尽量避免使用匿名层。但当我们是样式库的作者,并想将某些css代码不被使用者修改可以借助匿名层做到这一点。
第四种方法是:使用@import。CSS 原生支持 @import 导入其他 CSS 文件。
@import url(index.css) layer(index);
同时,也支持匿名引入,例如:
@import url(index.css) layer;
我们在使用@import时候需要放在除@charset之外的样式规则前,否则无法导入。
可能的第五种方式仍在讨论中:通过元素上的属性。请参阅[css-cascade] Provide an attribute for assigning ato a cascade layer #5853。
图层可以嵌套。例如:
@layer base {
p { max-width: 70ch; }
}
@layer framework {
@layer base {
p { margin-block: 0.75em; }
}
@layer theme {
p { color: #222; }
}
}
生成的层可以表示为一棵树:
1.base
framework
base
2.theme
或可以用扁平列表表示
base
framework.base
framework.theme
要将样式附加到嵌套层,您需要使用以下全名来引用它:
@layer framework {
@layer default {
p { margin-block: 0.75em; }
}
@layer theme {
p { color: #222; }
}
}
@layer framework.theme {
/* 这些样式会被添加到framework层里面的theme层 */
blockquote { color: rebeccapurple; }
}
看效果:
级联层按照它们声明的顺序排序。第一层优先级最低,最后一层优先级最高。但是,未分层的样式具有最高优先级:
/* 未分层 */a { color: green; }
@layer layer-1 { a { color: red; } }
@layer layer-2 { a { color: orange; } }
@layer layer-3 { a { color: yellow; } }
优先级顺序如下:
未分层样式
layer-3
layer-2
layer-1
看下图示例:
层可以在一个地方被定义图层(以建立图层顺序),然后在任何地方添加样式
/* 定义在一个地方 */
@layer my-layer;
/* 其他样式*/
...
/* 在某个地方添加样式 */
@layer my-layer { a { color: red; } }
/* 未分层 */ a { color: green !important; }
@layer layer-1 { a { color: red !important; } }
@layer layer-2 { a { color: orange !important; } }
@layer layer-3 { a { color: yellow !important; } }
任何加上重要声明的样式都以相反的顺序应用
优先级顺序如下:
!important layer-1
!important layer-2
!important layer-3
!important 未分层样式
看下图示例:
这里我们看到对应规则在chrome浏览器的显示是正确的。但是在开发者控制台中的样式一栏规则显示有问题。应该是chrome浏览器开发者控制台的bug。
@layer layer-1 { a { color: red; } }
@layer layer-2 { a { color: orange; } }
@layer layer-3 {
@layer sub-layer-1 { a { color: yellow; } }
@layer sub-layer-2 { a { color: green; } }
/* 未嵌套 */ a { color: blue; }
}
/* 未分层 样式 */ a { color: black; }
优先级顺序如下:
未分层 样式
layer-3
-layer-3 未嵌套
-layer-3 sub-layer-2
-layer-3 sub-layer-1
layer-2
layer-1
以下层顺序将取决于匹配的媒体条件:
例如:
@media (min-width: 600px) {
@layer layout {
.item {
font-size: x-large;
}
}
}
@media (prefers-color-scheme: dark) {
@layer theme {
.item {
color: red;
}
}
}
如果两个媒体查询的规则中匹配一个那么对应的级联层生效。如果两者都匹配,那么图层顺序将为layout, theme都生效。如果两个都不匹配则不定义层。下图是两者都匹配的场景。
目前所有现代浏览器均已经支持 @layer 规则。所有浏览器厂商都支持的特性未来一定比较实用。
SS 的标准化流程由 W3C Cascading Style Sheets Working Group (CSSWG)——W3C层叠样式列表小组以及独立CSS专家组成。W3C 本身并不制定标准,而是作为一个论坛式的平台,接收来自小组成员的提交,并通过会议来商讨制定标准,所有的提交以及讨论都是公开透明的,可以在 W3C 网站上看到会议的记录,可以简单分为4个大阶段:
工作草案( WD )
候选人推荐( CR )
提议的建议( PR )
W3C 推荐( REC )
下图可以帮助理解:
图片来源:w3.org
W3C 通过状态码表示规范的成熟度。成熟度从低到高排序如下图。
图片来源:w3.org
再看下图:包含layer概念的标准讨论已经到达CR阶段。
图片来源:w3.org
W3C 鼓励从 CR阶段的标准 开始可以作为日常使用。
最后,我们回到通过级联层如何解决“引入了一个第三方组件库导致样式覆盖“的问题上。
css代码如下:
/* 排序层 */
@layer reset, lib;
/* 通用样式 */
@layer reset {
#app .item {
color: black;
width: 100px;
padding: 1em;
}
}
/* 第三方库样式 */
/*我们将第三方库的样式全部放到lib层,这里一般使用@import导入的方式,为了示例简单我们简化了操作*/
@layer lib {
#app .item {
color: green;
border: 5px solid green;
font-size: 1.3em;
width: 130px;
}
}
/* 开发者样式 */
.item {
color: red;
}
我们将第三方库的样式全部放到lib层,将需要重置的一些样式放到reset层,自己开发的样式不放入层中(当然你也可以放入到一层然后排序在最后)。由此我们实现了样式的分层解决了第三方组件库导致样式覆盖的问题,而且做到开发者样式简单不冗长。
效果如下:
级联层(CSS@layer)已经历概念提出到到浏览器全面支持的阶段。也许在不久的将来大家都会普遍使用它,期望本文能给大家带来一定帮助。
参考资料: