持久的-CSS-全-

持久的 CSS(全)

原文:zh.annas-archive.org/md5/75CD231CF1D89323893E2DE8217A208E

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

这是一本小书,概括了我目前对于如何最好地编写和维护不断变化、长期存在的网络项目的 CSS 代码库的思考。

  • 没有需要下载的框架

  • 没有必需的工具(尽管有关于工具应该提供什么的主观指南)

  • 没有为你写的东西

比喻地说,Enduring CSS 不是给你鱼,而是教你如何捕鱼。

这是一种编写 CSS 的方法论,一旦实施,将为您的项目提供可预测且易于维护的 CSS 代码库。

Enduring CSS 最早的版本是在 2014 年 8 月的博客文章中记录的,标题是Enduring CSS: writing style sheets for rapidly changing, long-lived projects (benfrain.com/enduring-css-writing-style-sheets-rapidly-changing-long-lived-projects/)。

然而,ECSS 方法自那时以来有所发展,因此这本书应该被视为该方法的权威资源。

希望你喜欢阅读。

Ben Frain

第一章:为快速变化、长期存在的项目编写样式

这实际上不是一本关于编写 CSS 的书,而是关于 CSS 的组织和架构;花括号外的部分。这是在较小的项目中可以忽略的考虑因素,但实际上成为在较大项目中编写 CSS 最困难的部分。

大规模 CSS大规模 CSS 编写这样的术语可能看起来相当模糊。我会尝试澄清。

当人们谈论大规模 CSS大规模 CSS 编写时,可能会涉及到部分描述的一些可能的指标:

  • 它可能是仅仅因为文件大小很大的 CSS。有很多 CSS 输出,因此对该代码库进行更改可能很困难,因为有很多代码需要考虑。

  • CSS 可能被称为大型,是因为正在构建的用户界面的复杂性。总体文件大小可能比第一种情况要小,但在这些样式中可能有许多用户界面的部分。考虑如何影响所有这些视觉可能会有问题。

  • 它可能是大型 CSS,仅仅是因为有许多开发人员可能会接触和更改 CSS 代码库。

或者,它可以是以上所有情况。

定义问题

持久的 CSS 诞生于我自己对在大规模 Web 应用程序上编写 CSS 的合理方法的需求。

定义什么使某物成为网络应用程序,而不仅仅是网页,可能会引起分歧,所以我们暂且不考虑这一点。让我们简单地考虑需要一种新的 CSS 编写方法的情况。

考虑一个界面,由于必要,密集地填充了视觉组件;滑块、按钮、输入字段等。

此外,考虑到这个界面(现在和以后)不断发展,并且需要迅速更改。此外,任何更改可能由任意数量的不同样式表作者进行。

如果没有明确定义的 CSS 编写方法,通过许多迭代,CSS 总是失控的。由于混合方法、不同作者之间的技术理解水平和代码文档的质量差异很大,样式表处于永久的熵状态。

因此,结果是 CSS 很难迭代,难以理解,没有人确切地知道冗余在哪里。更糟糕的是,样式表作者缺乏信心删除代码,因为担心无意中影响应用程序的其他部分。

如果你曾经继承或在一个大型 CSS 代码库的团队中工作,我相信我所描述的一些情况会听起来很熟悉。

因此,在我的旅程开始时,我定义了一些基本需求。更简单地说,这些是任何新的 CSS 编写方法必须解决的问题。以下是这些需求的列表:

  • 随着时间的推移,允许轻松维护大型 CSS 代码库

  • 允许从代码库中删除 CSS 代码的部分而不影响其余样式

  • 使得能够快速迭代任何新设计

  • 更改应用于一个视觉元素的属性和值不应无意中影响其他元素

  • 任何解决方案都应要求最少的工具和工作流程更改来实施。

  • 在可能的情况下,应使用 W3C 标准,如 ARIA,来传达用户界面中的状态变化

在下一章中,我们将更具体地看这些问题。然而,首先,一个重要的警告。

解决自己的问题

我相信Pin Cing Do,大致翻译为务实编码之道benfrain.com/be-better-front-end-developer-way-of-pragmatic-coding/)。这意味着解决你真正遇到的问题。因此,我会直截了当地说一些对一些人来说可能是显而易见的事情:

也许我遇到的问题并不是你所面临的问题。因此,你应该相应地调整所提供的建议和方法。或者,考虑到你的需求可能更适合不同的方法和方法论。我不会试图说服你,ECSS 在所有情况下都是最佳解决方案。例如:

  • ECSS 不会给你最小可能的 CSS 占用空间(考虑原子 CSSacss.io/))。

  • 它并不被广泛使用和记录(如果普遍性是一个主要问题,考虑BEMen.bem.info/))。

  • ECSS 不会抽象样式,允许使用一堆特定的实用类来为元素添加样式。你应该看看 OOCSS,并阅读其许多倡导者的著作。

好的,公共服务公告已经发布。让我们继续下一章。这是我们将研究大型项目的 CSS 扩展和架构的主要问题:特异性、级联、隔离和与结构元素相关的选择器。

第二章:CSS 在规模上的问题

在上一章中,我们谈到了导致 ECSS 方法论产生的场景。开发人员发现很难理解、难以处理并且充斥着缺乏注释和冗余代码的大型 CSS 代码库。然而,没有 CSS 代码库是这样开始的。

在大多数项目中,CSS 始于一些简单的规则。在开始时,您必须做一些相当愚蠢的事情才能使 CSS 的维护成为问题。

然而,随着项目的发展,CSS 也在增长。要求变得更加复杂。更多的作者参与编写样式。边缘情况和浏览器的解决方法需要被编写和考虑进去。事情很容易变得混乱。

让我们考虑一下一个不起眼的小部件所面临的不断增长的需求:

  • 当小部件在侧边栏中时,我们可以减小字体大小吗?

  • 当我们在主页时,小部件可以有不同的背景颜色吗?

  • 在更大的视口中,我们可以让小部件内部的东西垂直堆叠吗?

  • 当小部件在产品页面的侧边栏中时,字体颜色需要改变

不久之后,我们需要对一个关键选择器写一系列覆盖。让我们考虑一下我们可能需要的选择器:

.widget{
    /* Base Styles */
}

aside#sidebar .widget {
    /* Sidebar specific */
}

body.home-page aside#sidebar .widget {
    /* Home page sidebar specific */
}

@media (min-width: 600px) {
    .widget {
        /* Base Styles 600px and above */
    }

    aside#sidebar .widget {
        /* Sidebar specific 600px and above */
    }

    body.home-page aside#sidebar .widget {
        /* Home page sidebar specific 600px and above */
   }    
}

body.product-page .widget {
    /* Product page specific */
}

body.product-page aside#sidebar .widget {
    /* Product page sidebar specific */
}

如果这是我们想要扩展的 CSS,那么在这里存在一些基本的编写问题。现在让我们考虑一下这些规则中更明显的问题。

术语关键选择器用于描述任何 CSS 规则中最右边的选择器。这是您试图对其进行更改的选择器。

特异性

在尝试扩展 CSS 时,首要问题是特异性的问题。通常,特异性是一件有用的事情。它允许我们在 CSS 中引入一些逻辑。比其他样式更具体的样式将被应用在浏览器中。我们上面的例子证明了这一点:不同的规则将在不同的情况下应用(例如,在侧边栏中,我们希望覆盖默认样式)。

现在,CSS 选择器可以由 ID、类、属性和类型选择器组成,以及这些的任意组合。在响应式设计中,您还可以将媒体查询加入其中。

然而,并非所有选择器都是平等的。W3C 在这里描述了特异性的计算方式:www.w3.org/TR/css3-selectors/#specificity。以下是最相关的部分:

选择器的特异性计算如下:计算选择器中 ID 选择器的数量(= a)计算选择器中类选择器、属性选择器和伪类的数量(= b)计算选择器中类型选择器和伪元素的数量(= c)忽略通用选择器否定伪类内的选择器与其他选择器一样计数,但否定本身不算作伪类。将这三个数字 a-b-c 连接起来(在一个大基数的数字系统中)得到特异性。

那里缺少的一个重要事情是样式属性。关于此的信息在其他地方www.w3.org/TR/css-style-attr/)告诉我们:

样式属性中的声明适用于该属性所属的元素。在级联中,这些声明被认为具有作者来源和比任何选择器更高的特异性。

因此,在元素的样式属性中应用的样式将比 CSS 文件中的等效规则更具体。

无论如何,这里最重要的一点是 ID 选择器比基于类的选择器更具体。这使得覆盖包含基于 ID 的选择器的任何选择器变得更加困难。例如,在侧边栏中的小部件中,这不起作用:

.widget {
   /* Widget in the sidebar */
}

aside#sidebar .widget {
    /* Widget in an aside element with the ID of sidebar */
}

.class-on-sidebar .widget {
    /* Why doesn't this work */
}

在这种情况下,我们将在侧边栏元素(具有 ID 为侧边栏的 aside 元素)上应用 HTML 类(class-on-sidebar),然后在 CSS 中选择比基于 ID 的选择器更低的位置。然而,规则仍然不会被应用。

根据 W3C 规范,我们知道了特异性,我们可以计算这些规则的特异性。

让我们来算一下。从左到右,下面选择器后面的数字与:内联样式的数量、ID 选择器的数量、类选择器的数量以及最后类型选择器的数量有关。

选择器 内联 ID 类型
.widget 0 0 1 0
aside#sidebar .widget 0 1 1 1
.class-on-sidebar .widget 0 0 2 0

所以你可以看到这里,中间的选择器比最后一个选择器的特异性更高。遗憾。

在单个或较小的文件中,这并不是什么大问题。我们只需创建一个更具体的规则。然而,如果你的代码库的 CSS 分布在许多较小的部分 CSS 文件中,找到阻止你的覆盖工作的规则可能会成为一个不必要的负担。现在,问题不再是特异性选择器的问题。这更像是样式表中不平衡加权选择器的问题。可以把它想象成一个重量级拳击手对阵一个蝇量级拳击手。这不是一个公平的比赛。在使用的选择器之间创造一个公平的竞争环境比实际使用的选择器更重要。

这种不匹配的选择器混合是特异性问题的关键。一旦你有了一个包含数百条规则的 CSS 代码库,任何不需要的特异性都开始成为快速开发的主要障碍。

因此,总之,特异性是我们需要解决的问题,它存在于不断增长的 CSS 代码库中。

与选择器相关的标记结构

在编写大规模 CSS 时要避免的另一种做法是使用类型选择器;与特定标记相关的选择器。例如:

aside#sidebar ul > li a {
    /* Styles */
}

在这种情况下,我们需要在sidebar元素的内部有一个ul元素内部有一个li元素内部有一个a标记 - 哎呀!

如果我们想将这些样式应用到其他地方的div上会发生什么?或者任何其他标记结构?

我们刚刚不必要地将我们的规则与特定的标记结构联系在一起。这样做通常是很诱人的,因为给某些看似微不足道的标记(如aspan标记)添加类似的东西似乎很荒谬。然而,我希望一旦你读完这本书,你会被说服避免这种做法。

我们希望 CSS 尽可能地与结构松散耦合。这样,如果我们需要引入一个覆盖(特定实例的更具体选择器),我们可以尽可能地保持模糊以完成工作。再次,要习惯于只引入所需的特异性。

级联

通常,层叠样式表的级联部分是有用的。即使在使用的选择器中特异性非常相等,级联也允许在 CSS 中更深的位置应用等效规则。

然而,在一个庞大的代码库中,级联呈现了一种不良的诱惑;开发人员可以通过简单地在现有 CSS 底部编写更多新代码来修改现有 CSS 的能力。

这种诱惑既真实又容易被认同。出于许多原因,这可能是诱人的。例如,熟悉其他语言的作者可能缺乏对 CSS 代码库的信心或深入了解,无法自信地删除或修改现有代码。因此,他们选择最安全的选项,并使用更具体的规则覆盖现有规则。当时,这似乎是负责任的做法——只在需要时添加一两条规则。

然而,以这种方式依赖级联的问题在于,随着时间和迭代的进行,CSS 代码变得臃肿,充斥着许多多余的规则。这些 CSS 的使用者(用户)下载的是浏览器根本不需要的垃圾代码,而这些代码的维护者在每次需要理清代码库时都需要筛选更多的代码。

总结

到目前为止,我们已经从高层次上看到了一些问题,这些问题表明了一个在规模上难以应对的 CSS 代码库所面临的困境。问题包括过于具体的选择器、与特定标记结构相关联的选择器以及依赖级联和导致 CSS 膨胀的诱惑。

在下一章中,我们将探讨尝试驯服大型 CSS 代码库的公认智慧和方法,并考虑它们可能存在的不足之处。

第三章:实施得到的智慧

你通常在第一次实施解决方案之后才真正理解问题 -《大教堂与集市》(www.catb.org/esr/writings/cathedral-bazaar/cathedral-bazaar/ar01s02.html

在上一章中,我们考虑了处理大规模 CSS 代码库的一些更明显的困难。在本章中,我们将考虑一些现有的方法来解决这些问题。

在两年的时间里,我进行了 CSS 架构和维护的历程。在经历的开始,我做了任何明智的开发者都应该做的事情。我看了看聪明人是如何处理这个问题的。

CSS 架构方法似乎就像是减肥药对于超重者的等效物。很容易抓住表面上的解决方案,希望它们会正是你所需要的。然而,直到你至少尝试解决了问题之后,你可能并不知道自己确切需要什么。

这个建议在这里也适用。也许 ECSS 并不是你所面临问题的解决方案,所以如果你刚开始解决你的 CSS 问题,请务必考虑不同方法所提供的内容。

在我冒险的时候,处理大规模 CSS 的主要方法是:

现在,我会毫不羞愧地告诉你,我从每种方法中都偷了一些元素。然而,这些方法都没有真正解决我所面临的所有问题。

在我们正式介绍 ECSS 之前,我想简要介绍一下我研究的每种现有方法的优缺点。这样,至少当我们开始介绍 ECSS 时,你就能够理解它所解决的问题了。

关于 OOCSS

我所研究的现有方法中最广泛使用的,也是最受赞扬的是 OOCSS。这是我在试图解决不断增长的 CSS 代码库时首先采用的方法。

OOCSS 方法的一个主要论点是它消除了代码的重复,因此导致了更易维护的 CSS 代码库。实质上,您构建了一组 CSS Lego 片段,然后可以在 HTML/模板中使用它们来快速构建设计。希望一旦编写了 OOCSS 样式,它们就不应该增长(太多)。在可能的情况下重复使用,在需要的地方扩展。

在我们看 OOCSS 之前,我需要先说明一些警告。

  1. 这并不是对 OOCSS、Atomic CSS 或任何相关的单一职责原则SRP)方法的攻击。这只是我认为,根据你的目标,不同的方法可以提供更优选的结果。

  2. 我并不是在暗示我所倡导的方法是解决所有 CSS 扩展问题的灵丹妙药。它不是(没有)。

响应式网页设计,OOCSS 的软肋

对我来说,OOCSS 方法最大的问题是:

  • 响应式网页设计

  • 频繁的设计更改和持续的维护

  • 对新开发者来说是一种陌生的抽象概念

让我们看看我能否证明为什么我觉得这些问题值得考虑。

响应式问题

我认为Atomic CSSwww.smashingmagazine.com/2013/10/challenging-css-best-practices-atomic-approach/)(不要与Atomic Design (bradfrost.com/blog/post/atomic-web-design/)混淆)代表了 OOCSS 的极致。让我们考虑一个想象中的 Atomic CSS 示例:

<div class="blk m-10 fr">Here I am</div>

在这个 OOCSS/原子 CSS 示例中,元素的视觉需求已经被拆分/抽象为可重复使用的类。一个设置了块格式上下文(.blk),另一个设置了一些边距(.m-10),最后一个为元素提供了浮动机制(.fr)。毫无疑问,这是一种不带偏见且简洁的方法。

原则上,原子 CSS 与我设计的第一种架构方法非常相似。

它被称为PST!,这是 Position Structure Theme 的缩写。其想法是:不会有语义 HTML 类/CSS 选择器。相反,页面上的每个元素都可以通过其位置、结构和主题来描述。每个新选择器只需使用下一个可用的数字。例如,s1s2s3等等。它并不完全是每个责任一个类,就像原子 CSS 一样,但它是一种大大抽象化的样式需求的方法。

标记看起来像这样:

<div class="p1 s3 t4">Content</div>

就像原子 CSS 一样,它很简洁,你在编写时不需要思考该如何称呼某个东西,但实际上,对于我来说,它对我的需求来说是非常有问题的,就像本章描述的原因一样。

然而,当视口更改时,我们不希望有 10 像素的边距或项目浮动时会发生什么呢?

当然,我们可以在某些断点处创建一些类来执行某些操作。例如,Mplus-cc2可能会在Mplus断点(这里的 Mplus 是指中等大小的视口及以上)下更改颜色。但我发现这种做法很慢且费力。在某些断点处进行非常具体的更改,并将它们与必须添加到 HTML 中的类绑定起来似乎是毫无意义的复杂。此外,您最终会在样式表中得到一堆已过时的 SRP 类。当不再需要时,从编写样式表中删除任何无用的东西的机制是什么?

维护和迭代

让我们继续使用之前的例子。假设在将来的某个时候,我们将产品更改为更具有进步性的布局机制;我们从基于浮动的布局转移到基于 Flexbox 的布局。在这一点上,我们现在将有两倍的维护负担。我们不仅需要更改标记/模板中的类,还需要修改 CSS 规则本身(或编写全新的规则)。此外,使用float在 Flexbox 中是多余的,因此要么我们让.fr保持不变(因此它在我们的 CSS 中继续存在,毫无意义),要么我们让.fr负责其他事情,比如justify-content: flex-end。但是如果我们在特定视口下更改父元素的 flex-direction 会发生什么呢?烦死了!

希望您能看到 OOCSS 方法在设计频繁更改或需要在不同视口下呈现完全不同布局时的固有缺陷?

自 2013 年 Thierry 在 Smashing Magazine 上发表文章以来,原子 CSS 已经有了相当大的发展。根据您的目标,这可能正是您需要的东西,我鼓励您在acss.io上查看该项目。

对新开发人员来说是一种陌生的抽象

快速启用新开发人员可能对每个人来说都不是一个重要因素。然而,在开发人员经常加入和离开团队(甚至可能是公司)的情况下,这可能是一个重要的考虑因素。我正在寻找一种解决方案,它基本上允许开发人员继续按照他们所知道的方式编写 CSS。强迫新开发人员学习陌生的抽象可能是一个额外的不需要的负担。此外,将该抽象应用到模板层可能会有问题,毕竟许多解决方案甚至可能没有传统意义上的模板层。

一个纯粹的 OOCSS 示例

可以说,以原子 CSS 作为示例是不公平的,也许并不公平地代表了 OOCSS。然而,试图找到一个 OOCSS 的典型示例是困难的,因为 CSS 作者对它的理解似乎存在很大的差异,以及它的实际实现方式。

因此,我将提供一些进一步的,仅限于 OOCSS 的例子。我将使用 Nicole Sullivan 在她的幻灯片我们的最佳实践正在毁灭我们www.slideshare.net/stubbornella/our-best-practices-are-killing-us)中的原始例子。

我不愿意这样做,因为 Nicole 的原始例子现在已经很老了(2009 年,甚至在响应式网页设计出现之前),而且,不想替她说话,我敢说她现在可能会使用不同的例子和方法。

然而,希望我们能够达成共识,OOCSS 的基本目标是将结构与皮肤分离,将内容与容器分离github.com/stubbornella/oocss/wiki)?假设我们在这一点上达成一致,我坚信在某些情况下,OOCSS 对于创作速度和代码库的可维护性是有害的。

在响应式网页设计中,有时结构就是皮肤。或者说,结构在不同的上下文中有不同的作用,而使用 OOCSS 没有明智的处理方式。不过,你会做出判断。

考虑一下这个 OOCSS 的例子。首先是标记:

<div class="media attribution">
  <a href="#" class="img">
    <img src="img/mini.jpg" alt="Stubbornella" />
  </a>
  <div class="bd">@Stubbornella 14 minutes ago</div>
</div>

现在的 CSS(注意,我在这里删除了一些旧 IE 特定的属性/值):

.media { overflow: hidden; margin: 10px; }
.media .img { float: left; margin-right: 10px; }
.media .img img { display: block; }
.media .imgExt { float: right; margin-left: 10px; }

一个纯 OOCSS 的例子

OOCSS 的经典例子;媒体对象模式

然而,也许这个媒体对象在 300px 宽的视口下需要以不同的方式布局。你可以设置一个媒体查询,在这种情况下使它成为基于列的布局。但是假设你在同样的视口宽度下在不同的上下文中有相同的对象?在那个上下文中,它不应该是一个列布局。总结一下:

  1. 一个媒体对象需要在 300px 宽的基于列的布局中(我们称之为media1

  2. 第二个媒体对象需要在 300px 宽的基于行的布局中(因为它在另一个上下文/容器中,我们将其称为media2

让我们制作一个分离更多关注的类。它使媒体对象在特定视口上成为列布局:

@media (min-width: 18.75rem) {  
  .media-vp-small {
    /* Styles */
  }
}

这将被添加到任何需要在该视口上成为列的元素(media1),所以你需要转到模板/HTML 中进行更改,在需要的地方添加这个类。

此外,media2需要在较大的视口上有不同的背景颜色。让我们添加另一个类来分离这个关注点:

@media (min-width: 60rem) {
  .draw-focus {
    /* Styles */
  }
}

进入 HTML/模板,在需要的地方添加这个样式。

哦,media1需要在较大的视口上使.img更宽,而且不需要边距。我们可以为此制作另一个类:

@media (min-width: 60rem) {
  .expand-img {
    width: 40%;
    margin-right: 0!important;
  }
}

回到 HTML/模板中使这个变化发生。

希望现在你能看出这是怎么一回事了?我们需要添加很多单一职责原则(SRP)类来满足我们的媒体对象需要满足的许多不同情况。

这种方法并没有使我的大型响应式代码库更易于维护。事实上,恰恰相反。每当需要进行更改时,就必须去寻找特定情况的 SRP 类,并经常在标记/模板中添加/删除 HTML 类。这让我思考这个问题:

为什么这个东西就不能是这个东西呢?

目前,你可能会反驳说,这是一个愚蠢的例子,如果一个设计有这么多可能性,那就应该是正常的。在这一点上,我会反驳说这是不必要的。负责编写前端代码的人不应该因为这样会使他们的代码变得不可预测而限制设计师的创造力。他们应该能够简单轻松地编写出新的设计,而不必担心新的组件/模块/元素可能会影响其他部分。

提示

试图阻止项目的视觉变化仅仅是因为它们使我们的代码库难以维护和理解,这并不是一个站得住脚的立场。我们应该能够以速度和可预测性构建任何新的视觉效果,而不必担心无意中影响项目的其他部分。

当我使用 OOCSS 来满足我的需求时,我构建新视觉的速度减慢了,SRP 类的数量增加了;通常一个类在整个项目中只被使用一两次。即使对于 SRP 类有一个考虑周到的命名约定,记住特定需求的正确类名可能需要不断地进行思维操纵。

在一个快速变化的项目中使用 OOCSS 后,我发现在一段时间后,当需要进行更改时,解开这些抽象类变得非常令人沮丧。当它们很少被使用时,我不得不制作许多非常相似的抽象类。像w10w15w20w25等不同宽度百分比的实用类似乎是一个好主意,也是一个明显的抽象,但最终它们被证明是无用的,并且在迭代设计时会出现问题(回到了在不同上下文中需要做不同事情的问题)。

因此,当我使用 OOCSS 时的第一个重要教训与那位优秀的同行Kaelig Deloumeau-Prigent(www.kaelig.fr/)在 BBC 和《卫报》工作时所学到的教训是一样的:

两年前,我写了一本书,我在书中宣扬 DRY 代码,但在长期项目上的工作后,解耦对我来说变得更加重要。

在大型、快速变化的项目中,能够轻松地将视觉模块与项目解耦对于持续维护非常重要,而 OOCSS 并没有很好地满足这一需求。

SMACSS

SMACSS 代表可扩展模块化 CSS 架构,Jonathan Snook 关于这个主题的书smacss.com/)中有详细介绍。我不打算在这里详细介绍 SMACSS,因为我认为你应该自己去查看那本书。阅读 SMACSS 给了我很多思考的空间,我当时面临自己的挑战,我当然也从中学到了一些东西,比如如何思考状态变化。然而,我将详细说明为什么 SMACSS 对我不起作用。

再次强调,就像我对 OOCSS 的观点一样,这并不是对 SMACSS 的批评。它只是强调了对我来说不起作用的部分,以及为什么我觉得它未能解决我的问题。SMACSS 对网站的视觉方面有明确定义的术语和概念。因此,它规定了基础、布局、模块和可选的主题规则/文件来支持这些定义。例如,考虑这个建议的文件结构:

+-layout/ 
| +-grid.scss 
| +-alternate.scss 
+-module/ 
| +-callout.scss 
| +-bookmarks.scss 
| +-btn.scss 
| +-btn-compose.scss 
+-base.scss 
+-states.scss 
+-site-settings.scss 
+-mixins.scss”

摘自:Jonathan Snook. Scalable and Modular Architecture for CSS.

虽然这些定义在许多情况下都是合理的,但对我来说并不是。我想要一种更宽松的方法,一种不需要我考虑如何将我需要构建的东西适应这些视觉定义的方法;我构建和维护的应用程序经常无法遵循这些定义。

BEM

BEM 是由yandex.ru的开发人员开发的一种方法论。

我从 BEM 中学到的关键是,命名约定在 CSS 维护方面能为你带来多大的好处。

注意

如果你对 BEM 感兴趣,官方资源是en.bem.info。关于它的起源,我建议从这里开始阅读:en.bem.info/method/history

再次强调,就像 SMACSS 一样,我不打算完全解释 BEM 方法论的细节。然而,我会给你一个关键点的电梯演讲解释。BEM 方法论围绕着页面的关键区域可以被定义为的概念。然后,这些关键区域由元素组成。我们可以通过命名的方式来表示块和其元素之间的关系。考虑之前提到的 OOCSS 媒体对象示例。在 BEM 方法中,我们可能会使用这样的类:

<div class="media">
  <a href="#" class="media__img">
    <img class="media__headshot" src="img/mini.jpg"
    alt="Stubbornella" />
  </a>
  <div class="media__attribution">@Stubbornella 14 minutes
  ago</div>
</div>

这种命名方案的有用之处在于它清晰地传达了元素与它们所属的块之间的关系。此外,远离 HTML,如果我们在 CSS 中遇到这样的选择器:

.media__headshot {

}

我们立即知道这是一个名为headshot的元素,它位于名为media的块内。将组件命名为其他组件的一部分有助于隔离样式,并防止应用的样式泄漏 - 这是我对 OOCSS 的主要不满之一。这绝对是解决我尝试解决的问题的正确方向。

BEM 还有修饰符的概念。修饰符是添加到块上以修改其外观的东西。假设我们想在不同的情况下以不同的方式为我们的媒体对象设置主题。BEM 可以这样实现:

<div class="media media_dark">
  <a href="#" class="media__img">
    <img class="media__headshot" src="img/mini.jpg"
    alt="Stubbornella" />
  </a>
  <div class="media__attribution">@Stubbornella 14 minutes
  ago</div>
</div>

BEM 文档规定使用单个下划线字符来标识块的修饰符。这个修饰符类必须始终与块名称一起使用。例如,你必须这样做:


<div class="media media_dark">

而不是这样:

BEM

<div class="media_dark">

我看到了以这种方式使用修饰符的价值,但对我来说,这证明是有问题的。我经常需要对我正在设计的东西以更传统的方式进行不同的行为。也许视觉上需要根据它们被使用的上下文而显示不同,或者如果在 DOM 中添加了另一个类,或者由于某些媒体查询条件,或者任何这些情况的组合。我需要一种编写样式的方式,足够实用,可以处理发生的非理想情况。无论发生了什么,都能保持一些理智在编写样式表中。

总结

在我查看的所有现有 CSS 方法中,我从 BEM 中学到了最多。BEM 有很多值得赞赏的地方:

  • 所有元素具有相同的特异性;一个类被添加到所有元素。

  • 没有使用类型选择器,因此 HTML 结构与样式没有紧密耦合。

  • 很容易推断出一个元素的父级是什么,无论是在浏览器开发者工具中查看 DOM 树,还是在代码编辑器中查看 CSS。

然而,修饰符的使用并不完全符合我的需求。尽管也许这并不是理想的,但我的现实是,通常我需要根据 DOM 中它上面或旁边的某种可能性来覆盖块的样式(在 BEM 术语中)。

例如,在应用程序中已经确定了现有逻辑的情况下,可能会出现这样的情况,即在 DOM 中的问题项上方添加一个类contains2columns,我需要根据这个类进行样式更改,而不是直接在问题块上进行更改。

使用 BEM,我找不到一个清晰的方法来理解应该如何处理这种可能性。或者如何在编写样式表中包含这些类型的覆盖。我想要定义项目并封装可能发生在特定项目上的所有可能性。

当浏览类时,我也发现语法令人困惑。修饰符的书写方式和元素的书写方式之间的区别微乎其微。这可能很容易解决,但这仍然是我对它感到困扰的事情。

最后,我意识到我需要一些额外的东西。我希望能够沟通和促进模块的不同上下文。当一个东西由相同的逻辑创建,但可以在不同的上下文中以不同的方式使用和样式化时,我希望有一种沟通的方式。

从 SMACSS 中我发现最有用的是处理状态。我喜欢类似is-pressed或属性.btn[data-state=pressed]这样的声明方式,清晰地传达了元素的状态。

OOCSS 结果证明是我所需要的东西的对立面。虽然我欣赏 OOCSS 能提供的东西,但它并不是我所面临问题的解决方案。我不想创建一个作者可以用来在 DOM/模板中构建视觉效果的乐高样式盒。OOCSS 促进的抽象本质上是泄漏的,这使得维护成为问题(更改一个规则中的值可能会无意中影响许多元素),而且在处理不同的视口时也很难找到方法,出于已经解释的所有原因以及添加的额外抽象在新开发人员入职时会带来更多的复杂性。

最终,通过尝试和失败,以不同程度,使用每一种现有的解决方案,我终于完全理解了我的问题。现在是时候定制一个专门的解决方案了。用巴勃罗·毕加索的话来说:

好的程序员抄袭,伟大的程序员窃取 巴勃罗·毕加索(有点像-抱歉巴勃罗)

跟我走。

第四章:介绍 ECSS 方法论

在上一章中,我们考虑了现有的 CSS 方法论,以及对于我们谦逊的作者来说,它们存在的不足之处。

我不打算说服您持久 CSS 方法是阿尔法和欧米茄。然而,它确实具有与现有方法不同的优势和目标。因此,即使整体采用它并不吸引人,我希望您可以借鉴一些东西来解决自己的问题。

ECSS 的亮点:

  • 通过隔离每个视觉模式来获得可维护性

  • 文件大小随着时间的推移保持最小,因为您可以毫不留情地剪切部分/功能/组件

  • 规则是自我隔离的

  • 类名/选择器可以传达上下文、起源逻辑和变化

  • 所有规则、它们的效果和范围都是完全可预测的

当我第一次写关于持久 CSS 时,我预料到会有一些反对声音。那时(2014 年 8 月),没有人真正提倡我所建议的东西。对于扩展 CSS 的普遍看法是抽象视觉模式,尽可能规范设计,并使代码更干燥。持久 CSS在某种程度上与这些信念相反。

在本章中,我们不会涉及 ECSS 的具体技术细节,比如命名约定、工具、编写和组织。我们将在以后的章节中详细讨论这些主题。相反,我们将着眼于这种方法的广泛目标和好处,以及与其他方法相比的优势。

如果您不知道这个缩写,DRY 代表不要重复自己,这是编码时的一个流行目标,以便逻辑只在代码库中写一次,提供一个真实的单一来源。

在我们深入讨论之前,我认为澄清将要使用的术语可能会有所帮助。在不同的方法中,用于定义页面视觉部分的术语有不同的名称。我所提出的建议或我所使用的术语并没有什么新意,重要的是在我们深入讨论之前,我们都在同一页面上。

定义术语

我使用术语模块来指代功能区域和/或创建它的代码。举例来说,网站的页眉可以被视为一个模块。页眉模块将由其他更小的功能部分组成。例如,下拉菜单或搜索框。这些嵌套的功能部分将被定义为组件。最后,我们最小的将是组件或模块的组成部分的子节点。

因此,重申一下:

  • 模块是最宽的、在视觉上可识别的、独立的功能部分

  • 组件是包含在模块中的嵌套功能部分

  • 子节点是组件的组成部分(通常是 DOM 中的节点)

为了简洁起见,接下来,当我提到模块时,它可能是一个模块或组件。从 ECSS 编写的角度来看,这种区别并不重要。

ECSS 解决的问题

我的 ECSS 的主要目标是将样式隔离开来,而不是将其抽象化。

通常情况下,创建 CSS 类的抽象常见功能是有意义的。好处在于它们可以在许多不同的元素上被重复使用和重新应用。原则上这是合理的。问题是,在更大更复杂的用户界面上,即使对这些抽象进行微小的调整和修改也变得不可能,因为这样做会无意中影响到您不打算影响的东西。

因此,ECSS 的一个指导原则是将样式隔离到预期的目标上。

根据您的目标,即使重复,孤立性也可以为您带来更大的优势;允许可预测的样式和简单的样式解耦。

隔离样式的另一个优势是,设计师可以被鼓励带来他们需要的任何东西,而不一定感到受到现有视觉模式的限制。需要编写的每个新模块都可以是一个绿地。我发现,当从头开始编写设计时,我可以比尝试从任意数量的模糊抽象中构建它们更快地编写出设计。

处理特异性

我还想消除与特异性相关的问题。因此,我采用了广泛使用的方法,坚持所有选择器都使用单个(或尽可能接近理想的)基于类的选择器。

如果你遇到 CSS 问题,我为你感到难过,我有 99 个问题,但特异性不是其中之一
--twitter.com/benfrain/status/537339394706141184

此外,结构化的 HTML 元素(除了伪元素)在样式表中永远不会被引用为类型选择器。此外,ECSS 完全避免使用 ID 选择器。不是因为 ID 选择器本身不好,而是因为我们需要选择器强度的公平竞争环境。

通过简单的覆盖来处理组件的更改。然而,从作者的角度来看,它们的处理方式使它们易于管理和理解。

假设您有一个元素,如果它在特定容器内,需要具有不同的宽度-非常简单,我们不需要以严厉的方式进行覆盖。我们不需要将修改器应用于该特定元素。我们可以处理典型和非常宽松的覆盖场景,但可以自信地管理它们。您可以在作者样式表中这样编写:

.my-Module_Component {
    width: 100%;
    /* If in the sidebar */
    .sw-Sidebar & {
        width: 50%;
    }
}

它将产生以下 CSS:

.my-Module_Component {
  width: 100%;
}

.sw-Sidebar .my-Module_Component {
  width: 50%;
}

这可能看起来是一个微小的好处。毕竟,我们可能会以稍微不同的方式编写代码,通过嵌套覆盖,但最终结果是典型的 CSS;一个元素根据不同和更具体的选择器获得不同的样式。

提示

在嵌套上下文中使用和符号来表示父选择器的约定是从Sasssass-lang.com/)语言借鉴的。

然而,通过采用这种方法,从作者的角度来看,我们为每个关键选择器创建了一个真理的单一来源。将对该关键选择器进行任何更改的所有内容都嵌套在那组花括号的开头内。此外,该关键选择器永远不会在整个代码库中的任何其他地方被定义为根规则。

注意

DRY 的不同解释

我并不认为其他 CSS 开发者追求和赞美的 DRY 代码目标,是我想要的那种 DRY 代码。更具体地说,我并不太在意规则中重复的值和对,这是大多数人关注的 DRY 化的内容。我关心的是关键选择器在代码库中不重复。关键选择器是我的真理的单一来源,这是我想要 DRY 化的领域。因此,在 ECSS 中,我们强制执行了一种作者约定,防止在整个项目中多次定义关键选择器。我们将在第八章中更详细地讨论这个问题,理智样式表的十诫

这是!重要

偶尔,一个覆盖的存在不够,我们可以使用!important

您可能已经意识到,在 CSS 中使用!important通常是不受欢迎的。以下是 MDN 对!important的看法:

当在样式声明中使用!important 规则时,此声明将覆盖 CSS 中的任何其他声明,无论它在声明列表中的位置如何。尽管!important 与特异性无关,但使用!important 是一种不好的做法,因为它使调试变得困难,因为它破坏了样式表中的自然级联
--developer.mozilla.org/en-US/docs/Web/CSS/Specificity

然而,当我们无法控制的事件影响我们的样式(例如,在页面上加载了第三方 CSS 文件),我们需要一些权威,我会接受!important。以下是一个状态变化的例子,它从!important获得了额外的支持:

[aria-expanded="true"] & {
    transform: translate3d(0, -$super-height,
    0)!important;
}

我会诚实地说,当需要时,我真的不会因为使用!important而失眠。由于所有的覆盖都局限于它们在编写样式表中的父选择器,偶尔使用!important在 ECSS 中并不会出现问题。

接受重复

在我们进一步深入之前,我认为有必要处理一个可能存在的悬而未决的问题。我需要试图说服你,消除文件之间属性和值的重复可能并不像从维护的角度来看那么有价值,而是一个坚实且包含的模块集,可以根据需要轻松从代码库中删除。

ECSS 方法接受 CSS 中属性和值的重复。

使用 ECSS,每个视觉模块或组件都是用微命名空间编写的,以便与其他模块和组件隔离。以下是一个经过编写的 ECSS 规则的典型示例(编写语法与 Sass 非常相似,但通常由 PostCSS 进行辅助):

.ip-SubHeader_Wrapper {
    @mixin Headline;
    align-items: center;
    /* We want the subheader hidden by default at the
    smallest sizes */
    display: none;
    font-size: $text12;
    background-color: $color-grey-54;
    border-bottom: 1px solid color($color-grey-54 a(.5));
    min-height: $size-fine-quadruple;
    @media (min-width: $SM) {
        display: inline-block;
    }
    @media (min-width: $M) {
        display: flex;
        background-color: $color-grey-a7;
        color: $color-grey-54;
        font-size: $text13;
        min-height: 1.5rem;
        border-bottom: 1px solid $color-grey-54;
        border-top: 1px solid $color-grey-33;
    }
    /* However, even on mobile, if the SubHeader Wrapper
    is in section 1, we want to see it */
    .ip-Classification_Header-1 & {
        display: flex;
    }
}

倾向于 OOCSS 和 Atomic CSS 方法的人可能会看到这一点而感到恐惧。像colorfont-size这样的东西在大多数组件中都有声明。@mixin Headline mixin 也会生成大量的 CSS 来指定特定的字体堆栈。所以,是的,有属性和值的重复。

然而,积极的一面:

  • 它冗长,但不依赖于样式的继承。

  • 它通常是与上下文无关的(除了它所放置的大小上下文),影响这个组件的任何媒体查询都在这一组花括号内定义。

  • 这样的关键选择器只需编写一次。当这个关键选择器需要更改时,你只需要在这一个地方查找。

  • 编写的规则中所有的覆盖都嵌套在其中,形成了一种微型级联。通常情况下,覆盖可以出现在 CSS 的任何地方,遵循这种方法将它们限制在一个非常特定的区域。这样就更容易理解特定性与规则的关系。

零组件抽象

使用 ECSS,如果需要创建一个与现有组件类似但略有不同的组件,我们不会从现有组件中抽象或扩展。而是会编写一个新的组件。

是的,我是认真的。

即使有 95%是相同的。

这样做的好处是,每个组件都是独立的和隔离的。一个可以存在而另一个不需要。一个可以根据需要进行变化,而与其他组件独立。尽管它们在外观上看起来很相似,但它们可以根据需要进行变异,而不用担心感染或污染其他看起来相似的组件。延伸生物学的隐喻,我们获得了自我隔离的组件,通过它们独特的命名空间。

注意

进一步的类比:BMW 3 系与 BMW 5 系有很多共同之处。但它们并不相同。它们可能共享一些/许多零件(相当于 CSS 属性和值的组合),但这并不意味着它们是相同的。它们的差异定义了它们。它们不能由完全相同的零件制成,因为它们之间有根本的不同。我认为这也适用于使用 ECSS 定义的模块和组件。CSS 语言是抽象的。CSS 的属性/值对已经意味着我们可以从单个部分构建我们想要的东西。

重复的成本?

要充分利用 ECSS 的好处,你需要对它所创建的属性和值的重复感到舒适。此时,你可能会认为我是疯了。有了这么多重复,这种 ECSS 方法怎么可能是一个可行的选择?我会用一个词来解决这个问题:gzip。

好吧,我撒了谎。我想进一步说明。

gzip 在压缩重复字符串方面非常高效

我很好奇像 ECSS 这样的方法中重复属性/值对的冗长实际上产生了什么真实世界的差异?一个实验:

我正在进行的一个基于 ECSS 的项目的结果 CSS 文件,在经过 gzip 压缩后(因为它将通过网络传输),大小为 42.9 KB。这是一个相当大的 CSS 文件。

这个样式表中可以从中抽象出来的最常见和冗长的模式是一对基于 Flex 的规则,它们在整个样式表中被大量使用,用于垂直居中其容器中的内容。由于Autoprefixergithub.com/postcss/autoprefixer)添加了大量代码以支持旧设备,它们甚至更冗长。例如,定义 flex 布局的结果 CSS 是:

.flex {
    display: -webkit-box;
    display: -webkit-flex;
    display: -ms-flexbox;
    display: flex;
}

在测试样式表中,这四行 CSS 被重复了193次。

这只是其中一半。许多这些项目也需要对齐。这在 CSS 中也是必需的:

.flex-center {
    -webkit-box-align: center;
    -webkit-align-items: center;
    -ms-flex-align: center;
    align-items: center;
}

这个代码块被重复了117次。看起来没有更好的理由来抽象成一个 OOCSS 类,对吧?那一定会导致严重的膨胀?

不要那么着急,蝙蝠侠!

如果这些代码块被移除并重新压缩文件,CSS 文件大小将减少到 41.9 KB。

将最常见和冗长的视觉模式提取到一个 OOCSS 类中,仅在传输过程中节省了 1 KB 的 CSS。尽管 CSS 仅节省了 1 KB,但要考虑到,如果将这些样式抽象为单一职责类(例如.flex.flex-center),还需要在 HTML 中添加相关的 OOCSS 类来恢复视觉效果。

这是否使得单一职责类值得?

考虑到没有其他属性组合有类似的冗长和重复,从文件大小的角度来看,当然不是在我的书中。这将会大大降低开发的灵活性(记住,抽象会使作者和迭代变慢,因为需要同时更改模板和 CSS)和响应灵活性(如果我想在不同的视口中做一些不同的事情)以换取 CSS 文件大小的微小节省。这就是 CSS 版的拿来主义

让我非常清楚。尽管 gzip 的效果很好,如果你的优先级是拥有尽可能小的 CSS 文件大小,ECSS 并不是你最好的选择。

相反,去看看Atomic CSSacss.io/)。它的创作者是聪明人,确实,Thierry Koblentzwww.cssmojo.com/)是我所知道的最聪明的 CSS 人之一。我相信 ACSS 会满足你的需求。

另一方面,ECSS 的优先级是开发人员的人体工程学(可理解的类命名约定)、易维护性(按组件组织的样式,易于删除)和样式封装(命名空间防止泄漏的抽象)。

不同的问题,不同的解决方案。

总结

希望我已经给了你足够的理由来考虑,如果你试图创建可维护的样式表,那么过度关注重复的属性值和对可能不是最好的时间利用。在下一章中,除了看看 ECSS 命名约定的好处之外,我还会争辩说,对项目维护采取合理的组织方法将比类抽象和重用在长期内生成更精简的样式表。

第五章:文件组织和命名约定

在上一章中,我们对 ECSS 的设计考虑进行了高层次的概述。在本章中,我们将深入探讨如何实际开始使用 ECSS。

使 ECSS 适合您的两个基石是遵守文件组织和类命名约定原则。让我们接下来看看这些方面。

项目组织

如果我们想要方便地从我们的网站/应用程序中删除代码,我们需要考虑我们组成项目的文件的方式。通常,在构建网站,特别是 Web 应用程序时,考虑模块或组件的方式是有用的;可定义的用户界面部分。这些模块可能主要由视觉区域定义,或者可能由生成它们的文件定义。无论哪种方式,花时间考虑这些模块的组织都是值得的。

通常,按技术类型将项目中的文件拆分是一种常见做法。

考虑这个基本的文件夹结构:

my-project/
- html/
- js/
- css/

在这些文件夹中,您可能会命名相关文件。例如:

my-project/
- html/
    - v2ShoppingCart.html
- js/
    - v2ShoppingCart.js
- css/
    - v2ShoppingCart.css

然而,问题在于,超过一定程度后,即使给文件命名,也很难理清项目中每个样式表、逻辑文件和模板之间的关系。在css文件夹中可能有 80 多个 CSS 部分,在html文件夹中可能有 50 多个模板存根。

注意

我意识到现实情况是,网站或应用程序的view部分通常是由多种不同的技术生成的,如 Ruby、PHP、.NET 甚至 JavaScript - 而不是纯粹的 HTML。

随后,越来越需要依赖find在文本编辑器/IDE 中查找某个类别正在使用的任何模板。反之亦然;需要find来定位包含某个模块模板所需样式的部分。

这种结构并不会使事情变得不可行,只是效率低下,通常需要一点心理定位来记住什么与什么相关。

虽然对于 ECSS 来说并非必需,但通常更倾向于按视觉或逻辑组件组织文件,而不是按技术类型组织。因此,不是这样:

html/
- shopping-cart-template.html
- callouts-template.html
- products-template.html

js/
- shopping-cart-template.js
- callouts-template.js
- products-template.js

css/
- shopping-cart-template.css
- callouts-template.css
- products-template.css

我们的目标是这样的:

shopping-cart-template/
    - shopping-cart.html
    - shopping-cart.css
    - shopping-cart.js

callouts-template/
    - callouts.html
    - callouts.js
    - callouts.css

products-template/
    - products.html
    - products.js
    - products.css

乍一看,这似乎是一个看似不重要的区别,但它带来了重要的好处。

每个组件的代码都是物理上自包含的。然后,在我们持久的项目中,当需要更改或废弃功能时,可以轻松更新/删除与该模块相关的所有代码(样式、视图逻辑(HTML)和 JS)。

注意

应有的赞誉

Nicolas Gallagher (nicolasgallagher.com/) 在考虑规模化的 CSS 实现时总是领先一步,我从他的工作中借鉴并调整了大部分元素(特别是按组件组织代码)。我已经为组件命名空间化了一段时间(因此我宣称伪多重发现),但按组件组织代码的方法完全是从听他谈论 (www.youtube.com/watch?v=m0oMHG6ZXvo) 中得来的。

除了有意的全局CSS 之外,与组件或模块的呈现相关的所有代码都应包含在与该组件的 HTML/JS 并列的部分中。

注意

尽管你可能不喜欢,但总是需要一定程度的全局 CSS;至少需要一组简单的重置或规范化样式。

当一个模块被废弃时,可以轻松地一次性从代码库中删除与之相关的所有文件;只需删除包含该模块的文件夹。

为了明确,考虑我们想象中的ShoppingCart组件的文件夹结构:

ShoppingCart/
    - ShoppingCart.js
    - ShoppingCart.css

现在假设我们创建一个新的购物车:

v2ShoppingCart/
    - v2ShoppingCart.js
    - v2ShoppingCart.css

一旦我们的v2购物车完成,就可以轻松地从我们的代码库中删除先前版本的代码;我们只需删除包含旧代码的ShoppingCart文件夹。

当无法使用相同的文件夹组织时

可能无法或不希望将样式表、资源和应用程序逻辑包含在同一个文件夹中。

在这种情况下,下一个最佳选择是模仿逻辑的结构。举例来说。假设一个组件的逻辑存储在这样的文件夹结构中:

src/app/v2ShoppingCart/v2ShoppingCart.js

我们应尽可能模仿这种结构。在任何规模的应用程序中,这将使查找相关文件变得更容易。因此,我们可以这样做-尽可能地匹配逻辑文件的文件夹层次结构:

src/app/css/v2ShoppingCart/v2ShoppingCart.css

在使用 ECSS 时,相同的父文件夹应该被认为是黄金标准,但在没有这种情况下,模仿逻辑文件的结构应该能够提供一些好处。

有了如何组织项目中的文件的具体想法,让我们转向我们可以向我们的选择器/类传达附加含义和开发人员便利的主要方式。

使用 ECSS 命名类和选择器

回到第三章,实施接收到的智慧,我意识到 BEM 方法在命名 CSS 选择器方面给我们带来的好处。首先命名一个块,然后根据该块命名任何子元素,为子元素创建了一个命名空间。

模块的 CSS 命名空间创建了一种隔离形式。通过防止与其他元素发生名称冲突,CSS 块可以更轻松地从一个环境移动到另一个环境(例如从原型到生产)。这也大大减少了一个选择器的样式更改无意中影响其他选择器的可能性。

注意

有许多其他方法来解决名称冲突的问题。例如,如果您正在使用流行的React (facebook.github.io/react/) 框架构建应用程序,请考虑使用Radium (github.com/FormidableLabs/radium),它将为每个节点内联样式,因此您可以有效地不提供任何 CSS。当然,这也存在一些权衡,比如缺乏缓存和无法添加重置样式,但它确实解决了手头的问题。此外,当不使用 React 构建时,请考虑CSS 模块 (github.com/css-modules/css-modules)。虽然需要比 ECSS 更多的工具支持,但它意味着您可以完全不考虑命名事物,因为它会为您创建 CSS 作用域。在这里了解更多 (medium.com/seek-ui-engineering/the-end-of-global-css-90d2a4a06284)。

ECSS 采用选择器命名空间的概念,并将其提升到 11 (en.wikipedia.org/wiki/Up_to_eleven)。选择器实际上以两种方式进行命名空间:

  • 微型命名空间:通常用于指定上下文,但也可以指示父模块

  • 模块自己的命名空间:通常是创建所讨论元素的逻辑文件的名称

让我们更详细地看看这些。微型命名空间是每个模块的简单 2-3 个字母的命名空间。构建购物车?尝试使用. sc- 作为您的微型命名空间。构建同一购物车的下一个版本?那就是. sc2-。这足以隔离您的组件样式,并允许样式更具自我说明性。让我们考虑一个更复杂的例子。

提示

在命名事物时,不同的项目会有不同的合理性。虽然 ECSS 可以适应不同的方法,但我建议在每个项目中采用一致的方法。

例如,假设微命名空间用于传达创建它的逻辑的父级或起源。回到我们的购物车示例。我们可能有一个名为ShoppingCart.php的文件,其中包含了与我们想象中的购物车相关的所有逻辑。因此,我们可以使用sc-作为该文件名的缩写,以便我们知道以该命名空间开头的任何元素都与购物车相关,并由相关文件渲染。

在这种情况下,我们将有类似以下的选择器:

  • sc-Title:购物车的标题

  • sc-RemoveBtn:从购物车中移除物品的按钮

这里的选择器非常紧凑-如果一个选择器甚至可以用这种方式描述的话,那么它在美学上是令人愉悦的。然而,假设我们有一个可以存在于多个上下文中的购物车。迷你购物车视图和完整页面视图。在这种情况下,我们可能决定使用微命名空间来传达上下文。例如:

  • mc-ShoppingCart_Title:购物车的标题,在迷你购物车视图/上下文中由文件ShoppingCart生成。

  • mc-ShoppingCart_RemoveBtn:购物车的删除按钮,在迷你购物车视图/上下文中由文件ShoppingCart生成。

这两者都不是唯一的方法。ECSS 哲学的一部分是,虽然一些核心原则是必不可少的,但它可以适应不同的需求。一般来说,对于较小规模的用例,前一种方法是可以的。然而,尽管第二种方法中选择器的相对冗长,但它是最具弹性和自我记录的。通过第二种方法,您可以了解上下文,生成选择器的文件(因此它所属的模块)和它所关联的元素。

注意

在第七章中,有关将 ECSS 约定应用于 Web 应用程序和视觉模块的更具体信息,将 ECSS 应用于您的网站或应用程序

重申好处

由于命名空间模块和组件几乎肯定不会相互泄漏,因此非常容易构建和迭代新设计。这提供了一个迄今为止难以想象的免责保护。只需为正在构建的事物创建一个新的部分文件,分配一个合适的微命名空间和模块名称,并编写您的样式,确信您不会对任何不想要的东西产生不利影响。如果您正在构建的新事物不起作用,您可以删除部分文件,也可以确信您不会删除其他东西的样式。CSS 编写和维护的信心-终于!

源顺序变得不重要

由于我们的规则现在是隔离的,因此样式表中规则的顺序变得不重要。在处理大型项目时,这一好处变得至关重要。在这些情况下,通常希望可以以任何顺序组装部分文件。由于规则彼此隔离,这很简单。有了我们的自我隔离规则,部分样式表的文件全局匹配变得简单且无风险。通过一些基本的工具,您可以像这样一次性编译模块中的所有 CSS 部分:

@import "**/*.css";

不再为项目中的每个部分编写@import语句,并担心它们的顺序。

提示

我们将在第九章中更多地讨论文件全局匹配,ECSS 方法的工具

ECSS 命名约定的解剖

由于项目名称对于实现我们的目标非常有用和必要,下一节将更详细地记录 ECSS 的命名约定。把它想象成你的 CSS 选择器的Haynes 手册(haynes.co.uk/catalog/manuals-online)。

以下是 ECSS 选择器的分解:

.namespace-ModuleOrComponent_ChildNode-variant {}

为了说明各个部分,这是该选择器的解剖,用方括号划分各个部分:

.[namespace][-ModuleOrComponent][_ChildNode][-variant]

提示

在一个项目中有多个开发人员的情况下,我建议对不遵循 ECSS 命名模式的代码提交进行自动拒绝。关于必要的工具支持的一些信息在第九章中有所涉及,工具支持 ECSS 方法

选择器部分的解释

让我们回顾一下 ECSS 选择器的各个部分和允许的字符类型:

  • Namespace:这是每个选择器的必需部分。微命名空间应该全部小写/短横线命名法。通常是一个缩写,用于表示上下文或起始逻辑。

  • 模块或组件:这是大驼峰命名法。它应该始终前面有一个连字符(-)。

  • ChildNode:这是选择器的一个可选部分。它应该是大驼峰命名法,并且前面有一个下划线(_)。

  • Variant:这是选择器的另一个可选部分。它应该全部小写/短横线命名法。

使用这种语法,类名的每个部分都可以从另一个逻辑上区分出来。更多关于这些部分是什么以及它们应该如何使用的信息如下:

命名空间

如上所述,HTML 类/CSS 选择器的第一部分是微命名空间(全部小写/短横线命名法)。命名空间用于防止冲突,并为规则的更轻松维护提供一些软隔离。

模块或组件

这是创建选择器的视觉模块或逻辑片段。它应该用大驼峰命名法。我看到 ECSS 被成功应用时,模块或组件直接引用了创建它的文件的名称。例如,一个名为CallOuts.js的文件可以有一个选择器,如sw-CallOuts(这里的sw-微命名空间用于表示它将被站点范围使用)。这样可以消除未来开发人员对该元素起点的歧义。

子节点

如果某个大驼峰命名法前面有一个下划线(_),那么它是模块或组件的子节点。

例如:

.sc-Item_Header {}

在这里,_Header表示这个节点是属于sc命名空间的Item模块或组件的Header子节点(如果它是一个组件,那么该命名空间可以表示父模块)。

变体

如果某个东西全部小写/短横线命名法,并且不是类名的第一部分,那么它是一个变体标志。变体标志保留用于需要引用许多选择器变体的情况。假设我们有一个模块,需要根据分配给它的类别编号显示不同的背景图像。我们可以像这样使用变体指示器:

.sc-Item_Header-bg1 {} /* Image for category 1 */.
sc-Item_Header-bg2 {} /* Image for category 2 */.
sc-Item_Header-bg3 {} /* Image for category 3 */

这里选择器的-bg3部分表示这个sc-Item_Header是类别 3 版本(因此可以分配适当的样式)。

在 ECSS 选择器上加倍

我们之前的例子表明了一个完美的情况,适合在元素上使用两个类。一个用于分配默认样式,另一个用于设置变体的具体内容。

考虑这个标记:

<div class="sc-Item_Header sc-Item_Header-bg1">
    <!-- Stuff -->
</div>

在这里,我们将使用sc-Item_Header为元素设置通用样式,然后使用sc-Item_Header-bg1为变体设置特定样式。这种方法并不具有革命性,我只是在这里记录它,以明确指出 ECSS 方法中没有任何阻止这种做法的地方。

总结

本章我们涵盖了很多细节。我们主要关注了两个方面:如何组织项目的语言文件,以便更容易维护;以及如何命名 HTML 类/CSS 选择器,使得 DOM 中元素的类可以告诉我们关于其来源、目的和预期上下文的一切。我们还详细研究了 ECSS 选择器的接受语法:在哪里以及如何应用大小写区别来划分选择器的不同部分。到目前为止,我们只关注静态元素。在下一章中,我们将看看 ECSS 如何处理网站或应用程序的变化状态。

第六章:在 ECSS 中处理状态变化

在上一章中,我们考虑了项目组织以及如何理解和应用 ECSS 类命名约定。在本章中,我们将把重点转移到 ECSS 如何处理活动界面以及如何以一种合理和可访问的方式促进样式变化。

大多数 Web 应用程序需要处理状态。

首先让我们澄清一下我们所说的“状态”。考虑一些例子:

  • 用户点击按钮

  • 界面中的值已更新

  • 界面的某个区域被禁用

  • 界面中的小部件正在忙碌

  • 输入的值超过了允许的值

  • 应用程序的某个部分开始包含实时数据

所有这些情况都可以定义为状态变化。我们通常需要向用户传达的状态变化。因此,这些是需要传达给 DOM 的变化,随后我们的样式表需要一些合理的方式来满足这些需求。

我们如何以一致和深思熟虑的方式定义这些状态变化?

ECSS 以前如何处理状态变化

在第三章中,我提到了我有多喜欢 SMACSS 方法来传达状态。例如:


.has-MiniCartActive {}

表示在此节点上或下方的某处,迷你购物车处于活动状态。

另一个例子:


.is-ShowingValue {}

这将传达组件或其中一个组件正在显示一些值(先前隐藏的)。

在历史上,这就是我在应用 ECSS 时传达状态的方式。我使用了微命名空间类,除了节点上的任何现有类来传达这个状态。例如:

ECSS 以前如何处理状态变化


        .is-Suspended {} 
        .is-Live {} 
        .is-Selected {} 
        .is-Busy {}

在 DOM 中使用这些类的节点可能如下所示:


<button class="co-Button is-Selected">Old Skool Button</button>

注意

从历史上看,在 DOM 中更改类,特别是在 DOM 的根附近,是不鼓励的。这样做会使渲染树无效,这意味着浏览器必须执行大量的重新计算。然而,情况正在改善。WebKit(用于 iOS 和 Safari 浏览器)的 Antii Koivisto 最近的工作意味着这样的更改现在几乎是最佳的。有兴趣的人可以在这里阅读有关类更改的 WebKit 变更集:trac.webkit.org/changeset/196383,以及属性选择器,例如aria-*在这里:trac.webkit.org/changeset/196629

转向 WAI-ARIA

然而,通过为另一本书研究 ARIA 一点(如果您感兴趣的话),我发现如果这些信息纯粹是为了样式钩子而进入 DOM,它可能会在那里承担更多的重量。

这些相同的样式钩子实际上可以放在 DOM 中作为 WAI-ARIA(www.w3.org/TR/wai-aria/)状态。WAI-ARIA 的状态和属性部分描述了 W3C 标准化的方式,用于在应用程序中向辅助技术传达状态和属性。在 WAI-ARIA 的摘要描述的开头部分中,包含了这样的内容:

这些语义是为了让作者能够在文档级标记中正确传达用户界面行为和结构信息给辅助技术。

虽然规范旨在帮助通过辅助技术向残障用户传达状态和属性,但它也很好地满足了 ECSS 的需求。这是一个很好的结果!我们可以提高 Web 应用程序的可访问性,同时还获得了一个清晰定义、深思熟虑的词汇表,用于传达我们在应用程序逻辑中需要的状态。以下是使用 aria 重新编写的先前示例来传达状态:


<button class="co-Button" aria-selected="true">Old Skool Button</button>

类处理按钮的美学。aria-*属性传达该节点或其后代的状态(如果有)。

在 JavaScript 应用程序领域,唯一需要的更改是从classList修改为setAttribute修改。例如,设置我们的button属性:


button.setAttribute("aria-selected", "true");

提示

请注意,以这种方式分离关注点确实需要在 JavaScript 中添加一个触点。如果您绝对,绝对希望以最快/最简单的方式处理状态更改,那么使用classList更新一次将更快。

ARIA 属性作为 CSS 选择器

在我们首选的 CSS 语法中,将该更改写在一组大括号内将如下所示:

.co-Button {
    background-color: $color-button-passive;
    &[aria-selected="true"] {
        background-color: $color-button-selected;
    }
}

我们使用和符号(&)作为父选择器和属性选择器,以利用节点上的 aria 属性提供的增强特异性。然后我们可以根据需要对更改进行样式设置。

以这种方式嵌套状态更改在规则中提供了更高的开发人员人体工程学。意图是规则仅在整个应用程序样式中的根级别定义一次。这提供了一个真理的来源,以定义与该类相关的所有可能的情况。有关更多信息,请确保阅读第八章,理智样式表的十诫

提示

作为相关说明,CSS 选择器级别 4 规范drafts.csswg.org/selectors-4/#attribute-case)通过在右方括号之前使用i标志来提供不区分大小写。例如:

css .co-Button { background-color: $color-button-passive; &[aria-selected="true" i] { background-color: $color-button-selected; } } 这允许通过属性的任何情况值变体(默认情况下它区分大小写)

使用 ARIA 重新设计状态和属性

WAI-ARIA 规范的这一部分描述了Widget Attributes,这些属性包含了在处理 Web 应用程序和快速更改数据时所需的许多常见状态。

以下是使用 ARIA 重新编写的本章开头给出的示例:

还有很多,根据 W3C 规范标准,规范很容易理解。

如果无法使用 ARIA

如果由于任何原因,您无法使用aria-*属性来在站点或应用程序中传达状态。我现在倾向于在不使用微命名空间来指定状态的情况下命名选择器。例如,而不是:

如果无法使用 ARIA

 <button class="co-Button is-Selected">Old Skool
       Button</button>

我建议改用这样的选择器的变体版本:


<button class="co-Button co-Button-selected">Old Skool Button</button>

这保持了模块的上下文,并仅指示正在应用此相同类的变体。

注意

您应该知道使用属性选择器来传达状态时存在一个陷阱。某些较旧版本的 Android(例如 Android 4.0.3 原生浏览器)在属性值更改时不会强制重新计算样式。这样做的结果是,依赖于属性的任何样式都不会动态工作(例如在 JavaScript 中切换时)。有两种可能的解决方法。首先,您可以在 DOM 的某个地方切换一个类,同时更改属性。或者,您可以通过在 CSS 中的某个地方列出每个空规则来启动属性选择器。甚至链接在一起也可以工作,例如[aria-thing][aria-thing2]{}。任何一种选择都会给程序添加一个不必要的复杂性。关于这种行为的错误报告可以在此处找到:bugs.webkit.org/show_bug.cgi?id=64372,提到的解决方法来自于这个 Stack Overflow 问题:stackoverflow.com/questions/6655364/css-attribute-selector-descendant-gives-a-bug-in-webkit/

总结

使用 WAI-ARIA 状态来传达 DOM 中的变化提供了与标准 HTML 类一样有用且易于使用的样式钩子。尽管这纯粹是个人偏好,但我也喜欢在样式表中使用完全不同的选择器来传达状态;这样在规则中更容易识别。

这些先前的因素都没有给你带来任何新东西。使用 WAI-ARIA 状态将几乎免费地开始为辅助技术用户提供更好的通信方式。如果金钱有话说,还要考虑,通过使用 WAI-ARIA,您可以将产品扩展到更多的用户(请参见下面的附加信息)。

因此,使用 WAI-ARIA 状态和属性是采用 ECSS 方法的项目中传达状态的推荐方式。

来自英国皇家国家盲人协会(RNIB)的附加信息和统计数据

RNIBrnib.org.uk/)很友好地提供了一些关于英国盲人数量的数据。在为您的项目使用 ARIA 状态进行辩论/考虑时,这些数据可能会有所帮助。

  • 在英国,有超过 84,000 名注册的盲人和部分视力受损的工作年龄人口(估计总人口约为 6400 万)。

然而,根据政府的劳动力调查,英国有大约 185,000 名工作年龄人口自报有视力困难。这包括那些视力损失不符合注册条件但仍足以影响他们日常生活的人。也包括那些不认为自己是残疾人的人。在这 185,000 人中:

  • 113,000 人认为自己有视力困难并认为自己是长期残疾人

  • 72,000 人认为自己有视力困难但不认为自己是残疾人

  • 2011 年英国视力受损人口估计人数-1,865,900

  • 2020 年英国预计患有视力受损的人数-2,269,700

  • 2012 年英国成年糖尿病患者人数,这是视力受损的主要原因-3,866,980

第七章:将 ECSS 应用于您的网站或应用程序

在本章中,我们将涵盖以下主题:

  • 将 ECSS 应用于逻辑模块

  • 将 ECSS 应用于视觉模块

  • 组织模块、它们的组件和命名文件

  • 使用 CMS 生成的内容

  • ECSS 和全局样式

ECSS 非常适用于复杂的 Web 应用程序。首先,让我们考虑如何在大型应用程序的逻辑周围应用 ECSS。

将 ECSS 应用于逻辑模块

通常,在 Web 应用程序中,某种编程语言(例如 JavaScript/TypeScript/Ruby/等)将生成某个东西

使用该东西的文件名作为模块(或模块的组件)的名称通常是实用且可取的。因此,如果一个文件名为Header.js并生成了页眉的容器,那么页眉的任何组件部分都可以相应地命名。例如,在 ECSS 术语中,公司注册号可能会得到sw-Header_Reg作为其选择器。扩展一下,页眉内的搜索框组件可能具有类似sw-HeaderSearch_Input的选择器(由HeaderSearch.js文件创建的输入框)。

一个例子

让我们考虑一个更具体的例子。假设我们正在编写一个 JavaScript 客户端应用程序,并且有一个名为ShoppingCartLines.js的组件。它的任务是渲染出购物车中的行,然后显示在名为ShoppingCart.js的模块中。ShoppingCart模块渲染出与购物车本身有关的任何内容。到目前为止都很简单。

现在让我们通过建议,在某些情况下,我们的购物车将在模态视图中工作,在其他情况下,作为页面的一部分,正常文档流中,来使我们想象的情景稍微复杂化。

在这种情况下,我们有一个更广泛的模块:ShoppingCart和一个通常位于模块内的组件称为ShoppingCartLines。每个都将有自己的子节点。该模块和组件有两种可能的视图:在模态中和在页面中。让我们还想象一下,上下文的切换将由应用程序逻辑处理。

我们的常数是模块本身,我们可以使用命名空间为其提供上下文。在应用 ECSS 处理应用程序逻辑时,始终使用应用程序模块或组件的完整名称作为 ECSS 样式选择器的模块部分是有意义的。这样做的好处是,使 DOM 中的所有 HTML 类都能描述其来源和目的。

提示

在命名模块或组件的最外层容器的类时,不应该向类/选择器添加子扩展。只有模块或组件的子部分应该获得节点扩展。

好的,所以,目前为止,我们的选择器在样式表中可以这样命名:

.mod-ShoppingCart {} /*Modal*/
.page-ShoppingCart {} /*Page*/
.mod-ShoppingCartLines {} /*Modal*/
.page-ShoppingCartLines {} /*Page*/

这样,我们的模块和组件通过命名空间切换隔离了它们的两个上下文。我们可以自由地根据需要为每个模块进行样式设置,而不会从一个模块泄漏到另一个模块。这是典型的情况,当组件和模块共享 HTML 类以实现抽象和重用时,通常会变得复杂。

让我们考虑一下这种情景的变化。假设我们不会在应用程序逻辑中切换上下文,而是在媒体查询中切换样式。在较小的视口上有一个模态实现,在较大的视口上有页面样式,正常文档流中。

在这种情况下,我们可以使用单个命名空间,例如sc-ShoppingCart(我使用sc-来指定上下文为ShoppingCart),并在 CSS 中使用媒体查询来提供视觉变化。

例如:

.sc-ShoppingCart {
    /*Modal styles for smaller viewports*/
    @media (min-width: $M) {
        /* Page styles for larger viewports */
    }
}

.sc-ShoppingCartLines {
    /* Modal styles for smaller viewports */
    @media (min-width: $M) {
        /* Page styles for larger viewports */
    }
}

模块或组件的子节点

如前所述,模块或组件将有其自己的子节点元素。这些选择器应该使用子扩展进行命名。例如:

.sc-ShoppingCart {
    /* The root of the component/module, no child
    extension needed */
}

.sc-ShoppingCart_Title {
    /* The 'title' child node of the Shopping Cart */
}

.sc-ShoppingCart_Close {
    /* A 'close' button child of the Shopping Cart for
    when the cart is modal */
}

每个子节点都会获得其父节点的命名空间和组件(或模块)名称。

提示

有关 ECSS 命名约定的详细信息,请参阅第五章,文件组织和命名约定

因此,此时我们已经了解了在应用 ECSS 时如何命名我们的选择器,围绕应用模块和逻辑。现在我们将看看如何命名选择器并在纯粹的视觉模块周围应用 ECSS。但首先,关于使用类型选择器的一个简短但重要的离题。

关于类型选择器的说明

在编写 CSS 时,有时会诱人地使用类型选择器。通常情况下,这是在存在 HTML5 文本级元素时,比如<i><b><em><span>。例如,假设我们有一个句子,其中有几个需要加粗的单词。那么诱惑就会是这样做:

关于类型选择器的说明

 <p class="ch-ShoppingCart_TextIntro">Here is the contents
 of your cart. You currently have <b>5 items</b>.</p>

并使用这些选择器来应用样式到b标签的内容:

关于类型选择器的说明

 .ch-ShoppingCart_TextIntro { 
      /* Styles for the text */ 
      b { 
        /* Styles for the bold section within */ 
      } 
  }

这里有几个问题:

  1. 我们已经对某些标记结构创建了依赖(它必须是一个子节点并且是一个b标签)。

  2. 由于第 1 点,我们创建了一个比必要更具体的选择器。这使得任何未来的覆盖更难以理解和执行。

虽然这可能看起来过于冗长,但这就是应该处理该场景的方式:

<p class="ch-ShoppingCart_TextIntro">Here is the contents of your cart. You currently have <b class="ch-ShoppingCart_TextIntroStrong">5 items</b>.</p>

并且这个 CSS:

.ch-ShoppingCart_TextIntro {
    /* Styles for the text */
}

.ch-ShoppingCart_TextIntroStrong {
    /* Styles for the bold section within */
}

每个元素都有自己的选择器和规则。它们互不依赖。也不需要特定的标记来应用任何规则。

提示

应用于元素的每个规则都应尽可能地对其自身的外观持有意见。例如,如果您有一个包含两个文本节点的元素,似乎逻辑上将字体大小和行高应用于包装元素,以便两个文本节点将从中继承。但是,这会阻止该文本节点被移动到另一个位置并保持一致的渲染。相反,对每个节点应用颜色、字体大小和行高,即使它们最初非常相似(也许一开始只有颜色不同)。这起初似乎有违直觉,但可以防止未来可能的偏差(在 DOM 中移动,样式分歧等)。

将 ECSS 应用于视觉模块

视觉组件指的是不一定由特定应用逻辑生成的标记区域。

您仍然可以将区域分成逻辑视觉区域,并对其应用 ECSS。这是ecss.io网站采用的方法。

没有硬性规定。例如,我们可以将设计分为结构、菜单、页脚、导航、快速跳转菜单、主图等视觉区域。

在这种情况下,我们的选择器看起来像这样:

.st-Header {
    /* Structural container for header */  
}

.st-Footer {
    /* Structural container for footer */
}

然而,我们也可以这样做:

.hd-Outer {
    /* Structural container for header */ 
}

.ft-Outer {
    /* Structural container for footer */
}

或者如果是模块的话,也可以这样:

.hd-Header {
    /* Structural container for the Header module */
}

.ft-Footer {
    /* Structural container for the footer module */
}

这些方法都没有对错之分。只要子节点/选择器遵循相同的命名约定,样式就会被隔离到特定区域。

事实是,在较小的网站上,您可以使用几乎任何类命名方法,碰撞的危险将是最小的。但是,一旦项目开始增长,命名空间和严格的命名约定的好处将开始丰厚地回报您。只需做出决定,并一致地应用该选择。

组织模块、它们的组件和命名文件

在这一点上,我认为考虑一个更详细的示例模块结构将是有用的。它类似于我习惯使用 ECSS 的结构。它比我们之前的例子更复杂,提供了另一种略有不同的文件组织和选择器命名变化。从我们的 CSS 角度来看,我们的目标是隔离、一致性和良好的开发人员人体工程学。让我们来看看。

假设我们有一个模块。它的工作是加载我们网站的侧边栏区域。目录结构可能最初看起来像这样:

SidebarModule/ => everything SidebarModule related lives in here
  /assets => any assets (images etc) for the module
  /css => all CSS files
  /min => minified CSS/JS files
  /components => all component logic for the module in
  here
  css-namespaces.json => a file to define all namespaces
  SidebarModule.js => logic for the module
  config.json => config for the module

就示例标记结构而言,我们期望这个模块应该产生类似这样的东西:

<div class="sb-SidebarModule">

</div>

样式化此初始元素的 CSS 应该放在css文件夹中,就像这样:

SidebarModule/ 
  /assets 
  /css 
    /components 
    SidebarModule.css 
/min 
/components 
css-namespaces.json 
SidebarModule.js 
config.json

现在,假设我们在SidebarModule内有一个组件,它为SidebarModule创建一个标题。我们可能会将组件命名为一个名为Header.js的文件,并将其存储在SidebarModulecomponents子文件夹中,就像这样:

SidebarModule/ 
  /assets 
  /css
    /components 
    SidebarModule.css 
/min 
/components 
  Header.js 
css-namespaces.json 
SidebarConfig.js 
SidebarModule.js 
config.json

有了这个,Header.js可能会呈现如下标记:

<div class="sb-SidebarModule">
    <div class="sb-Header">
        <div class="sb-Header_Logo"></div>
    </div>
</div>

注意Header组件,由于在SidebarModule的上下文中,携带sb-微命名空间来指定其父级。并且由这个新组件创建的节点根据创建它们的逻辑进行命名。

就一般约定而言:

组件应携带原始逻辑的微命名空间。如果您正在创建一个位于模块内的组件,它应携带原始模块的命名空间(模块的可能命名空间在css-namespaces.json中定义)。

HTML 类/CSS 选择器应根据生成它们的文件名/组件进行命名。例如,如果我们在我们的模块内创建了另一个名为HeaderLink.js的组件,它将在Header.js组件的子级内呈现其标记,那么它生成的标记和适用的 CSS 选择器应与此文件名匹配。

例如:

<div class="sb-SidebarModule">
    <div class="sb-HeaderPod">
        <div class="sb-HeaderPod_Logo"></div>
    </div>
    <div class="sb-HeaderPod_Nav">
        <div class="sb-HeaderLink">Node Value</div>
        <div class="sb-HeaderLink">Node Value</div>
        <div class="sb-HeaderLink">Node Value</div>
        <div class="sb-HeaderLink">Node Value</div>
    </div>
</div>

就文件夹结构而言,现在看起来是这样的:

SidebarModule/ 
  /assets 
  /css 
    /components 
      Header.css 
      HeaderLink.css 
    SidebarModule.css 
  /min 
  /components 
    Header.js
    HeaderLink.js 
  css-namespaces.json 
  SidebarConfig.js 
  SidebarModule.js
  tsconfig.json

注意组件逻辑(*.js文件)和相关样式(*.css文件)之间存在一对一的对应关系-两者都位于components子文件夹中。尽管逻辑和样式不共享相同的直接父文件夹,但它们都位于同一个模块文件夹中,如果需要,可以轻松删除整个模块。

组件内的节点

总之。以这种方式使用,组件内节点的 ECSS 命名约定应始终是:

ns-Component_Node-variant
  • ns:微命名空间(始终小写)

  • -Component:组件名称(始终使用大驼峰命名法)

  • _Node:组件的子节点(始终大驼峰,前面有下划线)

  • -variant:节点的可选变体(始终小写,并在连字符之前)

变体

请注意,组件内节点的-variant部分是可选的,只应用于表示细微差异的项目。例如,除了不同的背景图像之外完全相同的多个标题可能会呈现如下:

<div class="sb-Classification_Header sb-Classification_Header-2"></div>

请记住,我们在第五章中更详细地讨论了变体选择器,文件组织和命名约定

从 CMS 生成的内容中工作

很可能,如果您使用 ECSS 与任何类型的内容管理系统(Wordpress、Ghost、Drupal 等),您将遇到一种情况,即不可能向每个元素添加类。例如,在 Wordpress 页面或帖子中,期望用户输入内容并记住要添加到每个段落标记的正确类是不现实的。在这些情况下,我认为务实必须取胜。

为封闭元素设置一个 ECSS 类,并(勉强)接受所有嵌套元素都将使用类型选择器进行设置。以下是一些示例标记:

<main class="st-Main">
    <h1>How to survive in South Central?</h1>
    <p>A place where bustin' a cap is fundamental. </p>
    <ul>
        <li>Rule number one: get yourself a gun. A nine in yo' ass'll be fine</li>
        <li>Rule number two: don't trust nobody.</li>
    </ul>    

</main>

以下是您可能编写的 CSS 来处理选择这些元素的方式:

.st-Main {
    h1 {
        /* Styles for h1 */
    }
    p {
        /* Styles for p */
    }
    ul {
        /* Styles for ul */
    }
    li {
        /* Styles for li */
    }
}

我对此并不疯狂。我们正在嵌套选择器,将我们的样式与元素绑定,基本上是我们通常希望避免的 ECSS。但是,我很诚实。现实情况是,这可能是我们能够做出的最好妥协。在可能向元素添加类的情况下,我们绝对应该这样做。但是,会有一些情况下这根本不可能,任何象牙塔的理想主义都无法在这些情况下帮助。记住平青哥

ECSS 和全局样式

虽然网页应用中大部分的 CSS 可以描述为基于模块的,但我们需要处理的全局 CSS 也是不可避免的。从 ECSS 的角度来看,我们应该尽量保持全局 CSS 的最小化。通常,除了任何必需的重置样式之外,还会有默认的字体大小、字体系列,也许还有一些默认的颜色。这些样式通常应用于类型选择器。除非你在根 HTML 元素或者 body 上有类。

注意

如果你正在寻找一个网页应用的基本重置样式集,你可能会发现我的App Reset CSS 很有用。你可以在 GitHub 上找到它:github.com/benfrain/app-reset,或者通过 NPM 安装npm install app-reset

可能还需要一些全局结构。例如,如果你的应用程序中有一个常见的结构(头部、页脚、侧边栏等),你可能希望创建一些选择器来反映这一点。过去,我曾经使用.st-.sw-微命名空间来定义结构站点范围,但你可以使用最适合你的任何东西。然而,我的建议是,这些选择器实际上不应该有很多,因为这些通常涉及到应用程序的所有模块应该存在的非常广泛的领域。

在组织全局 CSS 方面,我目前更倾向于在任何项目的根目录下创建一个名为globalCSS的文件夹。在那个文件夹里会有任何变量、混合、全局图像资源、任何字体或图标字体文件,一个基本的 CSS 重置文件和任何需要的全局 CSS。

总结

在本章中,我们已经看过了你可能在项目中应用 ECSS 的两种主要方式。我们还考虑了一个完整和更复杂的模块可能的文件夹结构。我希望到这一点,你已经对如何在你的项目中应用 ECSS 有了一个大致的想法。

与实施 CSS 的架构方法相辅相成的是实际编写样式表的实践。你知道,在编辑器中代码实际上是什么样子的。本书中的代码示例一直在演示这种语法,但现在是时候更详细地深入研究了。

如何最好地编写样式表来实践所有这些 ECSS 的花哨东西,这将是我们在下一章中要讨论的内容。

第八章:理智样式表的十诫

  1. 你应该有一个所有关键选择器的单一真相来源

  2. 除非你正在嵌套媒体查询或覆盖,否则不应该嵌套

  3. 即使你认为你必须使用 ID 选择器,也不应该使用 ID 选择器

  4. 在编写样式表中不应该写厂商前缀

  5. 你应该使用变量来设置大小、颜色和 z-index

  6. 你应该总是首先编写移动规则(避免使用 max-width)

  7. 节制使用混合,并避免 @extend

  8. 你应该注释所有魔术数字和浏览器黑客

  9. 不应该内联图片

  10. 当简单的 CSS 能够正常工作时,你不应该编写复杂的 CSS

遵循这些规则的人是有福的,因为他们将继承理智的样式表。

阿门。

为什么是十诫?

以下是一套高度主观的规则,是为了在开发团队中编写可预测的样式表而产生的。每个规则都可以通过工具强制执行。当一个项目中只有一个 CSS 开发人员时,花时间开发或集成工具可能看起来是多余的。然而,在超过两个活跃的开发人员之后,工具将一次又一次地赚取它的时间投资。我们将在下一章中处理工具来“监督”规则。现在,让我们考虑语法和规则本身。

工具

为了实现更易维护的样式表,我们可以依赖于 PostCSS,这是一个允许使用 JavaScript 操作 CSS 的 CSS 工具。有兴趣的人可以在这里查看更多信息:github.com/postcss/postcss

PostCSS 促进了扩展 CSS 语法的使用。为了编写,所使用的语法大量借鉴了 Sass (sass-lang.com/)。这提供了使我们的编写样式表更易于维护的功能。

使用 PostCSS,我们能够利用:

  • 变量

  • 混合(如宏,用于某些设置,如字体系列)

  • 使用和符号(&)引用关键选择器

实际上,PostCSS 可以实现类似于 CSS 预处理器(如 Sass、LESS 或 Stylus)的功能。

它的不同之处在于它的模块化和可扩展性。与前面提到的预处理器需要“吞下整颗药丸”不同,使用 PostCSS 允许我们更加选择我们使用的功能集。它还允许我们轻松地根据需要扩展我们的功能集,可以使用任意数量的“现成的”插件,或者使用 JavaScript 编写我们自己的插件。

例如,Sass 允许编写循环,我们选择阻止该功能。例如,如果需要循环来解决特定问题(例如,100 种不同颜色的标题变体),我们仍然可以通过使用 JavaScript 编写的 PostCSS 插件来实现。

此外,由于 PostCSS 生态系统,我们可以对编写的样式进行静态分析和 linting;当编写不良代码时,会导致构建失败和代码提交。

注意

如果术语“linting”对你来说是陌生的,那么它是静态分析的另一个术语。它查看编写的代码,并根据任意数量的预定义规则提出建议。例如,如果你使用浮动,或者没有在需要的地方放置空格或分号,它可能会发出警告。一般来说,你可以使用 linters 来强制执行任何你喜欢的编码约定,而且在独自工作时非常有用,但在团队中工作时可能是无价的:在那里,许多(粗心的)人可能会触及代码。

原理

当我们编写 ECSS 时,我们希望避免产生 CSS,这些 CSS 遭受过于具体、充斥着不需要的前缀、缺乏注释和充满“魔法”数字的问题。

以下 10 条规则阐明了被认为是实现此目标最重要的规则。

Throughout 使用的定义:

  • 覆盖:根据继承有意修改关键选择器的值的情况

  • 关键选择器:任何 CSS 规则中最右边的选择器

  • 前缀:供应商特定前缀,例如-webkit-transform:

  • 编写样式表:我们在其中编写样式规则的文件

  • CSS:工具生成的结果 CSS 文件,最终由浏览器消耗

现在让我们考虑每个规则及其旨在解决的问题。

1. 所有关键选择器都应该有一个单一的真相来源

在编写样式表中,关键选择器应该只写一次。

这使我们能够在代码库中搜索关键选择器并找到我们选择器的单一真相来源。由于使用了扩展的 CSS 语法,发生在关键选择器上的一切都可以封装在一个规则块中。

通过嵌套和使用选择器引用关键选择器来处理对关键选择器的覆盖。稍后会详细讨论。

考虑这个例子:

.key-Selector {
    width: 100%;
    @media (min-width: $M) {
        width: 50%;
    }
    .an-Override_Selector & {
        color: $color-grey-33;
    }
}

这将在 CSS 中产生以下结果:

.key-Selector {
  width: 100%;
}

@media (min-width: 768px) {
  .key-Selector {
    width: 50%;
  }
}

.an-Override_Selector .key-Selector {
  color: #333;
}

在编写样式表中,关键选择器(.key-Selector)在根级别永远不会重复。因此,从维护的角度来看,我们只需要在代码库中搜索.key-Selector,就能找到关于该关键选择器的一切内容,这是一个单一的真相来源。

  • 如果我们需要在不同的视口大小下以不同的方式显示,会发生什么?

  • 当它存在于 containerX 中时会发生什么?

  • 当通过 JavaScript 向其添加这个或那个类时会发生什么?

在所有这些情况下,关键选择器的可能性都嵌套在同一个规则块中。这意味着任何可能的特异性问题都完全隔离在一个大括号集合内。

让我们接下来更详细地看一下覆盖。

处理覆盖

在先前的例子中,演示了如何处理对关键选择器的覆盖。我们将覆盖选择器嵌套在关键选择器的规则块内,并使用&符号引用父级。&符号在 Sass 语言中是父选择器。你可以把它想象成 JavaScript 中的this

提示

要使用父选择器测试规则,我建议使用sassmeister.com

标准覆盖

考虑这个例子:

.ip-Carousel {
    font-size: $text13;
    /* The override is here for when this key-selector sits within a ip-HomeCallouts element */
    .ip-HomeCallouts & {
        font-size: $text15;
    }
}

这将产生以下 CSS:

.ip-Carousel {
  font-size: 13px;
}

.ip-HomeCallouts .ip-Carousel {
  font-size: 15px;
}

这会导致ip-Carousel在具有ip-HomeCallouts类的元素内部时font-size增加。

在同一元素上使用额外的类进行覆盖

让我们考虑另一个例子,如果我们需要在此元素获得额外类时提供覆盖?我们应该这样做:

.ip-Carousel {
    background-color: $color-green;
    &.ip-ClassificationHeader {
        background-color: $color-grey-a7;
    }
}

这将产生以下 CSS:

.ip-Carousel {
  background-color: #14805e;
}

.ip-Carousel.ip-ClassificationHeader {
  background-color: #a7a7a7;
}

同样,覆盖包含在关键选择器的规则块内。

在另一个类中覆盖并且还有额外的类

最后让我们考虑一种情况,我们需要为另一个元素内部的关键选择器提供覆盖,该元素还有额外的类:

.ip-Carousel {
    background-color: $color-green;
    .home-Container &.ip-ClassificationHeader {
        background-color: $color-grey-a7;
    }
}

这将产生以下 CSS:

.ip-Carousel {
  background-color: #14805e;
}

.home-Container .ip-Carousel.ip-ClassificationHeader {
  background-color: #a7a7a7;
}

在这里,我们使用父选择器来引用我们的关键选择器,介于上面的覆盖(.home-Container)和另一个类(.ip-ClassificationHeader)之间。

使用媒体查询进行覆盖

最后,让我们考虑使用媒体查询进行覆盖。考虑这个例子:

.key-Selector {
    width: 100%;
    @media (min-width: $M) {
        width: 50%;
    }
}

这将产生以下 CSS:

.key-Selector {
  width: 100%;
}

@media (min-width: 768px) {
  .key-Selector {
    width: 50%;
  }
}

再次,所有可能性都包含在同一个规则内。注意媒体查询宽度的变量使用?我们很快会讨论到这一点。

任何和所有媒体查询都应以相同的方式包含。以下是一个更复杂的例子:

.key-Selector {
    width: 100%;
    @media (min-width: $M) and (max-width: $XM) and (orientation: portrait) {
        width: 50%;
    }
    @media (min-width: $L) {
        width: 75%;
    }
}

这将产生以下 CSS:

.key-Selector {
  width: 100%;
}

@media (min-width: 768px) and (max-width: 950px) and (orientation: portrait) {
  .key-Selector {
    width: 50%;
  }
}

@media (min-width: 1200px) {
  .key-Selector {
    width: 75%;
  }
}

通过刚刚查看的所有覆盖的嵌套,你可能会认为嵌套子元素也是有道理的?你错了。非常错误。这将是一件非常非常糟糕的事情。接下来我们将看看为什么。

2. 除非你是嵌套媒体查询或覆盖,否则不得嵌套

CSS 中的关键选择器是任何规则中最右边的选择器。它是应用封闭属性/值的选择器。

我们希望我们的 CSS 规则尽可能扁平。我们不希望在关键选择器(或任何 DOM 元素)之前有其他选择器,除非我们绝对需要它们来覆盖默认的关键选择器样式。

原因是添加额外的选择器并使用元素类型(例如h1.yes-This_Selector):

  • 创建额外不需要的特异性

  • 使维护变得更加困难,因为随后的覆盖需要更加具体

  • 向结果 CSS 文件添加不必要的膨胀

  • 在元素类型的情况下,将规则绑定到特定元素和/或标记结构

例如,假设我们有这样的 CSS 规则:

2. 除非你在嵌套媒体查询或覆盖,否则不要嵌套

 #notMe .or-me [data-thing="nope"] .yes-This_Selector {
 width: 100%; 
 }

在上面的例子中,yes-This_Selector是关键选择器。如果这些属性/值应该在所有情况下添加到关键选择器中,我们应该制定一个更简单的规则。

简化前面的例子,如果我们只想针对关键选择器进行目标定位,我们会希望有这样的规则:

.yes-This_Selector {
    width: 100%;
}

不要在规则中嵌套子元素

假设我们有这样的情况,我们在包装元素内部有一个视频播放按钮。考虑这个标记:

<div class="med-Video">
    <div class="med-Video_Play">Play</div>
</div>

让我们为包装器设置一些基本样式:

.med-Video {
    position: absolute;
    background-color: $color-black;
}

现在我们想要在包装元素内部定位播放元素。你可能会想这样做:

不要在规则中嵌套子元素

 .med-Video {
 position: absolute; 
 background-color: $color-black; 
 /* Center the play button */
 .med-Video_Play { 
 position: absolute; 
 top: 50%; 
 left: 50%;
 transform: translate(-50%, -50%); 
 } 
 }

这将产生以下 CSS(为简洁起见删除了供应商前缀):

不要在规则中嵌套子元素

 .med-Video { 
 position: absolute; 
 background-color: #000;
 /* Center the play button */ 
 } 
 .med-Video .med-Video_Play { 
 position: absolute; 
 top: 50%; 
 left: 50%; 
 transform: translate(-50%, -50%); 
 }

你看到问题了吗?当完全不需要时,我们为.med-Video_Play元素引入了额外的特异性。

这是一个微妙的例子。然而,重要的是要意识到这一点,并避免这样做,以免最终得到这样的规则:

不要在规则中嵌套子元素

 .MarketGrid > .PhoneOnlyContainer > .ClickToCallHeader >
  .ClickToCallHeaderMessage > .MessageHolder > span { 
 font-weight: bold; 
 padding-right: 5px; 
 }

相反,记住每个关键选择器都有自己的规则块。覆盖是嵌套的,子元素不是。这是正确重写的示例:

.med-Video {
    position: absolute;
    background-color: $color-black;
}

/* Center the play button */
.med-Video_Play {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

这将产生以下 CSS:

.med-Video {
  position: absolute;
  background-color: #000;
}

/* Center the play button */
.med-Video_Play {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

每个关键选择器只有在需要时才会具体化,而不会更多。

3. 即使你认为你必须使用 ID 选择器,也不要使用

在复杂 UI 中 ID 的限制已经有了详细的记录。简而言之,它们比类选择器更具体 - 因此使覆盖更加困难。此外,它们在页面中只能使用一次,因此它们的有效性有限。

提示

记住我们在第二章中详细讨论了特异性,CSS 在规模上的问题

使用 ECSS 时,我们不在 CSS 中使用 ID 选择器。它们与基于类的选择器相比没有任何优势,并引入了不需要的问题。

在几乎难以置信的情况下,如果您必须使用 ID 来选择元素,请在属性选择器中使用它以保持特异性较低:

[id="Thing"] {
    /* Property/Values Here */
}

4. 不要在编写样式表中写供应商前缀

多亏了 PostCSS,我们现在有了工具,这意味着在编写样式表中不需要为 W3C 指定的属性/值编写供应商前缀。这些前缀由Autoprefixergithub.com/postcss/autoprefixer)工具自动处理,可以配置为为所需的平台/浏览器支持级别提供供应商前缀。

例如,不要这样做:

4. 不要在编写样式表中写供应商前缀

 .ip-Header_Count { 
 position: absolute; 
 right: $size-full;
 top: 50%; 
 -webkit-transform: translateY(-50%); 
 -ms-transform: translateY(-50%); 
 transform: translateY(-50%); 
 }

相反,你应该只写这个:

.ip-Header_Count {
    position: absolute;
    right: $size-full;
    top: 50%;
    transform: translateY(-50%);
}

这不仅使编写样式表更易于阅读和处理,而且意味着当我们想要改变我们的支持级别时,我们可以对构建工具进行单一更改,添加的供应商前缀将自动更新。

唯一的例外是可能仍然需要非 W3C 属性/值的情况。例如,在 WebKit 设备中的触摸惯性滚动面板,仍然需要在作者样式中添加某些供应商前缀的属性,因为它们不是 W3C。例如:

.ui-ScrollPanel {
    -webkit-overflow-scrolling: touch;
}

或者在移除 WebKit 的滚动条时:

.ui-Component {
    &::-webkit-scrollbar {
      -webkit-appearance: none;
    }
}

5. 使用变量进行尺寸、颜色和 z-index 设置

对于任何规模的项目,设置尺寸、颜色和 z-index 的变量是必不可少的。

UI 通常基于某种网格或尺寸比例。因此,尺寸应该基于固定尺寸,并对这些尺寸进行合理的划分。例如,这里是基于11px的尺寸和变量的变体:

$size-full: 11px;
$size-half: 5.5px;
$size-quarter: 2.75px;
$size-double: 22px;
$size-treble: 33px;
$size-quadruple: 44px;

对于开发人员来说,使用变量还提供了额外的经济效益。例如,它可以节省从复合中选择颜色值。它还有助于规范化设计。

例如,如果一个项目只使用 13px、15px 和 22px 的字体大小,并且有一个变化要求 14px 的字体大小,那么变量提供了一些标准化的参考。在这种情况下,如果字体是 13px 或 15px,因为 14px 在其他地方都没有使用?这使开发人员可以向设计人员反馈可能存在的设计不一致之处。

颜色值也是如此。例如,假设我们有一个十六进制#333的变量。我们可以这样写一个变量:

$color-grey-33: #333333;

表面上看,当十六进制值更短时,写变量名似乎是荒谬的。然而,再次强调,使用变量可以防止不需要的变体渗入代码库(例如#323232),并有助于识别代码中的红旗

在对颜色进行修改时,仍然使用变量是很重要的。使用颜色函数对变量进行操作以实现你的目标。例如,假设我们想要一个半透明的#333颜色。

应该在作者样式表中实现这样:

.ip-Header {
    background-color: color($color-grey-33 a(.5));
}

PostCSS 可以为 W3C 颜色函数提供一个 polyfill:drafts.csswg.org/css-color/#modifying-colors,上面的示例产生了以下 CSS:

.ip-Header {
    background-color: rgba(51, 51, 51, 0.5);
}

在这个例子中,我们使用了 alpha CSS 颜色函数。我们使用color()函数,传入我们想要操作的颜色,然后进行操作(在这种情况下是 alpha)。

最初使用变量可能看起来更复杂,但这样可以让未来的作者更容易理解正在操作的颜色是什么。

提示

我还鼓励你看看CSS Color Guardgithub.com/SlexAxton/css-colorguard),这是一个用于警告代码库中颜色在视觉上难以区分的工具。

同样重要的是使用变量来设置 z-index。这在堆叠上下文方面是很重要的。不应该需要z-index: 999或类似的东西。而是使用几个默认值(设置为变量)中的一个。这里有一些与 z-index 相关的变量:

$zi-highest: 50;
$zi-high: 40;
$zi-medium: 30;
$zi-low: 20;
$zi-lowest: 10;
$zi-ground: 0;
$zi-below-ground: -1;

6. 总是首先编写移动端规则(避免使用 max-width)

对于任何响应式工作,我们希望在样式中采用移动优先的思维方式。因此,规则的根部应该是适用于最小视口(例如移动设备)的属性和值。然后我们使用媒体查询来覆盖或添加这些样式,根据需要。

考虑这个:

.med-Video {
    position: relative;
    background-color: $color-black;
    font-size: $text13;
    line-height: $text15;
    /* At medium sizes we want to bump the text up */
    @media (min-width: $M) {
        font-size: $text15;
        line-height: $text18;
    }
    /* Text and line height changes again at larger
    viewports */
    @media (min-width: $L) {
        font-size: $text18;
        line-height: 1;
    }
}

这将产生以下 CSS:

.med-Video {
  position: relative;
  background-color: #000;
  font-size: 13px;
  line-height: 15px;
}

@media (min-width: 768px) {
  .med-Video {
    font-size: 15px;
    line-height: 18px;
  }
}

@media (min-width: 1200px) {
  .med-Video {
    font-size: 18px;
    line-height: 1;
  }
}

我们只需要在不同的视口更改font-sizeline-height,所以这就是我们要修改的全部内容。通过在媒体查询中使用min-width(而不是max-width),如果在更大的视口尺寸上font-sizeline-height需要保持不变,我们就不需要额外的媒体查询。只有当视口尺寸变化时,我们才需要媒体查询。因此,不建议将max-width作为媒体查询的单个参数。

底线:使用min-width而不是max-width编写媒体查询。这里唯一的例外是如果您想将一些样式隔离到中等范围。例如:在中等和大型视口之间。

.med-Video {
    position: relative;
    background-color: $color-black;
    font-size: $text13;
    line-height: $text15;
    /* Between medium and large sizes we want to bump the
    text up */
    @media (min-width: $M) and (max-width: $L) {
        font-size: $text15;
        line-height: $text18;
    }
}

7. 谨慎使用 mixins(避免@extend)

避免将代码抽象为 mixins 的诱惑。有一些领域是 mixins 非常适合的。CSS 文本截断的代码(例如@mixin Truncate)或 iOS 风格的惯性滚动面板,其中有许多伪选择器需要针对不同的浏览器进行正确设置。另一个很好的用例可能是复杂的字体堆栈。

提示

字体堆栈很难设置,而且很烦人。我发现处理字体的最理智的方法是让body使用最常见的字体堆栈,然后只在需要时用不同的字体堆栈覆盖它。

例如:

.med-Video {
    position: relative;
    background-color: $color-black;
    font-size: $text13;
    line-height: $text15;
    /* At medium sizes we want to bump the text up */
    @media (min-width: $M) {
        @mixin FontHeadline;
        font-size: $text15;
        line-height: $text18;
    }
}

对于更简单的字体堆栈,变量可以轻松处理这个需求,因此可能更可取。然而,对于更复杂的字体堆栈,混合很适合,其中在某些情况下最好使用某些字体堆栈。例如,也许 LoDPI 需要一个字体,而 HiDPI 需要另一个字体。这些情况不能仅通过使用变量来处理,因此需要根据需要使用混合。

最终,一个项目中应该有十个或更少的 mixins。如果超过这个数量,那么可能是滥用 mixins 来无谓地抽象代码。

避免@extend

我第一次接触@extend是在使用 Sass 时(sass-lang.com/documentation/file.SASS_REFERENCE.html#extend)。@extend指令使一个选择器继承另一个选择器的样式。虽然这可能会带来一些文件大小的好处,但它可能会使调试变得更加困难,因为规则以一种在编写时不一定能够预测的方式组合在一起。

为了确定使用@extend是否值得,我在当时正在使用的 Sass 代码库上进行了一个简短的实验。有 73 个实例需要一个 Headline 字体堆栈,37 个实例需要一个 headline condensed 字体堆栈(因此,如果选择 mixins 路线,那就是 73 个@include Headline和 37 个@include HeadlineCondensed)。

让我们看看没有任何字体引用的文件大小,字体引用定义为 mixins/@includes,然后字体引用为@extend

没有字体引用

没有任何字体声明:

105.5 KB(最小化),14.2 KB(Gzipped)

这是我们的base或者说控制。让我们看看通过 mixins/@includes 添加所有字体的区别。

使用@includes

使用 mixins(Sass 中的@include)HeadlineHeadline Condensed的结果 CSS 文件大小为:

146.9 KB(最小化),15.4 KB(Gzipped)

因此,增加了 1.2 KB。@extend的表现如何?

使用@extend

使用@extend而不是@include

106.9 KB(最小化),14.5(Gzipped);只增加了 0.3 KB 的文件大小。

从这些轶事数据中得出什么结论?对我来说,其他一切都相等的话,如果你绝对想要最小的文件大小,也许@extend是一个好选择。虽然节省不多,但确实有一些。

然而,务实地说,如果使用@include而不是@extend可以获得任何可维护性的收益,我肯定不会担心文件大小。

就我个人而言,我不允许在项目中使用@extend功能。它增加了调试的复杂性,而好处很少。

8. 一切魔数和浏览器 hack 都应该有注释

每个项目都应该有一个包含与项目相关的所有变量的变量文件。

提示

PostCSS 可以在 CSS 文件或 JavaScript 对象中定义变量和 mixins。您可以在benfrain.com/creating-and-referencing-javascript-mixins-and-variables-with-postcss/了解更多关于后者的信息。

如果出现需要在编写样式表中输入基于像素的值,而这些值在变量中尚未定义,那么这应该对你构成一个警示。这种情况也在上面有所涉及。在需要在编写样式表中输入魔术数字的情况下,请确保在上一行添加注释以解释其相关性。这可能在当时看起来多余,但请考虑其他人和自己在 3 个月后。为什么你要给那个元素添加一个负边距 17 像素?

例子:

.med-Video {
    position: relative;
    background-color: $color-black;
    font-size: $text13;
    line-height: $text15;
    /*We need some space above to accommodate the
    absolutely positioned icon*/
    margin-top: 20px;
}

对于任何设备/浏览器的 hack 也是一样。你可能有自己的语法,但我在 hack 代码的开始上方使用/*HHHack:*/前缀添加注释,当我不得不添加代码来满足特定情况时。考虑一下:

.med-Video {
    background-color: $color-black;
    font-size: $text13;
    line-height: $text15;
    /*HHHack needed to force Windows Phone 8.1 to render the full width, reference ticket SMP-234 */
    width: 100%;
}

这种覆盖应该尽可能放在规则的最下面。但是,请确保添加注释。否则,未来的作者可能会查看你的代码并认为这行(行)是多余的,然后将其删除。

提示

如果你发现你有很多代码,纯粹是为了服务特定的浏览器,你可以考虑将这些规则(手动或使用工具)提取到一个只在需要时提供的单独文件中。

9. 不要在编写样式表中放置内联图像

虽然我们继续支持基于 HTTP 的用户(而不是 HTTP2),但内联资源的做法提供了一些优势;主要是减少了为向用户提供页面所需的 HTTP 请求的数量。然而,不鼓励将内联资源放在编写样式表中。

考虑一下:

.rr-Outfit {
    min-height: $size-quadruple;
    background-image:
url(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAA3NCSVQICAjb4U/gAAAAw1BMVEX///8AAACEhIQAAABYWFgAAAAAAAAAAAB6enomJiYAAAAiIiJ4eHgAAABiYmJgYGAAAACJiYkAAAC0tLR/f3/IyMjHx8e/v7+vr6+fn5+ZmZmUlJSBgYFmZmYiIiIQEBAAAAD////w8PDv7+/i4uLh4eHf39/W1tbS0tLR0dHMzMzHx8fFxcXCwsK/v7+4uLi0tLSvr6+rq6ulpaWfn5+ZmZmQkJCPj498fHxwcHBgYGBAQEAwMDAgICAfHx8QEBAAAAAphWZmAAAAQXRSTlMAESIiMzNEVWZmZneIiKqqu8zM3d3u7u7u7u7u7u7u7u7//////////////////////////////////////////wP/q/8AAAAJcEhZcwAACxIAAAsSAdLdfvwAAAAVdEVYdENyZWF0aW9uIFRpbWUAMjEvNS8xMpVX8IQAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzVxteM2AAAAw0lEQVQYlU2P6VLCUAyFD1KvWFQ2F1ChnLLW2oIs1ZZC8/5PRW4rM+RH5uSbLCeARt2YO2Pq+I+aafUjRs+PplbVzfeQcbyZzoZuSZoBdyKSkQxdOx+SuQKZK/m8BZ7Iia3lT8m6BQy4/NodpYjpb9OiC3i/U+1NfE08iAdIamXACggwzq7BCGgfrIxt8pOsDbjpRu/k+Q/DBWf3auSVq/1Rz55074d16gT0i8pq4JTPOC9MRIqEbzeXfx860eS71yj1GWENHluGjvJwAAAAAElFTkSuQmCC);
}

未来的作者应该如何理解那个资产是什么?

提示

如果你在样式表中遇到现有的内联图像,可以复制并粘贴数据到浏览器地址栏中来确定图像是什么。

相反,让工具内联图像。这意味着编写样式表可以提供图像可能是什么的线索,但也使得该图像更容易被替换。如果使用 postcss-assets (github.com/assetsjs/postcss-assets) 插件,你可以使用内联命令内联图像。下面是之前的例子重写:

.rr-Outfit {
    min-height: $size-quadruple;
    background-image: inline("/path/to-image/relevant-image-name.jpg");
}

这不仅更容易阅读,还指定了现有资产的位置。这是一种更好的方法。

10. 当简单的 CSS 能够正常工作时,不要写复杂的 CSS

尽量编写尽可能简单的 CSS 代码,以便其他人在未来能够理解。循环、混合和函数很少需要编写。一般规则是,如果一个规则少于 10 个变体,就手动编写。另一方面,如果你需要为一个包含 30 个图像的精灵表创建背景位置,这就是工具应该使用的东西。

这种追求简单性应该延伸到布局的实现方式。如果一个更好支持的布局机制以与较少支持的机制相同的 DOM 节点数量实现了相同的目标,那就使用前者。然而,如果不同的布局机制减少了所需的 DOM 节点数量或提供了额外的好处,但只是不熟悉(例如 Flexbox),花时间了解它可能提供的好处。

总结

规则没有执行力就什么都不是。当许多人触及 CSS 代码库时,无论教育、强硬的话语还是文档都无法阻止你的代码库质量被稀释。提供胡萝卜只能走得那么远,通常也需要使用一点棍子

在这种情况下,棍子将采取静态分析linting工具的形式,这些工具可以在作者编写代码时检查和强制执行代码。这种方法可以防止不符合规范的代码进一步传播到有问题的开发者的本地机器之外。在下一章中,我们将讨论如何处理这个问题,以及工具的一般情况。

乐警来了!

第九章:ECSS 方法的工具

在这最后一章中,我们将看一些免费和开源的工具,以便编写合理和可维护的样式表。

在为持久项目编写 CSS 时,用于生成 CSS 的技术应该基本上是无关紧要的。我们应该始终意识到可能会有更好或更有效的工具可用来实现我们的目标,如果可能的话,应该加以采纳。

因此,无论是 Sass、PostCSS、LESS、Stylus、Myth 还是其他任何 CSS 处理器,都不应该成为编写样式表的障碍。如果需要的话,编写的样式表应尽可能容易迁移到另一种元语言。

此外,所采用的 CSS 处理器应该最好能满足整个项目的需求,而不仅仅是个别作者的偏好。也就是说,CSS 处理器应具备一些必要的功能,接下来我们将简要介绍这些功能。

CSS 处理器的 CSS 要求

我认为 CSS 处理器对于样式表编写是必不可少的。这允许区分编写的样式表(作者在其选择的 CSS 处理器中编写的样式表)和结果的 CSS(编译和压缩后提供给用户的 CSS)。

尽管声明 CSS 处理器是必不可少的,但所需的功能相当微不足道:

  • 变量:减少人为错误,如颜色选择和指定常量如网格尺寸

  • 部分文件:为了方便作者编写与特性分支、模板或逻辑文件相对应的样式表

  • 颜色操作:允许对上述变量进行一致的操作,例如能够调整颜色的 alpha 通道或轻松调整颜色

  • 所有其他能力被认为是非必要的,应根据项目的特定需求进行评估

从编写的样式表构建 CSS

需要某种构建系统将编写的样式表编译成纯 CSS。

提示

有许多工具可用于执行此任务,例如 Grunt、Gulp 和 Brocolli 等。然而,就像没有普遍正确的 CSS 处理器或 CSS 方法论一样,也没有普遍正确的构建工具。

除了将编写的样式表编译成 CSS 之外,良好的工具还可以提供进一步的好处。

  • Linting:启用代码一致性并防止非工作代码达到部署

  • Aggressive minification:重新定位 z-index,将长度值转换为更小的长度值,例如(虽然1pt等同于16px,但字符数少了一个),合并相似的选择器

  • Autoprefixer:启用快速准确的供应商前缀,并防止供应商前缀出现在编写的样式表中

提示

对于样式表编写中被认为是必不可少的语法方面的考虑,请参阅第八章,“合理样式表的十诫”。

保存编译,ECSS 样式表的旅程

就工具而言,在撰写本文时,我目前使用 Gulp 和 PostCSS 以及其众多插件来编写 ECSS。这是一个运作良好的过程,所以我会在这里简要记录一下。

注意

对于非常好奇的人,可以在这里找到更多关于我从 Sass 到 PostCSS 的经历benfrain.com/breaking-up-with-sass-postcss/)。

样式表作者将样式表写入一个部分 CSS 文件(带有*.css文件扩展名),使用的语法与 Sass 非常相似。

在保存作者样式表时,Gulp watch 任务会注意到文件的更改,并首先运行 linting 任务。然后,如果一切正常,它会将部分作者样式表编译为 CSS 文件,然后自动添加前缀,最后 BrowserSync 将更改的 CSS 直接注入到我正在工作的网页中。通常,在我可以 Alt + Tab 到浏览器窗口之前,或者甚至在我从文本编辑器移动到浏览器窗口之前,都会创建一个源映射文件,因为一些作者发现在开发者工具中使用源映射更容易进行调试。所有这些都发生在我可以 Alt + Tab 到浏览器窗口之前,甚至在我可以从文本编辑器移动到浏览器窗口之前。

这是一个演示如何在基于 Gulp 的构建工具中设置 PostCSS 的 gulpfile.js 示例:

//PostCSS related
var postcss = require("gulp-postcss");
var postcssImport = require("postcss-import");
var autoprefixer = require("autoprefixer");
var simpleVars = require("postcss-simple-vars");
var mixins = require("postcss-mixins");
var cssnano = require("cssnano");
var reporter = require("postcss-reporter");
var stylelint = require("stylelint");
var stylelinterConfig = require("./stylelintConfig.js");
var colorFunction = require("postcss-color-function");
var nested = require("postcss-nested");
var sourcemaps = require("gulp-sourcemaps");

// Create the styles
gulp.task("styles", ["lint-styles"], function () {

    var processors = [
        postcssImport({glob: true}),
        mixins,
        simpleVars,
        colorFunction(),
        nested,
        autoprefixer({ browsers: ["last 2 version", "safari 5", "opera 12.1", "ios 6", "android 2.3"] }),
        cssnano
    ];

    return gulp.src("preCSS/styles.css")

    // start Sourcemaps
    .pipe(sourcemaps.init())

    // We always want PostCSS to run
    .pipe(postcss(processors).on( class="st">"error", gutil.log))

    // Write a source map into the CSS at this point
    .pipe(sourcemaps.write())

    // Set the destination for the CSS file
    .pipe(gulp.dest("./build"))

    // If in DEV environment, notify user that styles have been compiled
    .pipe(notify("Yo Mofo, check dem styles!!!"))

    // If in DEV environment, reload the browser
    .pipe(reload({stream: true}));
});

对于 Gulp,构建选择是相当无限的,这只是一个示例。但是,请注意 styles 任务的第一步是运行 lint-styles 任务。

如前几章所述,样式表的 linting 是一个非常重要的步骤,特别是在涉及多个样式表作者的项目中。让我们接下来更深入地了解一下。

Stylelint

Stylelint 是一个基于 Node 的静态分析样式表的 linting 工具。通俗地说,它会分析你的样式表,找出你特别关心的问题,并警告你任何问题。

提示

如果你使用 Sass,你应该查看 scss-lintgithub.com/brigade/scss-lint),它为 Sass 文件提供了类似的功能。

如果发现任何作者错误,linting 任务会导致构建失败。通常情况下,在两个地方运行 linting 是最有益的。在文本编辑器(例如 Sublime)和构建工具(例如 Gulp)中。这样,如果作者有必要的文本编辑器,那么 基于编辑器的 linting (github.com/kungfusheep/SublimeLinter-contrib-stylelint) 就会在作者点击 保存 之前指出问题。

即使用户没有编辑器中的 linting 功能,保存时 linting 任务也会通过 Gulp 运行。构建步骤可以防止编译后的代码进入生产环境(因为持续集成软件也会导致构建失败)。

这是一个巨大的时间节省,对于代码的同行审查和质量保证测试来说是非常宝贵的。

这是一个 Stylelint 的 .stylelintrc 配置示例(这是针对 Stylelint 的 v5 版本的,所以未来/之前的版本可能会有些许不同):

{
    "rules": {
        "color-hex-case": "lower",
        "color-hex-length": "long",
        "color-named": "never",
        "color-no-invalid-hex": true,
        "font-family-name-quotes": "always-where-
        required",
        "font-weight-notation": "numeric",
        "function-comma-newline-before": "never-multi-
        line",
        "function-comma-newline-after": "never-multi-
        line",
        "function-comma-space-after": "always",
        "function-comma-space-before": "never",
        "function-linear-gradient-no-nonstandard-
        direction": true,
        "function-max-empty-lines": 0,
        "function-name-case": "lower",
        "function-parentheses-space-inside": "never",
        "function-url-data-uris": "never",
        "function-url-quotes": "always",
        "function-whitespace-after": "always",
        "number-leading-zero": "never",
        "number-no-trailing-zeros": true,
        "string-no-newline": true,
        "string-quotes": "double",
        "length-zero-no-unit": true,
        "unit-case": "lower",
        "unit-no-unknown": true,
        "value-keyword-case": "lower",
        "value-no-vendor-prefix": true,
        "value-list-comma-space-after": "always",
        "value-list-comma-space-before": "never",
        "shorthand-property-no-redundant-values": true,
        "property-case": "lower",
        "property-no-unknown": true,
        "property-no-vendor-prefix": true,
        "declaration-bang-space-before": "always",
        "declaration-bang-space-after": "never",
        "declaration-colon-space-after": "always",
        "declaration-colon-space-before": "never",
        "declaration-empty-line-before": "never",
        "declaration-block-no-duplicate-properties": true,
        "declaration-block-no-ignored-properties": true,
        "declaration-block-no-shorthand-property-
        overrides": true,
        "declaration-block-semicolon-newline-after":
        "always",
        "declaration-block-semicolon-newline-before":
        "never-multi-line",
        "declaration-block-single-line-max-declarations":
        1,
        "declaration-block-trailing-semicolon": "always",
        "block-closing-brace-empty-line-before": "never",
        "block-no-empty": true,
        "block-no-single-line": true,
        "block-opening-brace-newline-after": "always",
        "block-opening-brace-space-before": "always",
        "selector-attribute-brackets-space-inside":
        "never",
        "selector-attribute-operator-space-after":
        "never",
        "selector-attribute-operator-space-before":
        "never",
        "selector-attribute-quotes": "always",
        "selector-class-pattern": "^[a-z{1,3}-[A-Z][a-zA-Z0-9]+(_[A-Z][a-zA-Z0-9]+)?(-([a-z0-9-]+)?[a-z0-9])?$", { "resolveNestedSelectors": true }],
        "selector-combinator-space-after": "always",
        "selector-combinator-space-before": "always",
        "selector-max-compound-selectors": 3,
        "selector-max-specificity": "0,3,0",
        "selector-no-id": true,
        "selector-no-qualifying-type": true,
        "selector-no-type": true,
        "selector-no-universal": true,
        "selector-no-vendor-prefix": true,
        "selector-pseudo-class-case": "lower",
        "selector-pseudo-class-no-unknown": true,
        "selector-pseudo-class-parentheses-space-inside":
        "never",
        "selector-pseudo-element-case": "lower",
        "selector-pseudo-element-colon-notation":
        "single",
        "selector-pseudo-element-no-unknown": true,
        "selector-max-empty-lines": 0,
        "selector-list-comma-newline-after": "always",
        "selector-list-comma-newline-before": "never-
        multi-line",
        "selector-list-comma-space-before": "never",
        "rule-nested-empty-line-before": "never",
        "media-feature-colon-space-after": "always",
        "media-feature-colon-space-before": "never",
        "media-feature-name-case": "lower",
        "media-feature-name-no-vendor-prefix": true,
        "media-feature-no-missing-punctuation": true,
        "media-feature-parentheses-space-inside": "never",
        "media-feature-range-operator-space-after":
        "always",
        "media-feature-range-operator-space-before": "always",
        "at-rule-no-unknown": [true, {"ignoreAtRules": ["mixin"]}],
        "at-rule-no-vendor-prefix": true,
        "at-rule-semicolon-newline-after": "always",
        "at-rule-name-space-after": "always",
        "stylelint-disable-reason": "always-before",
        "comment-no-empty": true,
        "indentation": 4,
        "max-empty-lines": 1,
        "no-duplicate-selectors": true,
        "no-empty-source": true,
        "no-eol-whitespace": true,
        "no-extra-semicolons": true,
        "no-indistinguishable-colors": [true, {
            "threshold": 1,
            "whitelist": [ [ "#333333", "#303030" ] ]
        }],
        "no-invalid-double-slash-comments": true
    }
}

这只是一个示例,你可以从 不断扩展的列表 (stylelint.io/user-guide/rules/) 中设置你关心的任何规则。如果是第一次使用这类工具,你可能还会发现下载/克隆 ecss-postcss-shell (github.com/benfrain/ecss-postcss-shell) 也很有用。这是一个基本的 Gulp 设置,用于通过 PostCSS 运行作者样式表,并使用 Stylelint 对样式进行 linting。

注意

我甚至为 Stylelint 项目贡献了一点代码,帮助添加了一个名为 selector-max-specificity 的规则,用于控制任何选择器的最大特异性级别。如果你参与控制 CSS 代码库,这是一个很好的项目可以参与。

如果这还不够,Stylelint 是可扩展的。很容易添加额外的功能。对于我工作中的当前构建 ECSS 项目,我们有额外的 Stylelint 规则:

  • 确保只有覆盖和媒体查询可以嵌套(防止不使用父(&)选择器的嵌套)

  • 确保关键选择器与 ECSS 命名约定匹配(Stylelint 现在有一个 selector-class-pattern 规则来帮助解决这个问题)

  • 防止关键选择器成为复合选择器(例如 .ip-Selector.ip-Selector2 {}

  • 确保关键选择器是单数(例如 .ip-Thing 而不是 .a-Parent .ip-Thing {}

这些提供了定制的质量保证,如果手动执行将会耗费大量时间并容易出错。

如果我没有表达清楚,我想让你知道我喜欢 Stylelint,并认为 linting 是大型 CSS 项目中不可或缺的工具,有多个作者。我简直无法推荐它。

注意

关于 Stylelint 还有更多信息,请参阅这篇博客文章 (benfrain.com/floss-your-style-sheets-with-stylelint/) 或者官方的Stylelint (stylelint.io/) 网站。

优化

当 CSS 即将投入生产时,它需要通过cssnano (cssnano.co/) 进行额外的处理。这是一个由非常有才华的 Ben Briggs 开发的出色且模块化的 CSS 缩小器。强烈推荐。

除了 cssnano 提供的更明显的缩小步骤外,您还可以通过在 PostCSS 生态系统中使用插件执行一些微观优化。例如,通过一致地对 CSS 声明进行排序,Gzip 可以更有效地压缩样式表。这不是我想要手动完成的工作,但postcss-sorting (github.com/hudochenkov/postcss-sorting) 插件可以免费完成。以下是使用各种声明排序配置的 Gzip 文件大小的比较。

举例来说,我拿了一个大型的测试 CSS 文件,未排序时 Gzip 后大小为 37.59 kB。这是同一个文件在使用其他声明排序配置后 Gzip 后的文件大小:

  • postcss-sorting: 37.54

  • CSSComb: 37.46

  • Yandex: 37.48

  • Zen: 37.41

所以,我们最多只能节省原始大小的不到 1%。虽然是微小的节约,但你可以免费有效地获得它。

还有其他一些类似的优化,比如将类似的媒体查询分组,但我会留下这些微观优化供您探索,如果您对它们感兴趣的话。

总结

在本章中,我们已经介绍了工具,以促进不断的代码质量和改进的样式表编写体验。然而,您应该知道,我们所涵盖的所有内容中,这里列出的具体工具可能是最短命的。工具技术发展迅速。仅仅三年时间,我从普通的 CSS,转到了 Sass(带有scss-lintgithub.com/brigade/scss-lint)),再到了 PostCSS 和 Stylelint,同时也从 CodeKit 这样的 GUI 构建工具转到了 JavaScript 构建工具 Grunt,然后是 Gulp,现在是 NPM 脚本。

我不知道在 6 个月后最好的选择是什么,所以要记住的是要考虑工具和方法如何改进团队的样式表编写体验,而不是当前的工具是什么。

在你的个人关系中要忠诚,但在你选择的工具和技术上要多变
--务实编码之道 (benfrain.com/be-better-front-end-developer-way-of-pragmatic-coding/)

结束的右花括号

现在,朋友们,我们已经到达了这本小书的结尾。

虽然我希望你们中的一些人能够接受 ECSS 并开始全面实施它,但如果它只是激发了你自己的探索之旅,我同样会很高兴。

一开始,我试图找到一种处理以下问题的 CSS 扩展方法:

  • 允许随着时间的推移轻松维护大型的 CSS 代码库

  • 允许从代码库中删除 CSS 代码的部分,而不影响其余的样式

  • 应该能够快速迭代任何新设计

  • 更改应用于一个视觉元素的属性和值不应无意中影响其他元素

  • 任何解决方案都应该需要最少的工具和工作流程更改来实施

  • 在可能的情况下,应使用 W3C 标准,如 ARIA,来传达用户界面中的状态变化

ECSS 解决了所有这些问题:

  • 将 CSS 分隔成模块可以轻松删除已弃用的功能

  • 独特的命名约定避免了全局命名冲突,降低了特异性,并防止了对不相关元素的不必要更改

  • 由于所有新模块都是greenfield,因此可以轻松构建新设计

  • 尽管有一些工具可以适应全局导入和 linting,我们仍然在 CSS 文件中编写 CSS,这使得开发人员的入职过程变得更加容易

  • 我们也可以接受 ARIA 作为控制和传达状态变化的手段,不仅仅是为了辅助技术,而且在更广泛的意义上也是如此

考虑到 CSS 的扩展是一种有点小众的追求。在未来,我们将拥有诸如CSS Scoping (www.w3.org/TR/css-scoping-1/#scope-atrule)之类的东西,但在那之前,我们必须利用手头的工具和技术来弯曲现有技术以符合我们的意愿。

我已经多次提到过,有很多方法可以解决这个问题。其他方法可能更可取。以下是一些人和资源的列表,没有特定顺序,可能有助于你自己的探索。

亲爱的读者,直到下次,祝你探险愉快。

吸收有用的东西,拒绝无用的东西,添加特别属于你自己的东西。
--李小龙

资源

以下是一些经常谈论或写作关于 CSS 架构/扩展的人:

关于使用 JavaScript 内联样式的讨论:Shop Talk show #180 (shoptalkshow.com/episodes/180-panel-on-inline-styles/)

围绕 CSS 的有趣方法/项目:

附录 1. CSS 选择器性能

2014 年初,我与一些其他开发人员进行了一场辩论(我在那里用了引号),讨论了担心 CSS 选择器速度的相关性或无关性。

每当交换关于 CSS 选择器相对速度的理论/证据时,开发人员经常引用Steve Soudersstevesouders.com/)2009 年关于 CSS 选择器的工作。它被用来验证诸如属性选择器速度慢伪选择器速度慢等说法。

在过去的几年里,我觉得这些事情根本不值得担心。多年来我一直在重复的一句话是:

对于 CSS,架构在大括号外;性能在大括号内

但是,除了参考Nicole Sullivan 在 Performance Calendar 上的后续帖子calendar.perfplanet.com/2011/css-selector-performance-has-changed-for-the-better/)来支持我对所使用的选择器并不重要的信念外,我从未真正测试过这个理论。

为了解决这个问题,我尝试自己进行一些测试,以解决这个争论。至少,我相信这会促使更有知识/证据的人提供进一步的数据。

测试选择器速度

Steve Souders 之前的测试使用了 JavaScript 的new Date()。然而,现在,现代浏览器(iOS/Safari 在测试时是一个明显的例外)支持导航定时 APIwww.w3.org/TR/navigation-timing/),这为我们提供了更准确的测量。对于测试,我实现了这样的方法:

<script>
    ;(function TimeThisMother() {
        window.onload = function(){
            setTimeout(function(){
            var t = performance.timing;
                alert("Speed of selection is: " + (t.loadEventEnd - t.responseEnd) + " milliseconds");
            }, 0);
        };
    })();
</script>

这让我们可以将测试的时间限制在所有资产都已接收(responseEnd)和页面呈现(loadEventEnd)之间。

因此,我设置了一个非常简单的测试。20 个不同的页面,所有页面都有相同的巨大 DOM,由 1000 个相同的标记块组成:

<div class="tagDiv wrap1">
  <div class="tagDiv layer1" data-div="layer1">
    <div class="tagDiv layer2">
      <ul class="tagUl">
        <li class="tagLi"><b class="tagB"><a href="/" class="tagA link" data-select="link">Select</a></b></li>
      </ul>
    </div>
  </div>
</div>

测试了 20 种不同的 CSS 选择方法来将最内部的节点着色为红色。每个页面只在应用于选择块内最内部节点的规则上有所不同。以下是测试的不同选择器和该选择器的测试页面链接:

  1. 数据属性:benfrain.com/selector-test/01.html

  2. 数据属性(带修饰):benfrain.com/selector-test/02.html

  3. 数据属性(未经修饰但有值):benfrain.com/selector-test/03.html

  4. 数据属性(带值):benfrain.com/selector-test/04.html

  5. 多个数据属性(带值):benfrain.com/selector-test/05.html

  6. 单独伪选择器(例如:after):benfrain.com/selector-test/06.html

  7. 组合类(例如class1.class2):benfrain.com/selector-test/07.html

  8. 多个类:benfrain.com/selector-test/08.html

  9. 多个类与子选择器:benfrain.com/selector-test/09.html

  10. 部分属性匹配(例如[class<sup>ˆ=</sup>“wrap”]):benfrain.com/selector-test/10.html

  11. nth-child 选择器:benfrain.com/selector-test/11.html

  12. 紧接着另一个 nth-child 选择器的 nth-child 选择器:benfrain.com/selector-test/12.html

  13. 疯狂选择(所有选择都有资格,每个类都使用,例如div.wrapper``> div.tagDiv > div.tagDiv.layer2 > ul.tagUL > li.tagLi > b.tagB > a.TagA.link):benfrain.com/selector-test/13.html

  14. 轻微疯狂选择(例如.tagLi .tagB a.TagA.link):benfrain.com/selector-test/14.html

  15. 通用选择器:benfrain.com/selector-test/15.html

  16. 单一元素:benfrain.com/selector-test/16.html

  17. 元素双:benfrain.com/selector-test/17.html

  18. 元素三倍:benfrain.com/selector-test/18.html

  19. 元素三倍带伪:benfrain.com/selector-test/19.html

  20. 单一类:benfrain.com/selector-test/20.html

每个浏览器上的测试运行了 5 次,并且结果是在 5 个结果之间平均的。测试的浏览器:

  • Chrome 34.0.1838.2 dev

  • Firefox 29.0a2 Aurora

  • Opera 19.0.1326.63

  • Internet Explorer 9.0.8112.16421

  • Android 4.2(7 英寸平板电脑)

使用了 Internet Explorer 的以前版本(而不是我可以使用的最新 Internet Explorer)来揭示非常绿色浏览器的表现。所有其他测试过的浏览器都定期更新,所以我想确保现代定期更新的浏览器处理 CSS 选择器的方式与稍旧的浏览器有没有明显的差异。

注意

想要自己尝试相同的测试吗?去这个 GitHub 链接获取文件:github.com/benfrain/css-performance-tests。只需在您选择的浏览器中打开每个页面(记住浏览器必须支持网络定时 API 以警报响应)。还要注意,当我进行测试时,我丢弃了前几个结果,因为它们在某些浏览器中往往异常高。

提示

在考虑结果时,不要将一个浏览器与另一个浏览器进行比较。这不是测试的目的。目的纯粹是为了尝试和评估每个浏览器上使用的不同选择器的选择速度之间的比较差异。例如,选择器 3 是否比任何浏览器上的选择器 7 更快?因此,当查看表格时,最好看列而不是行。

以下是结果。所有时间以毫秒为单位:

测试 Chrome 34 Firefox 29 Opera 19 IE 19 Android 4
1 56.8 125.4 63.6 152.6 1455.2
2 55.4 128.4 61.4 141 1404.6
3 55 125.6 61.8 152.4 1363.4
4 54.8 129 63.2 147.4 1421.2
5 55.4 124.4 63.2 147.4 1411.2
6 60.6 138 58.4 162 1500.4
7 51.2 126.6 56.8 147.8 1453.8
8 48.8 127.4 56.2 150.2 1398.8
9 48.8 127.4 55.8 154.6 1348.4
10 52.2 129.4 58 172 1420.2
11 49 127.4 56.6 148.4 1352
12 50.6 127.2 58.4 146.2 1377.6
13 64.6 129.2 72.4 152.8 1461.2
14 50.2 129.8 54.8 154.6 1381.2
15 50 126.2 56.8 154.8 1351.6
16 49.2 127.6 56 149.2 1379.2
17 50.4 132.4 55 157.6 1386
18 49.2 128.8 58.6 154.2 1380.6
19 48.6 132.4 54.8 148.4 1349.6
20 50.4 128 55 149.8 1393.8
最大差异 16 13.6 17.6 31 152
最低 13 6 13 10 6

最快选择器和最慢选择器之间的差异

最大差异行显示了最快和最慢选择器之间的毫秒差异。在桌面浏览器中,IE9 以31毫秒的最大差异脱颖而出。其他浏览器的差异都在这个数字的一半左右。然而,有趣的是。

最慢的选择器

我注意到,最慢的选择器类型在不同的浏览器中有所不同。Opera 和 Chrome 都发现insanity选择器(测试 13)最难匹配(这里 Opera 和 Chrome 的相似性可能并不令人惊讶,因为它们共享blink引擎),而 Firefox 和 Android 4.2 设备(Tesco hudl 7 英寸平板电脑)都难以匹配单个伪选择器(测试 6),Internet Explorer 9 的软肋是部分属性选择器(测试 10)。

良好的 CSS 架构实践

我们可以肯定的是,使用基于类的选择器的扁平层次结构,就像 ECSS 一样,提供的选择器与其他选择器一样快。

这意味着什么?

对我来说,这证实了我的信念,即担心所使用的选择器类型是绝对愚蠢的。对选择器引擎进行猜测是毫无意义的,因为选择器引擎处理选择器的方式显然是不同的。而且,即使在像这样的庞大 DOM 上,最快和最慢的选择器之间的差异也不是很大。正如我们在英格兰北部所说,“有更重要的事情要做”。

自从记录了我的原始结果以来,WebKit 工程师本杰明·普兰联系我,指出了他对所使用方法的担忧。他的评论非常有趣,他提到的一些信息如下所述:

通过选择通过加载来衡量性能,你正在衡量比 CSS 大得多的东西,CSS 性能只是加载页面的一小部分。

如果以[class^="wrap"]的时间配置文件为例(在旧的 WebKit 上进行,以便与 Chrome 有些相似),我看到:

  • ~10%的时间用于光栅化。

  • ~21%的时间用于第一次布局。

  • ~48%的时间用于解析器和 DOM 树的创建

  • ~8%用于样式解析

  • ~5%用于收集样式-这是我们应该测试的内容,也是最耗时的内容。(剩下的时间分布在许多小函数中)。

通过上面的测试,我们可以说我们有一个基线为 100 毫秒的最快选择器。其中,5 毫秒将用于收集样式。如果第二个选择器慢 3 倍,总共将显示为 110 毫秒。测试应该报告 300%的差异,但实际上只显示了 10%。

在这一点上,我回答说,虽然我理解本杰明指出的问题,但我的测试只是为了说明,相同的页面,在其他所有条件相同的情况下,无论使用哪种选择器,渲染基本上都是相同的。本杰明花时间回复并提供了更多细节:

我完全同意提前优化选择器是没有用的,但原因完全不同:

仅仅通过检查选择器就几乎不可能预测给定选择器的最终性能影响。在引擎中,选择器被重新排序、拆分、收集和编译。要知道给定选择器的最终性能,你必须知道选择器被收集到了哪个桶中,它是如何编译的,最后 DOM 树是什么样子的。

各种引擎之间都非常不同,使整个过程变得更不可预测。

我反对网页开发人员优化选择器的第二个论点是,他们可能会让情况变得更糟。关于选择器的错误信息比正确的跨浏览器信息要多。有人做正确的事情的机会是相当低的。

在实践中,人们发现 CSS 的性能问题,并开始逐条删除规则,直到问题消失。我认为这是正确的做法,这样做很容易,并且会导致正确的结果。

因果关系

在这一点上,我感到 CSS 选择器的使用几乎是无关紧要的。然而,我想知道我们还能从测试中得出什么。

如果页面上的 DOM 元素数量减半,正如你所期望的,完成任何测试的速度也相应下降。但在现实世界中,减少 DOM 的大部分并不总是可能的。这让我想知道 CSS 中未使用的样式数量对结果的影响。

样式膨胀会产生什么影响?

另一个测试benfrain.com/selector-test/2-01.html):我拿了一张与 DOM 树完全无关的庞大样式表。大约有 3000 行 CSS。所有这些无关的样式都是在最后一个选择我们内部的a.link节点并将其变红的规则之前插入的。我对每个浏览器进行了 5 次运行的结果平均值。

然后删除了一半的规则并重复了测试benfrain.com/selector-test/2-02.html)以进行比较。以下是结果:

测试 Chrome 34 Firefox 29 Opera 19 IE 19 Android 4
完全膨胀 64.4 237.6 74.2 436.8 1714.6
一半膨胀 51.6 142.8 65.4 358.6 1412.4

规则减肥

这提供了一些有趣的数据。例如,Firefox 在完成这个测试时比其最慢的选择器测试(测试 6)慢了 1.7 倍。Android 4.3 比其最慢的选择器测试(测试 6)慢了 1.2 倍。Internet Explorer 比其最慢的选择器慢了 2.5 倍!

你可以看到,当删除了一半的样式(大约 1500 行)后,Firefox 的速度大大下降。Android 设备在那时也降到了其最慢选择器的速度。

删除未使用的样式

这种恐怖的场景对你来说是不是很熟悉?巨大的 CSS 文件包含各种选择器(通常包含甚至不起作用的选择器),大量更具体的选择器,七层或更深的选择器,不适用的供应商前缀,到处都是 ID 选择器,文件大小为 50-80 KB(有时更大)。

如果你正在处理一个有着庞大 CSS 文件的代码库,而且没有人确切知道所有这些样式实际上是用来做什么的,我的建议是在选择器之前查看 CSS 优化。希望到这一点你会相信 ECSS 方法在这方面可能会有所帮助。

但这并不一定会帮助 CSS 的实际性能。

括号内的性能

最终测试benfrain.com/selector-test/3-01.html)我进行的是对页面应用一堆昂贵的属性和值。考虑这条规则:


.link {
    background-color: red;
    border-radius: 5px;
    padding: 3px;
    box-shadow: 0 5px 5px #000;
    -webkit-transform: rotate(10deg);
    -moz-transform: rotate(10deg);
    -ms-transform: rotate(10deg);
    transform: rotate(10deg);
    display: block;
}

应用了这条规则后,以下是结果:

测试 Chrome 34 Firefox 29 Opera 19 IE 19 Android 4
昂贵的样式 65.2 151.4 65.2 259.2 1923

在这里,所有浏览器至少都达到了其最慢选择器的速度(IE 比其最慢的选择器测试(10)慢了 1.5 倍,Android 设备比最慢的选择器测试(测试 6)慢了 1.3 倍),但这还不是全部。试着滚动那个页面!这种样式的重绘可能会让浏览器崩溃(或者浏览器的等价物)。

我们放在大括号内的属性才是真正影响性能的。可以想象,滚动一个需要无休止昂贵的重绘和布局更改的页面会给设备带来压力。高分辨率屏幕?这会更糟,因为 CPU/GPU 会努力在 16 毫秒内将所有内容重新绘制到屏幕上。

在昂贵的样式测试中,在我测试的 15 英寸 Retina MacBook Pro 上,Chrome 连续绘制模式中显示的绘制时间从未低于 280 毫秒(请记住,我们的目标是低于 16 毫秒)。为了让你有所了解,第一个选择器测试页面从未超过 2.5 毫秒。这不是打错字。这些属性导致绘制时间增加了 112 倍。天啊,这些属性真是昂贵啊!确实是罗宾。确实是。

什么属性是昂贵的?

昂贵的属性/值配对是我们可以相当确信会使浏览器在重新绘制屏幕时感到吃力的(例如在滚动时)。

我们如何知道什么样式是昂贵的?幸运的是,我们可以运用常识来得出一个相当好的想法,知道什么会让浏览器负担。任何需要浏览器在绘制到页面之前进行操作/计算的东西都会更加昂贵。例如,盒阴影,边框半径,透明度(因为浏览器必须计算下面显示的内容),变换和性能杀手,如 CSS 滤镜-如果性能是你的优先考虑因素,那么任何类似的东西都是你的大敌。

注意

Juriy kangax Zaytsev 在 2012 年做了一篇非常棒的博客文章,也涵盖了 CSS 性能perfectionkills.com/profiling-css-for-fun-and-profit-optimization-notes/)。他使用各种开发者工具来衡量性能。他特别出色地展示了各种属性对性能的影响。如果你对这种事情感兴趣,那么这篇文章绝对值得一读。

总结

从这些测试中得出的一些要点:

  • 在现代浏览器中纠结于所使用的选择器是徒劳的;大多数选择方法现在都非常快,真的不值得花太多时间在上面。此外,不同浏览器对最慢的选择器也存在差异。最后查看这里以加快你的 CSS 速度。

  • 过多的未使用样式可能会在性能上造成更多的损失,而不是你选择的任何选择器,所以第二要整理那里。在页面上有 3000 行未使用或多余的样式并不罕见。虽然将所有样式都捆绑到一个大的styles.css中很常见,但如果站点/网络应用的不同区域可以添加不同的(额外的)样式表(依赖图样式),那可能是更好的选择。

  • 如果你的 CSS 随着时间被多位不同的作者添加,可以使用UnCSS等工具(github.com/giakki/uncss)来自动删除样式;手动进行这个过程并不有趣!

  • 高性能 CSS 的竞争不在于所使用的选择器,而在于对属性和值的慎重使用。

  • 快速将某物绘制到屏幕上显然很重要,但用户与页面交互时页面的感觉也很重要。首先寻找昂贵的属性和值对(Chrome 连续重绘模式在这里是你的朋友),它们可能会带来最大的收益。

附录 2. 浏览器代表对 CSS 性能的看法

作为附录 1 的补充,CSS 选择器性能,以下文字涉及浏览器代表对 CSS 性能的看法。

TL;DR

如果你不想读这一节的其他内容,那么请读下一段并牢记:

在没有检查自己的数据之前,不要记忆与 CSS 性能相关的规则。它们基本上是无用的、短暂的和太主观的。相反,熟悉工具并使用它们来揭示自己场景的相关数据。这基本上是 Chrome 开发者关系人员多年来一直在推广的口号,我相信是 Paul Lewis(下文还有更多)创造了与 Web 性能故障排除相关的术语工具,而不是规则

现在我理解那种情绪。真的理解了。

浏览器代表对 CSS 性能的看法

通常情况下,我在编写样式表时不太担心 CSS 选择器(通常我只是在我想要设置样式的任何东西上放一个类并直接选择它),但偶尔我会看到一些比我聪明得多的人对特定的选择器发表评论。以下是Paul Irishwww.paulirish.com/)在与Heydon Pickeringalistapart.com/article/quantity-queries-for-css)的一篇文章相关的评论,该文章使用了一种特定类型的选择器:

这些选择器是可能的最慢的。比像 div.box:not(:empty):last-of-type .title”这样的东西慢大约 500 倍。测试页面 http://jsbin.com/gozula/1/quiet。也就是说,选择器速度很少是一个问题,但如果这个选择器最终出现在一个 DOM 变化非常频繁的动态 Web 应用程序中,它可能会产生很大的影响。因此,对于许多用例来说是不错的,但请记住,随着应用程序的成熟,它可能成为性能瓶颈。这是一个需要在那时进行分析的事情。干杯

我们应该从中得出什么?我们是否应该在头脑中将这种选择器放在某种紧急情况下不要使用的保险库中?

为了得到一些真正的答案,我询问了实际在浏览器上工作的聪明人,问他们对 CSS 性能应该关注什么。

在前端世界中,我们很幸运,因为 Chrome 开发者关系团队是如此可及。然而,我喜欢平衡。此外,我还联系了微软和火狐的人,并包括了 WebKit 的一些很好的意见。

我们应该担心 CSS 选择器吗?

问题本质上是,作者是否应该关注与 CSS 性能相关的选择器?

让我们从开始的地方开始,那里有 CSSOM 和 DOM 实际上被构建。Chrome 开发者关系的开发者倡导者Paul Lewisaerotwist.com/)解释说,样式计算受两个因素影响:选择器匹配和无效大小。当你首次加载页面时,所有元素的所有样式都需要计算,这取决于树的大小和选择器的数量。

更详细的内容,Lewis 引用了 Opera 团队的Rune Lillesveendocs.google.com/document/d/1vEW86DaeVs4uQzNFI5R-_xS9TcS1Cs_EUsHRSgCHGu8/edit#)的话:

在撰写本文时,大约 50%的时间用于计算元素的计算样式,用于匹配选择器,另一半时间用于从匹配规则构造 RenderStyle(计算样式表示)

好吧,这对我来说有点科学,那是否意味着我们需要担心选择器呢?

Lewis 再次说道,选择器匹配可能会影响性能,但树的大小往往是最重要的因素

这是理所当然的,如果你有一个庞大的 DOM 树和一大堆无关的样式,事情就会开始变得困难。我的自己的膨胀测试benfrain.com/selector-test/2-01.html)支持这一点。再考虑另一种情况。如果我给你两堆各有 1000 张卡片,除了 5 张匹配的卡片外,每堆上的卡片名字都不同,那么很显然要花更长的时间来配对这些匹配的名字,而不是只有 100 张或 10 张卡片。对于浏览器也是同样的道理。

我认为我们都可以同意,样式膨胀比使用的 CSS 选择器更令人担忧。也许这是我们可以信赖的一个规则?

对于大多数网站,我认为选择器性能不是值得花时间寻找性能优化的最佳领域。我强烈建议专注于括号内的内容,而不是括号外的选择器
--Greg Whitworth, 微软的项目经理

那么 JavaScript 呢

然而,Whitworth 也指出,在处理 JavaScript 和 DOM 结构的动态性时需要额外的注意,如果你一遍又一遍地使用 JavaScript 在事件上添加或替换类,你应该考虑这将如何影响整体的网络管道和你正在操作的盒子的 DOM 结构

这与Paul Irishwww.paulirish.com/)早期的评论相吻合。由于类的更改而导致 DOM 区域的快速失效有时可能会显示出复杂的选择器。那么,也许我们应该担心选择器?

每个规则都有例外,有些选择器比其他选择器更有效,但我们通常只在有大量 DOM 树和 JavaScript 反模式导致 DOM 抖动和额外布局或绘制发生的情况下才会看到这些选择器
--Whitworth

对于更简单的 JavaScript 更改,Lewis 提供了这样的建议,解决方案通常是尽可能地紧密地定位元素,尽管 Blink 越来越聪明,可以确定哪些元素真正会受到对父元素的更改的影响。因此,实际上,如果可能的话,如果你需要影响 DOM 元素的更改,最好是在 DOM 树中直接在它上面添加一个类,而不是在 body 或 html 节点上。

处理 CSS 性能

在这一点上,我很高兴地得出了在附录 1 中得出的结论,CSS 选择器性能 - CSS 选择器在静态页面中很少会出现问题。此外,试图预测哪个选择器会表现良好可能是徒劳的。

然而,对于大型 DOM 和动态 DOM(例如不仅仅是偶尔的类切换,我们说的是大量的 JavaScript 操作),CSS 选择器可能会成为一个问题并不是不可能的。我不能代表所有的 Mozilla,但我认为当你处理性能问题时,你需要关注什么是慢的。有时候会是选择器;通常会是其他事情,来自Mozillawww.mozilla.org/en-US/)和 W3C 的 CSS 工作组成员L. David Barondbaron.org/)说道。我确实看到过选择器性能很重要的页面,也确实看到过很多页面选择器性能并不重要

那么我们应该怎么做?什么是最实用的方法?

你应该使用性能分析工具来确定你的性能问题在哪里,然后努力解决这些问题
--Baron

我和所有人交谈的时候都表达了这些观点。

总结

如果您在网络上开发了一段时间,就会知道大多数与网络相关的问题的答案是“这取决于情况”。我讨厌在 CSS 性能方面没有简单的、铁一般的规则可以在任何情况下依赖。我真的很想在这里写出那些规则,并相信它们是普遍适用的。但我不能,因为在性能方面根本没有普遍适用的“铁一般”的真理。永远不可能有,因为变量太多了。引擎更新,布局方法变得更加优化,每个 DOM 树都不同,所有的 CSS 文件也都不同。如此循环往复。你明白了吧。

我害怕我能提供的最好建议就是不要提前担心 CSS 选择器或布局方法。它们不太可能是你的问题(但是,你知道,它们可能会是)。相反,集中精力去做“那件事”。然后,当“那件事”做好了,测试“那件事”。如果它慢或者出了问题,找到问题并修复“那件事”。

额外信息

posted @ 2024-05-24 11:13  绝不原创的飞龙  阅读(9)  评论(0编辑  收藏  举报