最近学习到CSS的继承属性,正好看到这篇文章,便将它翻译出来。作者的思想,在平时的项目中或多或少都有用过,但是从来没有仔细去思考如何利用这些特性让代码更加优雅。
我热爱模块化设计。长期以来,我将网站分为组件,而不是页面,并动态地将这些组件合并成界面,这样可以提高弹性、效率与可维护性。
但是我不想让我设计的东西像是互相无关的事情,我是制作一个页面,而不是超现实主义蒙太奇。
幸运的是,有一门叫做CSS的技术,就是专门解决上述问题的。CSS可以通过赋予HTML组件样式以最小的成本确保设计的一致性。这多亏了CSS的两个主要的特征:
继承inheritance
层叠the cascade(CSS中的C)
尽管这些特征可以保证网页文档的样式代码高效、DRY,并且这也成为了CSS存在的理由,但是它们明显不受欢迎。封装的一些CSS模块,比如BEM、Atomic CSS,都是在尽他们最大的努力回避或者抑制这些特征,这让开发者能够更多地控制这些CSS,但这仅仅只是一项频繁的专项干预机制。
我准备带着一颗对模块化界面设计尊重的心来重新审视继承、层叠和作用域,目的在于向读者展示如何平衡这些特征来使我们的CSS代码变得更加的简洁和灵活,并且界面也更加易于扩展。
Inheritance And font-family
尽管很多人提议为什么CSS不只提供一个全局作用域,但是如果有,很多东西就会变得重复。相反的,CSS提供了全局作用域和局部作用域。就像在JavaScript里,局部作用域可以访问父级和全局作用域,CSS里的局部作用域帮助了继承。
比方说,如果在根元素(全局)html中声明了font-family,那就能确保这一规则会被应用于该文档中所有的祖先元素中(在下一节中将会讨论一些例外)。
就像在JavaScript里面,如果我声明一个变量为局域作用域,那它在全局或者说任意祖先作用域中都是无效的,但它能够对其子域起作用(比如上面代码中的p元素)。在下一个例子中,line-height为1.5并不能对html元素生效,然而在p元素里的a元素继承了p的line-heigh值。
继承的最棒之处是你只需要非常少的代码就能建立一致的可视化设计基础,这些样式甚至会影响到你还没有写出来的HTML。这些是不会过时的代码。
替代方法
当然,有很多的方法可以定义普通的样式。比如,我可以创建一个类.sans-serif
将它应用于任何我认为会用上这个样式的元素中:
这样我可以准确地选择哪些元素需要这个样式,而哪些元素不需要。
任何可以控制的机会都是诱人的,但是也存在很明显的问题。在这种情况下,我们不仅仅需要手动给需要的元素添加类名(这意味着必须一开始就知道这些类名),而且意味着我们已经放弃了支持动态内容的可能性:WYSIWYG编辑器和Markdown解析器都不能给默认的p元素提供sans-serif类。
除了class="sans-serif"需要同时在样式表和HTML里面添加代码外,它和style="font-family: sans-serif"的区别并不大。如果使用继承的话,那我们可以写更少的代码甚至不写其他代码。相比于给每一个字体样式写类名,我们只需要在html元素里声明任何我们想要的样式:
The inherit Keyword
有些属性的默认值不是inherit,有些元素也没有inherit一些属性。在一些情况下,我们可以利用[property name]: inherit来决定继承性。
比如,input和textarea元素不会继承字体的任何属性。为了保证所有的元素能够从全局作用域中继承这些属性,我们可以用全局选择器和关键字inherit。这样,就能最大程度地利用继承。
注意,我没有设置font-size的值。我不想让font-size具有直接的继承性,这样它可能会覆盖一些元素的默认user-agent样式,比如说头部元素、small元素等。如此一来,我可以节约一行代码,并且让user-agent决定这些样式。
另外一个属性我不想用继承的是font-style:我不想重新定义em的斜体,这会浪费工作时间,并且导致更多的代码量。
现在,如我所想实现了所有可以继承或者是强制继承的字体样式。现在只花了两个区块就能声明一致性和作用域。从现在起,开发者在构造组件时,甚至不用再考虑font-family、line-height或者是color,除非碰到一些特例,这时就需要用到层叠。
Exceptions-Based Styling
我可以利用继承实现主要的标题拥有相同的font-family、color和line-height的目的。但是我想让font-size的值不一样时,可能什么都不需要设置,因为user-agent已经为h1元素提高了更大号的font-size(它是我们设置的基础字体的125%倍)。
但是如果我想调整元素的font-size呢?这时候就可以利用全局作用域的优势,在局部作用域里只对我需要调整的元素进行调整。
如果默认封装了CSS元素的样式,那这种情况就不可能实现:给h1添加所有的字体样式。相反地,我可以将我的样式分成独立的类,用空格分离添加到每一个h1中。
无论采用哪种方法,都会增加工作量,并且只能产生一个具有固定样式的h1。如果利用层叠,就可以用我想用的方式去定义最多的元素的样式,其实h1就仅仅是一个特例。层叠就像一个过滤器,意味着只有在添加新的样式时旧的样式才会被覆盖。
Element Styles
我们已经有了一个好的开始,但是真正要掌握层叠,我们需要尽可能多地定义公共元素的样式。为什么呢?因为我们混合的模块是由独立的HTML元素组成的,而屏幕阅读器友好的界面应该充分利用语义化的结构进行标记。
组成界面的“分子”的"atoms"样式(<a href="http://bradfrost.com/blog/post/atomic-web-design/#molecules">atomic design terminology</a>)可以通过元素选择器定位。元素选择器的<a href="https://www.smashingmagazine.com/2007/07/css-specificity-things-you-should-know/">优先级</a>很低,因此他们不会覆盖任何基于类选择器的样式。
我们需要做的第一件事是定义所有我们需要用到的元素的样式。
如果想较简洁地实现统一性的界面,接下来的步骤很重要:每一次需要创建一个新的组件时,如果引入了新的元素,那么利用元素选择器定义这些新元素样式。现在还不需要使用限制性、高优先级的选择器,也不需要添加类名来定义样式,就使用语义化元素其本身。
举个例子,如果我还没有定义button的样式(正如前面的例子),而我新的组件中包含了一个button元素,这时我就需要为整个界面的button元素定义样式。
现在,如果你需要写一个新的组件但是拥有相同的button,就少了一件需要担心的事情了。你不需在不同的命名空间重写相同的CSS,也不需要记住或者编写类名。CSS设计的目的就是为了让事情变得更简单和高效。
使用元素选择器有以下优点:
避免了HTML的冗长(没有冗余的类)。
避免了样式表的冗长(不同的组件可以共用样式,不需要对每个组件进行重写)。
最终的界面拥有语义化的HTML结构。
利用类来提供唯一的样式经常被定义为“关注点分离”,这是对W3C中<a href="https://www.w3.org/TR/html-design-principles/#separation-of-concerns">关注点分离</a>原则的误解,它的目标是利用HTML和CSS样式来描述整个结构。类是专门为了指定样式而制定的,并且出现在结构中,因此无论他们出现在哪里,在技术上都是在打破分离。你不得不通过改变自然结构来获取样式。
如果不依赖表面的结构标记(类、内联样式),你的CSS就会兼容通用结构,并且符合语法规范。这使得扩展内容与功能变得不再重要,并且不需要把它变成一个样式任务。它也使得你的CSS能够被拥有传统语义化结构的不同的工程重复利用(CSS的‘方法论’可能要另当别论)。
特殊案例
在有人提出我思考得太简单前,我意识到界面上不是所有的按钮都在做相同的事情,我也意识到功能不同的按钮可能看起来样式也会不一样。
这并不代表我需要定义类、继承或者是层叠。让一个界面的按钮看起来完全不一样会混淆你的用户。为了保证可访问性和一致性,很多的按钮只需要在外表的标签上进行区分。
记住,样式不是视觉上进行区分的唯一方法。内容也可以提供视觉区分,而且这种方法会更加直观,你可以从文字上直接明白他们的区别。
必须或者适当地单独使用样式对内容进行区分的场景可能比你想象的还要少。通常,差异化的样式只是补充条件,比如一个红色的背景或者带图标的文本标签。文本标签的存在对那些使用语音激活的软件有特殊作用:说“红色按钮”或者“button with cross icon”不可能让软件识别。
我将在“Utility Classes”一节中,介绍如何给看起来相似的元素添加细节。
Attributes
语义化的HTML不仅仅指结构,标签特征能定义类型、样式属性和状态。这些对可访问性都非常重要,因此需要写在HTML中需要的地方。正因为他们出现在HTML中,他们为制作样式钩子提供了额外的机会。
比如,input元素拥有type属性,你应该利用它的优点,另外比如说aria-invalid是用来描述状态的。
有几点需要注意:
由于使用了inherit关键字,color、font-family、line-height继承了html的值,因此不需要设置。如果想改变整个应用范围内的font-family,只需要在html块中进行一次声明。
边框颜色与color相关联,因此它也继承了全局域的颜色,我需要设置的只有边框的宽度和风格。
属性选择器[aria-invalid]是无限制的。这意味着它可以被更好地应用(可以同时应用在input和textarea选择器上),且低优先级。简单的属性选择器和类选择器有相同的优先级。无限制的使用它们意味着任意写在更深层叠处的类可以覆盖它们。
BEM方法可以利用修饰类来解决这个方法,比如input-invalid。但考虑到无效状态只能应用在可通信时,因此input-invalid一定是多余的。换句话说,aria-invalid属性必须出现,那类的作用是什么呢?
只写HTML
在层叠多,大量的元素和属性选择器绝对是我的最爱:新组件的构造就变成了,了解HTML结构比了解公司或者组织的命名规范重要得多。分配到这个项目中的开发者将会从已经存在的继承样式中获益,这减少了对参考文档的需求或者写新的CSS的必要性。Tim Baxter在<a href="https://alistapart.com/article/meaningful-css-style-like-you-mean-it">Meaningful CSS: Style It Like You Mean It</a>中提到了案例。
Layout
到目前为止,我们都没有写额外的特定组件的CSS,这不代表我们没有给它们任何样式。所有的组件都是由HTML元素的,更复杂的组件都是依靠这些元素的排列顺序进行组合的。
这就引出了布局的概念。
我们主要需要处理流式布局——连续的块级元素的间距。你可能注意到至今我仍没有为元素设置任何的margin。那是因为margin不应该考虑为元素的属性,而应该是元素上下文的属性。也就是说,只有元素相遇时他们才起作用。
幸运的是,<a href="https://developer.mozilla.org/en/docs/Web/CSS/Adjacent_sibling_selectors">相邻选择符</a>可以准确地描述这种关系。除了少数例外,利用层叠可以对所有出现在子级中的块级元素设置统一的默认值。
极低优先级<a href="https://alistapart.com/article/axiomatic-css-and-lobotomized-owls">lobotomized owl选择器</a>确保了所有的元素(除了常见的例外)的间距为一行。这意味着在所有案例中默认存在白色的间距,所有开发流式布局的开发者都有一个合理的起点。
在多数情况下,margins只关心它本身。但由于优先级较低,也很容易在需要的地方覆盖它的一行间距。比如,为了表示标签和它们相关元素的关联性,我可能需要缩小它们之间的距离。在下面的例子中,标签和它们后面的所有元素(input、textarea、select等等)之间的间隔将会缩小。
使用层叠意味着只需要在需要的地方写特定的样式,其他的符合一个合理的基准。
需要注意的是,因为margins只出现在元素之间,它们不会和其容器的padding重叠。
不管你是否引入了容器元素,都会产生相同的边距。这意味着,你可以像下面这样实现相同的布局——这些margins在divs之间出现比在标签之间出现要好。
利用像<a href="https://acss.io/">atomic CSS</a>的方法实现相同的结果,可以手动组合具体的margin-related类,包括通过* + *隐式处理控制first-child的特例。
请牢记如果一个元素使用atomic CSS,那只会覆盖它的上边距。你需要为color、background-color,或者说其他的主要属性单独添加特定的类,因为atomic CSS不会利用继承或者元素选择器。
Atomic CSS让开发者能够完全控制样式,而不需要内联样式(内联样式不像类一样可以重复利用)。为单独的属性提供类名可以在样式表中减少声明的复制。
然而,它必须直接在结构中添加标记来实现。这就要求开发者学习它冗长的API,并且增加很多额外的HTML代码。
相反,如果对任意HTML元素和它们的空间关系设置样式,那CSS方法就会变得过时。相比于包含一个叠加样式的HTML系统,使用一致性设计系统进行维护将拥有很大的优势。
无论如何,下面是我们的CSS架构和流式布局解决方案需具备的特点:
全局(html)样式和强制继承;
流式布局和一些例外(lobotomized owl选择器);
元素和属性样式。
我们还没有写具体的组件和CSS类,但是我们已经完成很绝大多数的样式——前提是假设我们能够合理地写出可复用的类。
Utility Classes
类有一个全局作用域:不管它们应用在HTML的哪个位置,它们都会受到相关的CSS的影响。这很多情况下会被看成缺点,因为两个分开工作的开发者可能会写出相同名称的类,从而影响到对方的工作。
最近构思的<a href="https://css-tricks.com/css-modules-part-1-need/">CSS modules</a>通过编程产生唯一的类名绑定到他们本地或者组件内,以弥补上述不足。
忽略生成代码的丑陋肤浅,你可能很容易看到独立编写的组件直接的差别:唯一的标识用来定义相似的东西的样式。这样导致界面要么不一致,要么在更大的努力和更多的冗余下变得一致。
把公共元素视为独一无二的是毫无道理的。你应该定义某种类型的元素的样式,而不是具体的元素实例。牢记“类”意味着“可能包含更多内容的某一类型的事物”。换句话说,所有的类应该是实用工具类:可在全局重复使用。
当然,在这个例子中,类名.button是多余的:因为我们可是使用button元素选择器。但如果是一个特殊类型的button呢?例如,我们可能写一个类名.danger表示这类buttons可以进行代表危险性的操作,比如删除数据:
因为类选择器比元素选择器的优先级高,和属性选择器拥有一样的优先级,任何写在类选择器里的规则都会覆盖元素选择器和属性选择器的。所有上面的危险button将会以白色文本红色背景的样式出现,但是其他的属性——比如padding、聚焦时的outline和margin都会保持不变。
如果数个开发者长时间在相同的代码上进行开发,偶尔会发生命名冲突。但是这里有很多方法去避免,比如,哦,我不知道,首先要做的事是对你将要采用的命名进行搜索看是否存在。你永远不知道,有人可能已经解决了你要解决的问题。
局部作用域
对于utility classes,我最喜欢的事是将它们设置在容器上,然后利用钩子去影响里面的子元素的布局。比如,我可以快速对任何元素编码出一个等间隔、响应式、居中的布局:
这样,我可以让列表、按钮、按钮和链接的组合居中。这多亏了使用> *,它表示.centered里面任何的直接子元素都适用这些样式,在这个范围内继承全局和父级元素的样式。
我已经调整了元素的margins,这样这些元素就可以自由地换行,而不会破坏* + *选择器里设置的垂直布局。这样,通过设置任意元素的局部作用域,就可以采用了少量的代码就提供了一个通用的、响应式的布局解决方案。
我的小型<a href="https://github.com/Heydon/fukol-grids">flexbox-based grid system</a>(压缩后93B)基本上就只是一个utility class。它高度可复用,并且因为利用了flex-basis,不需要断点进行干预。我只用了flexbox布局的方法。
使用BEM,可以在每个网格上设置一个显式的“元素”类:
但这不是必须的。只需要一个标识去实例化局部作用域。相比于我的版本,这些主题不会受到外部影响的保护,也不应该受> *影响。唯一的不同是充斥着大量的标记。
现在,我们已经开始合并类,但是如预期一样只合并通用的样式。我们仍旧没有单独为复杂组件定义样式。相反,我们以可复用的方式解决系统层面的问题。当然,你需要在注释中说明如何使用这些类。
这些Utility classes同时利用了CSS的全局作用域、局部作用域、继承和层叠的优势。这些类可以被应用在各个地方,它们实例化局部作用域以影响子元素,继承了父级元素或者全局作用域的样式,同时不需要过度使用元素选择器或者类选择器。
现在我们的层叠是这样的:
全局(html)样式和强制继承;
流式布局和一些例外(lobotomized owl选择器);
元素和属性的样式;
通用utility classes。
当然,可能会有不需要写上面案例中那样的utility classes的情况。但重点是,如果需要对一个组件使用,那解决方案应该针对所有的组件,应该站在系统的角度思考问题。
具体的组件样式
我们已经定义了组件的样式,组合组件的方式,所以很容易忽略这一部分的内容。但是值得一提的是,任何没有从其他组件中创建出来的组件(包括单个的HTML元素)都是有必要存在的。它们是使用ID选择符的组件,并且可能产生系统风险。
事实上,一个好的经验是只通过ID来标识复杂的组件(“molecules,” “organisms”),并且不要在CSS中使用这些ID。比如,你可以在你的登录表单组件中标识#login,不需要在CSS中有元素、属性或者流式布局样式的地方使用#login,即使你可能发现你构造了一个或两个通用的utility classes可以用到其他的组件上。
如果你使用了#login,那这只能影响到特定的组件。这提醒了你已经偏离了开发一个设计系统,而朝着大量冗长代码的工作前进。
Conclusion
当我告诉大家我不采用BEM这样的方法或者CSS modules这样的工具,许多人会认为我是这样写CSS的:
然而并不是的。这里已经阐述了一个明确的过度规范,我们需要小心去避免。就像BEM(OOCSS, SMACSS, atomic CSS等)并不是避免复杂、难以控制的CSS的唯一方法。
为了解决优先级问题,很多的方法都是引入了类选择器。这样带来的麻烦是导致一串冗余的类:让结构变得更加臃肿的奇异的代码——不会注意到文档——会使进入到这个系统工程中的新的开发者感到困惑。
通过大量地使用类,你可以让你的样式文件最大化地和HTML系统分离。这不适合“关注点分离”,会导致冗长或者更糟的代码,导致不可访问性:它可能在不影响可访问性状态的情况下影响视觉样式:
除了大量的编写和使用类之外,我看到了下面这些方法:
利用继承建立一致性的前置条件;
用最多的元素和属性选择器来支持透明的、标准的基础组件;
利用简单的流式布局系统;
结合一些通用的utility classes来解决被多元素影响的布局问题。
所有这些方法对于创造可设计系统,能够更加容易地写出新的界面组件,而让成熟的项目可以更少地依赖新的CSS代码。这并不是得益于严格的命名和封装,反而是因为缺少它们。
即使你不是很习惯我这里推荐的这些方法,我也希望这篇文章至少能够帮助你重新思考什么是组件。它们不是你隔离创造出来的。有时候,在标准的HTML元素下,它们也不是你创造的东西。通过组件组合越多的组件,你的界面就能够以更少的CSS来实现高可访问性,并拥有一致的视觉效果。
CSS没有任何错。事实上,它明显地让你以更少的代码做更多是事情。我们只是没有充分利用它的优点。
作者:Heydon Pickering
原文链接:<a href="https://www.smashingmagazine.com/2016/11/css-inheritance-cascade-global-scope-new-old-worst-best-friends/">https://www.smashingmagazine.com/2016/11/css-inheritance-cascade-global-scope-new-old-worst-best-friends/</a>