历数技术进步的代价时,弗洛伊德遵循的路线使人感到压抑。他同意塔姆斯的评论:我们的发明只不过是手段的改进,目的却未见改善。
——尼尔波斯曼《技术垄断》
虽然开发工具早已经从 preprocessor 进化到了 styled component 甚至是 functional css,但在我看来新的工具并没有让我们的样式代码写的更好,只是更快——也可能会让代码坏的更快。工具的繁荣并没有让那些导致代码难以维护的根本问题烟消云散,而是更易让我们对其视而不见。这篇文章旨在回答一个问题:为什么样式代码难以写对,它的陷阱究竟在哪里?
如果一本正经的聊架构,套路多半是按照某些重要的特征依次展开讲解。但这些所谓的重要特征其实在编程领域中是放之四海而皆准的,例如“扩展性”、“可复用”、“可维护性”等等,按这种思路聊,空谈大于应用。所以我们不如通过解决某个具体的样式问题,来审视样式代码应该如何编写和组织
下图是一个非常简单的 popup 组件,我们会以它的样式开发过程串起整篇的内容。
我们首先以一种简单粗暴的方式来实现它,直觉上看,实现这个 popup 只需要三个元素即可:div 是最外面的容器,h1 用于包裹 "Success" 文案,button 用来实现按钮
<div class="popup">
<div>Success</div>
<button>OK</button>
</div>我不会完整的写出它的完整样式,只大概列出其中一些关键属性
.popup {
display: flex;
justify-content: space-around;
padding: 20px;
width: 200px;
height: 200px;
div {
margin: 10px;
font-size: 24px;
}
button {
background: orange;
font-size: 16px;
margin: 10px;
}
}第一版实现即完成了。目前看来并没有什么不妥。
问题不在于实现而是在于维护。接下来我就以一些常见的实际需求变更来看看上面的代码存在怎样的问题。
假设现在需要在“Success”下方新增一个元素用于展示成功的具体信息
想当然的我们需要新增一个 div 标签。但如果这样的话上面样式中的 .popup div 样式就会同时对这两个 div 产生同样的效果,这并不是我们希望的,很明显这两个元素的样式是不同的。OK,如果你坚持使用标签作为选择器的话,你可以使用伪类选择器 nth-child 来区分样式:
.popup {
div:nth-child(1) {
margin: 10px;
font-size: 24px;
}
div:nth-child(2) {
margin: 5px;
font-size: 16px;
}但如果某一天你认为"Success"应该使用 h1 而非 div 封装更为恰当的话,那么修改的成本则是:
但如果你一开始就能给 button 和 div 一个确切的 class 名称,那么当你修改 DOM 元素时也仅仅需要修改 DOM 元素,而无需修改样式文件了
上面举得这个例子是水平拓展的情况,也就是说我在某一元素的同一级新增一个元素。纵向拓展也会出现同样的问题,你可以完全想象的出类似于这样的选择器:
.popup div > div > h1 > span {
}
.popup {
div {
div {
span {}
}
}
}无论是上面代码中的哪一种情况,样式是否生效都极度依赖于 DOM 结构。在一连串的 DOM 标签的层级关系中,哪怕只有一个元素出现了问题(可能是元素标签类型发生了修改,还有可能是在它之上新增了一个元素)都会导致样式大面积失效。同时这样的做法也会让你复用样式难上加难,如果你希望复用 .popup div > div > h1 > 的样式,你不得不将 DOM 结构也拷贝到想要复用的地方。
所以这里我们至少能得出一个结论:CSS 不应该过分的依赖 HTML 结构
而之所以加上“过分”二字,是因为样式完全无法脱离结构独立存在,例如 .popup .title .icon 这样的的依赖关系背后就暗示了 HTML 结构的大致轮廓。
所以我们可以继续将上面的原则稍作更正:CSS 应该拥有对 HTML 的最小知识。理想情况下一个 .button 样式无论应用在任何元素上看上去都应该像同一个立体的可点击按钮。
上一节中我们开发完毕的组件通常会在页面上被多处引用,但总存在个别场景需要你对组件稍作修改才得以适配。假设有一个需求是希望把这个 popup 应用在他们的移动端网站上,但为了适配动设备,某些元素的有关尺寸例如长宽内外边距等都要缩小,你会怎么实现?
我见过的 90% 的解决方案都是以添加父元素的依赖进行实现,也就是判断该组件是否在某个特定的 class 下,如果是的话则修改样式:
body.mobile {
.popup {
padding: 10px;
width: 100px;
height: 100px;
}
}但如果此时你需要给平板设备添加一个新的样式,我猜你可能会再添加一个 body.tablet { .popup {} } 代码。又如果移动端网站有两处需要使用 popup ,那么你的代码很最终会变成这样:
body.mobile {
.sidebar {
.popup
}
.content {
.popup
}
}这样的代码依然是难以复用的。如果某位开发者看到了移动端网站 popup 打开的样式很喜欢,然后想移植到另一处,那么单纯引入 popup 组件是不够的,他还需要找到真正的生效的代码,将样式和 DOM 层级都复制粘贴过去。
在一个组件自身已经拥有样式的情况下,过分的依赖父组件间接的调整样式,是一种 case by case 的编码行为,本质上这架空了 popup 自带样式。假设 popup 自带 box-shadow 的样式属性,但在有的用例里,box-shadow 可能会被加重,而在有的用例里,box-shadow 又可能会消失,那么它自带的 box-shadow 根部本就没有意义了,因为它永远不会生效。
架空违背了“最小惊讶原则”,给后续的维护者带来了“惊喜”。如果此时 popup 的设计稿发生了修改,阴影需要减少,则修改它自身的样式是不会生效的,或者说无法在每一处生效。而至于还有哪些地方无法生效,为什么它们无法生效,维护者并不知道,他同样需要 case by case 的去查看代码。这么做无疑增加了修改代码的成本.
解决这个问题并不像解决 DOM 依赖问题那么简单,需要我们多管齐下。
想提高代码的可维护性,分离关注点永远是屡试不爽的手段。纵观现有的各类组织样式的方法论,比如 SMASS 或者是 ITCSS,对样式进行适当的角色划分是它们的核心思想之一。
我们以一个完整的 popup 样式为例:
.popup {
width: 100px;
height: 30px;
background: blue;
color: white;
border: 1px solid gary;
display: flex;
justify-content: center;
}在这一组样式中,我们看到
根据这些特点和常见的规范,可以考虑从下面几个维度对样式进行分离:
从表面上看,这种行为只是将样式(尺寸)从一个组件转移到另一个组件(容器)上,但却从根本上解决了我们上面提到的父元素依赖的困恼。任何想使用 popup 的其他组件,不用再设法关心 popup 组件的尺寸是如何实现的,它只需要关自己。
进一步从深层次上说,它消灭了依赖。你可能没有注意到,flex 布局的样式配置遵循的就是这种模式:当你想让你孩子元素按照某种规则布局的话,你只需要修改父元素和 flex 布局样式属性即可,完全不用再在孩子元素的样式上做出修改。
我个人认为另一个反模式的例子是 text-overflow: ellipsis 属性,单一的该样式属性是不足以自动省略容器内的文字,容器还需要满足 1) 宽度必须是 px 像素为单位 2) 元素必须拥有 overflow:hidden 和 --tt-darkmode-color: #A3A3A3;">而至于布局功能元素是与父元素为同一元素,还是独立元素,我倾向于后者,毕竟几个 markup 代码并不会给我们添加多少负担,但清晰的职责划分却能给我们将来的维护带来不少便利。
在这个前提下任何给 popup 添加的布局样式实际上都意味这你新增了隐性依赖,因为你实际上是在暗示:它在这个父容器下的这个 margin 值看上去刚好。
通常我们不会只需要单一样式的按钮,可能还需要带有红底白字的错误样式的按钮,还需要黄底白字的警告样式按钮。这种用例常见的解决方案不是新建 N 个不同的按钮样式,比如 primary-button, error-button(这样务必会出现很多公共的 button 代码),而是在一个 button 样式的基础上,通过提供样式的“修饰”类来达到最终的目的。例如基础款的按钮 class 名称为 button, 如果你想让它变得带有警告样式的话,只需要同时使用 error 的 class 名称即可。
<div className="button error"></div>从本质上说这也是一种关注点的分离,只不过从这个角度上看它关心的是“变”与“不变”。我们将“变量”统统转移到“修饰”类中。
但这种方案在实现时会遇到不少问题,首先是修饰类的设计,例如当我在定义例如 error, primary, warning 的修饰类时,究竟哪些样式属性是我可以覆盖的哪些是不可以,这必须有事前约定。否则某人在写 error 样式时,可能会无脑的覆盖原 button 上的样式直到看上去满意为止。它依赖于抽象能力,但糟糕的抽象比不抽象还要难以维护。
组件并非是封装样式的唯一单位,在一个网站中,还可能存在诸如 base、reset 这种全局或者说切面性质的样式属性。我理想的模块化样式应该能够轻松达到以下的目的:
诠释这两点最好的例子是在进行响应式开发时,业内通用的对字体大小适配的解决方案。例如下面这个组件的 html 结构:
<div class="ancestor">
<div class="parent">
parent
<div class="child">
hello
</div>
</div>
</div>在样式中我们会设定:
.ancestor {
font-size: 1rem;
}
.parent {
font-size: 1.5em;
}
.child {
font-size: 2em;
}这样当我们需要根据设备调整字体大小时,只需要调整根元素 html 字体大小,那么页面上其他元素就会自我调节了。而如果我们只想调整局部样式时,我们只需要调整 .ancestor 的字体大小即可,不会影响到其他元素。
你阅读到这里不难看出来,样式难写对的问题在于它太容易影响别的组件,也太容易受别的组件所影响了。绝大部分人遇到的问题是:
解决这个问题的办法早就有了,那就是样式的隔离。比如在 Angular 中,它是靠给元素添加随机属性并且给样式附带上属性选择器来实现的,例如你同时创建了 page-title 组件和 section-title 组件,它们都拥有 h1 元素的样式,但是在编译之后你看到的 css 分别是:
h1[_ngcontent-kkb-c18] {
background: yellow;
}
h1[_ngcontent-kkb-c19] {
background: blue;
}这样所有的 h1 元素样式都不会被互相影响。
Pre-Processer
无论你主观上多么想避免以上的所有问题,给样式一个好的整洁架构。在实现的过程中,我们依然会不小心掉入工具的陷阱中。
再一次回到我们上面提到的 popup 样式:
.popup {
width: 100px;
height: 30px;
background: blue;
color: white;
}假如你发现 { background: blue; color: white; } 作为常见样式出现频繁,希望对它进行复用,在使用 Sass 编程的前提下很明显此时你有两个选择:@mixin 或者 @extend。
如果采用 mixin,代码如下:
@mixin common {
background: blue;
color: white;
}
.popup {
@include common;
}
而如果采用 extend:
.common {
background: blue;
color: white;
}
.popup {
@extend .common;
}第一个问题是,无论你选择哪种模式,你都很难说开发者是有意在依赖抽象还是在依赖实现。我们可以把 @mixin common 和 .common 解读为对一种抽象的封装,但很有可能后续的消费者只是想复用 background 和 color 而已。一旦如此,common 模块就变得难以修改,因为对任意一个属性的修改都会影响到未知的若干个模块。
在 SASS 中虽然我们可以给类名添加参数,把它当作参数相互传递,但它与我们实际编程中的变量和函数并不相同:JavaScript 中的函数我们往往只关心它的输入与输出,只是定义函数并不会对程序的结果造成影响。而当你在定义样式类的那个时刻就已经可能对页面产生了影响,并且其中的每一条属性都会产生影响。
如果你听说过“组合优于继承”,我相信会对这一点有更深刻的体验。你可以回想继承体系中存在的副作用,例如继承打破了对超类的封装,子类不能减少超类的接口等等,在 SASS 的这类复用关系中都能找到相似的影子。
extend 相比 mixin 更危险的地方在于,它破坏了我们一如既往组织模块的方式。
例如目前已有一个 page 页面,其中拥有一组 page-title 的样式:
.page {
.page-title {
.icon {
width: 10px;
}
.label {
width: 100px;
}
}
}现在 card-title 想通过 extend 来复用它:
.card-title {
@extend .page-title;
}那么编译之后的结果看上去会非常奇怪:
.page .page-title .icon, .page .card-title .icon {
width: 10px;
}
.page .page-title .label, .page .card-title .label {
width: 100px;
}哪怕你没有听说过 BEM,你的编程经验也应该会告诉你 page 和 card 的样式应属于不同的模块。但事实上编译后的结果更像是优先考虑复用,从横切面强行把二者耦合在一起。
而如果你尝试将公共的 title 样式抽象为 mixin,再在 page-title 和 card-title 中进行复用:
@mixin title {
.icon {
width: 10px;
}
.label {
width: 100px;
}
}
.page {
.page-title {
@include title
}
}
.card-title {
@include title
}编译的结果如下:
.page .page-title .icon {
width: 10px;
}
.page .page-title .label {
width: 100px;
}
.card-title .icon {
width: 10px;
}
.card-title .label {
width: 100px;
}很明显 page 和 card 的样式更泾渭分明。
An Necessary Evil
如果你问我我是否会遵守上面自己写的每一条原则,我的答案是否定的。在实际开发中我倾向用便捷性换取可维护性。
在编程领域里面唯一不变的就是变化本身,无论在敲键盘之前你面向对象设计的多么准确,对组件拆分的多么恰当,任何业务上的变化都有可能让你所有的设计推倒重来。所以为了保证代码能够精确反馈业务知识的合理性,我们需要时常对代码设计重新设计。
你可以想象整个过程需要重新审视架构,从头阅读理解代码,修改完毕后验证。执行这一系列步骤需要不小的成本,还不包括其中的试错,以及因为重构而浪费的添加新功能的机会。更重要的是成本摆在那里,但收益却并不明显。
如果你的样式代码是基于 design system 之上的,那么你的改动成本会更高。因为你更不可能以个人的视角随心所欲的改动代码了,而是要自上而下的用整个产品的设计语言来衡量修改的合理性。
另一个更实际的问题是,代码从来不是依靠个人来维护。当这一套理论在团队内并没有达成共识,或者是大家只在理论层面了解过而实操时并不在意时,少数人的精心付出终究会化为泡影。代码在理想状态下应该最大成度上摒弃“人”这个因素成为流水线上工业化的产品。所以当我发现某个框架只有要求人们阅读完数十页最佳实践有关的文档才能写出符合官方标准的好代码时,那么现实工作中好代码出现的概率基本为0——在规范输出代码上,一则有效的 eslint 规则比十页文档都要强。而在本篇中叙述的各种原则属于后者。
然而 css 代码被写的乱七八糟又会怎样呢?产品坏了是肯定的,但相比其他 bug 有意思的事情是:
基于上面的三点,同时考虑到当下技术栈繁杂学习成本高,脚本开发工作量大,交付压力重,样式架构的正确性想当然是被牺牲掉的那一个。
最后重申我不鼓励这样的行为,这只是屈服于现实压力下其中的一种可能性而已。如果你所在的项目资源充足,以及大家有决心把事情做对,那也未尝不可。
Functional CSS
在我看来还有一类实践是游离于以上体系之外的,比如 tailwind 和 tachyons 。之所以将它们称之为“函数式”样式,是因为在这些框架不提供组件化、语义化的样式,比如 .card, .btn,而提供的是“工具类(utility class)”,比如 .overflow-auto,.box-content,它们 类似于函数式编程中没有副作用的纯函数。当你需要给你元素添加样式时,只需要给这个元素添加对应的 class 名称即可:
<div class="overflow-auto box-content float-left"></div>之所以说这种实践游离于以上体系之外,是因为它打破了我上面所说的前提:样式和 DOM 结构之间存在依赖关系。在这种编程模式下,因为不再存在“级联”关系,所以每个元素的样式都是独立的,互不影响。
如此看来这种模式简直就是天堂,本文里提及的所有问题都可以避免了:父元素依赖、角色耦合、预处理器里纠结的复用。
但仔细想想,这种方式是不是很 inline style 类似?用 inline style 也能解决我们所说的上述所有问题。我们是不是又回到了起点?
除了上面的问题外,我不再给出进一步推荐或者反对意见的原因在于,一方面这种实践存在很大的争议。另一方面我缺乏使用这类框架的经验。这里经验的判断标准不是“是否用过”,而是“是否长期投入到多人协作的大型项目中”——“长期”、“多人”、“大型”这几个关键词很重要。因为我们在做技术选型的时候,更多要考虑和现有项目的契合度、团队的适应成本,以及评估长远来看它能给我们带来巨大的好处是否能抵消替换它的成本。这些经验是我缺乏的。
文/Thoughtworks 李光毅
更多精彩洞见,请关注公众号Thoughtworks洞见
对于前端来说,HTML 都是最基础的内容。
今天,我们来了解一下 HTML 和网页有什么关系,以及与 DOM 有什么不同。通过本讲内容,你将掌握浏览器是怎么处理 HTML 内容的,以及在这个过程中我们可以进行怎样的处理来提升网页的性能,从而提升用户的体验。
不知你是否有过这样的体验:当打开某个浏览器的时候,发现一直在转圈,或者等了好长时间才打开页面……
此时的你,会选择关掉页面还是耐心等待呢?
这一现象,除了网络不稳定、网速过慢等原因,大多数都是由于页面设计不合理导致加载时间过长导致的。
我们都知道,页面是用 HTML/CSS/JavaScript 来编写的。
HTML由一系列的元素组成,通常称为HTML元素。HTML 元素通常被用来定义一个网页结构,基本上所有网页都是这样的 HTML 结构:
<html>
<head></head>
<body></body>
</html>其中:
HTML 中的元素特别多,其中还包括可用于 Web Components 的自定义元素。
前面我们提到页面 HTML 结构不合理可能会导致页面响应慢,这个过程很多时候体现在<script>和<style>元素的设计上,它们会影响页面加载过程中对 Javascript 和 CSS 代码的处理。
因此,如果想要提升页面的加载速度,就需要了解浏览器页面的加载过程是怎样的,从根本上来解决问题。
浏览器在加载页面的时候会用到 GUI 渲染线程和 JavaScript 引擎线程(更详细的浏览器加载和渲染机制将在第 7 讲中介绍)。其中,GUI 渲染线程负责渲染浏览器界面 HTML 元素,JavaScript 引擎线程主要负责处理 JavaScript 脚本程序。
由于 JavaScript 在执行过程中还可能会改动界面结构和样式,因此它们之间被设计为互斥的关系。也就是说,当 JavaScript 引擎执行时,GUI 线程会被挂起。
以网易云课堂官网为例,我们来看看网页加载流程。
(1)当我们打开官网的时候,浏览器会从服务器中获取到 HTML 内容。
(2)浏览器获取到 HTML 内容后,就开始从上到下解析 HTML 的元素。
(3)<head>元素内容会先被解析,此时浏览器还没开始渲染页面。
我们看到<head>元素里有用于描述页面元数据的<meta>元素,还有一些<link>元素涉及外部资源(如图片、CSS 样式等),此时浏览器会去获取这些外部资源。除此之外,我们还能看到<head>元素中还包含着不少的<script>元素,这些<script>元素通过src属性指向外部资源。
(4)当浏览器解析到这里时(步骤 3),会暂停解析并下载 JavaScript 脚本。
(5)当 JavaScript 脚本下载完成后,浏览器的控制权转交给 JavaScript 引擎。当脚本执行完成后,控制权会交回给渲染引擎,渲染引擎继续往下解析 HTML 页面。
(6)此时<body>元素内容开始被解析,浏览器开始渲染页面。
在这个过程中,我们看到<head>中放置的<script>元素会阻塞页面的渲染过程:把 JavaScript 放在<head>里,意味着必须把所有 JavaScript 代码都下载、解析和解释完成后,才能开始渲染页面。
到这里,我们就明白了:如果外部脚本加载时间很长(比如一直无法完成下载),就会造成网页长时间失去响应,浏览器就会呈现“假死”状态,用户体验会变得很糟糕。
因此,对于对性能要求较高、需要快速将内容呈现给用户的网页,常常会将 JavaScript 脚本放在<body>的最后面。这样可以避免资源阻塞,页面得以迅速展示。我们还可以使用defer/async/preload等属性来标记<script>标签,来控制 JavaScript 的加载顺序。
百度首页
对于百度这样的搜索引擎来说,必须要在最短的时间内提供到可用的服务给用户,其中就包括搜索框的显示及可交互,除此之外的内容优先级会相对较低。
浏览器在渲染页面的过程需要解析 HTML、CSS 以得到 DOM 树和 CSS 规则树,它们结合后才生成最终的渲染树并渲染。因此,我们还常常将 CSS 放在<head>里,可用来避免浏览器渲染的重复计算。
我们知道<p>是 HTML 元素,但又常常将<p>这样一个元素称为 DOM 节点,那么 HTML 和 DOM 到底有什么不一样呢?
根据 MDN 官方描述:文档对象模型(DOM)是 HTML 和 XML 文档的编程接口。
也就是说,DOM 是用来操作和描述 HTML 文档的接口。如果说浏览器用 HTML 来描述网页的结构并渲染,那么使用 DOM 则可以获取网页的结构并进行操作。一般来说,我们使用 JavaScript 来操作 DOM 接口,从而实现页面的动态变化,以及用户的交互操作。
在开发过程中,常常用对象的方式来描述某一类事物,用特定的结构集合来描述某些事物的集合。DOM 也一样,它将 HTML 文档解析成一个由 DOM 节点以及包含属性和方法的相关对象组成的结构集合。
我们常见的 HTML 元素,在浏览器中会被解析成节点。比如下面这样的 HTML 内容:
<html>
<head>
<title>标题</title>
</head>
<body>
<a href='xx.com'>我的超链接</a>
<h1>页面第一标题</h1>
</body>
</html>打开控制台 Elements 面板,可以看到这样的 HTML 结构,如下图所示:
在浏览器中,上面的 HTML 会被解析成这样的 DOM 树,如下图所示:
我们都知道,对于树状结构来说,常常使用parent/child/sibling等方式来描述各个节点之间的关系,对于 DOM 树也不例外。
举个例子,我们常常会对页面功能进行抽象,并封装成组件。但不管怎么进行整理,页面最终依然是基于 DOM 的树状结构,因此组件也是呈树状结构,组件间的关系也同样可以使用parent/child/sibling这样的方式来描述。同时,现在大多数应用程序同样以root为根节点展开,我们进行状态管理、数据管理也常常会呈现出树状结构。
我们知道,浏览器中各个元素从页面中接收事件的顺序包括事件捕获阶段、目标阶段、事件冒泡阶段。其中,基于事件冒泡机制,我们可以实现将子元素的事件委托给父级元素来进行处理,这便是事件委托。
如果我们在每个元素上都进行监听的话,则需要绑定三个事件;(假设页面上有a,b,c三个兄弟节点)
function clickEventFunction(e) {
console.log(e.target===this); // logs `true`
// 这里可以用 this 获取当前元素
}
// 元素a,b,c绑定
element2.addEventListener("click", clickEventFunction, false);
element5.addEventListener("click", clickEventFunction, false);
element8.addEventListener("click", clickEventFunction, false);使用事件委托,可以通过将事件添加到它们的父节点,而将事件委托给父节点来触发处理函数:
function clickEventFunction(event) {
console.log(e.target===this); // logs `false`
// 获取被点击的元素
const eventTarget=event.target;
// 检查源元素`event.target`是否符合预期
// 此处控制广告面板的展示内容
}
// 元素1绑定
element1.addEventListener("click", clickEventFunction, false);这样能解决什么问题呢?
常见的使用方式主要是上述这种列表结构,每个选项都可以进行编辑、删除、添加标签等功能,而把事件委托给父元素,不管我们新增、删除、更新选项,都不需要手动去绑定和移除事件。
如果在列表数量内容较大的时候,对成千上万节点进行事件监听,也是不小的性能消耗。使用事件委托的方式,我们可以大量减少浏览器对元素的监听,也是在前端性能优化中比较简单和基础的一个做法。
注意:
我们了解了 HTML 的作用,以及它是如何影响浏览器中页面的加载过程的,同时还介绍了使用 DOM 接口来控制 HTML 的展示和功能逻辑。我们了解了DOM解析事件委托等相关概念。
用 CSS 最困难的部分之一是处理CSS的权重值,它可以决定到底哪条规则会最终被应用,尤其是如果你想在 Bootstrap 这样的框架中覆盖其已有样式,更加显得麻烦。不过随着 CSS 层的引入,这一切都发生了变化。 这个新功能允许您创建自己的自定义 CSS 层,这是有史以来第一次确定所有 CSS 代码权重的层次结构。 在本文中,我将剖析这对您意味着什么,它是如何工作的,以及您今天如何开始使用它。
什么是层(Layers)
创建您自己的自定义图层是 CSS 的新功能,但图层从一开始就存在于 CSS 中。 CSS 中有 3 个不同的层来管理所有样式的工作方式。
浏览器(也称为用户代理)样式 - user agent style
用户样式 - User Styles
作者样式 - Author Styles
浏览器样式是应用于浏览器的默认样式。这就是为什么 Chrome 和 Safari 中的按钮看起来不同的原因。在浏览器层中找到的样式在浏览器之间是不同的,并且给每个浏览器一个独特的外观。
下一层是用户样式,这并不是您真正需要担心的事情。这些通常是用户可以编写并注入浏览器的自定义样式,但浏览器不再真正支持这些样式。用户可能会更改一些浏览器设置,这些设置会向该图层添加样式,但在大多数情况下,可以完全忽略该层。
最后,我们来到作者层。这是您最熟悉的层,因为您编写的每一段 CSS 代码都属于这一层。
这些层分开的原因是因为它可以很容易地覆盖浏览器样式和用户样式中定义的代码,因为层定义了自己的层次结构,完全忽略了权重的影响。
这 3 个 CSS 层是有序的(浏览器样式、用户样式、然后是作者样式),后面层中的每个样式都将覆盖前一层的任何样式。这意味着即使浏览器样式定义了一个超级特定的选择器,例如#button.btn.super-specific,并且您的作者样式定义了一个超级通用的选择器,例如按钮,您的作者样式仍然会覆盖浏览器样式。
这实际上已经是您可能一直在使用而没有意识到的东西。
* {
box-sizing: border-box;
}上面的选择器没有权重,因为 * 符号对权重没有贡献。 这意味着例如使用 p 作为选择器的 p 标签的浏览器样式在技术上比 * 选择器更具体,权重更高。 但是,这一切并不重要,因为作者样式位于比浏览器样式层晚的层中,因此您的代码将始终覆盖浏览器样式。
理解这一点至关重要,因为使用这个新的图层 API,您可以在作者图层中创建自己的图层,从而更轻松地处理特定性。
如何创建你自己的层
下面来看个例子:
很明显,这是我们正常理解的CSS, ID设置的颜色权重更高,所以按钮显示为红色。让我们使用@layer给它们加上两个层,看看是什么效果:
按钮变成蓝色。为什么会这样?
我们给两条CSS分别建立了base和utilities层,很明显,后面创建的层的样式覆盖了前面层的样式,尽管前面层的样式有更高的权重。这就是层的默认工作原理。当然层的顺序是可以指定的,
@layer utilities, base;
@layer utilities, base;
您需要做的就是编写@layer 关键字,后跟以逗号分隔的层列表。 这将按从左到右的顺序定义所有层,其中列出的第一层到最后一层的权重是依次增加的。 然后,您可以稍后使用普通的@layer 语法向每个层添加代码,而不必担心定义层的顺序,因为它们都在这一行中定义。 需要注意的是,这行代码必须在定义任何层之前出现,所以我通常将它作为我的 CSS 文件中的第一行。如上图,通过指定层的顺序,我们让base层应用在utilities层之后,所以按钮又显示为红色。
导入层
上面这两种方式都是导入bootstrap框架的CSS,并且把他们放在framework层中,这样你如果想要覆盖它已有的样式,只需要新建一个自己的层,放置在framework层后面就行。像下面这样。
匿名层
匿名层不常用,但它写在后面可以覆盖其他层的样式,像下面可以把按钮设为橙色。
不在层里的样式
不在层里的样式会有更高的权重,下面这个列表会让你看得更清楚覆盖是怎么发生的
层还可以重叠设置,不过很少用。具体的用法可以查阅相关文档。
浏览器支持
自从IE死了以后,所有主流浏览器都已支持这一特性。大家请放心使用。
*请认真填写需求信息,我们会在24小时内与您取得联系。