我酷愛模組化設計。長期以來我都熱衷於將網站分離成元件,而不是頁面,並且動態地將那些元件合併到介面上。這種做法靈活,高效且易於維護。
但是我不想我的設計看起來是由一些不相關的東西組成的。我是在創造一個介面,而不是一張超現實主義的照片。
很幸運的是,已經有一項叫做 CSS 的技術,就是刻意設計用來解決這個問題的。使用 CSS,我就可以在 HTML 元件之間到處傳遞樣式,從而以最小的代價來保證一致性的設計。這很大程度上要感謝兩個CSS 特性:
繼承
層疊(CSS 當中的C,cascade)
儘管這些特性讓我們能夠以一種 DRY 且有效率的方式來為Web 文件添加樣式,同時也是CSS 存在的原因,但很明顯,它們已經不再受到青睞。在一些 CSS 方法論裡,如 BEM 和 Atomic CSS 這些透過程式化封裝 CSS 模組的方法,許多都盡力去規避或抑制這些特性。這也讓開發者有了更多機會控制他們的 CSS,但這只是基於頻繁幹預的專案控制。
我準備帶著對模組化介面設計的尊敬在此重新審視繼承、層疊和作用域。我想要的告訴你的是如何利用這些特性讓你的 CSS 程式碼更簡潔,實現更好的自適應,並且提高頁面的可擴展性。
font-family
儘管許多人在抱怨CSS 為什麼不單單提供一個全局作用域,但如果它這麼做的話,那麼就會有很多重複樣式了。反之,CSS 有全域作用域和局部作用域。就像在 JavaScript 裡,局部作用域有權限存取父級和全域作用域,而在 CSS 裡,局部作用域則幫助了繼承。
例如,如果給根部(也作:全域)的
html
元素定義一個
font-family
屬性,那麼可以確定這個規則會在文件裡應用到所有祖先元素(有一些例外情況,將在下個部分討論)。
html {
font-family: sans-serif;
}
/*
This rule is not needed ↷
p {
font-family: sans-serif;
}
*/
就像在JavaScript 裡那樣,如果我在局部作用域裡定義了某些規則,那麼它們在全域,或者說在任意祖先層級的作用域中都是無效的,只有在它們自己的子作用域裡是有效的(就像在上面程式碼中的
p
元素裡)。在下一個例子當中,
1.5
的
line-height
並沒有被
html
元素使用。但是,
p
裡的
a
元素則運用上了
line-height
的值。
html {
font-family: sans-serif;
}
## p {
}# line-height:# p {
}# line-height: 1.5;## /*
This rule is not needed ↷
p a {
line-height: 1.5;
p a {
line-height: 1.5;
}
可以繼承其中最大的程式碼為一致性的視覺化設計建立一個基礎。而這些樣式甚至會作用到你還沒寫的 HTML 上。我們在討論不會過時的程式碼!
替代方法
當然有另一種方式提供公用樣式。例如,我可以建立一個
.sans-serif
.sans-serif {
font-family: sans-serif;
}
…並將它應用到任意我想要它有這個樣式的元素上去:
Lorem ipsum.
sans-serif
class="sans-serif"
style="font-family: sans-serif"
的用法差不多 – 除了前者意味着要同时在样式表和 HTML 当中添加代码。使用继承,我们就可以在其中一个少写点,而另外一个则不用再写了。相比给每个字体样式写一个类,我们可以只在一个声明里,给
html
元素添加想要的规则。
html {
font-size: 125%;
font-family: sans-serif;
line-height: 1.5;
color: #222;
}
inherit
某些类型的属性是不会默认继承的,而某些元素则不会继承某些属性。但是在某些情况下,可以使用
[property name]: inherit
来强制继承。
举个例子,
input
元素在之前的例子中不会继承任何字体的属性,
textarea
也一样不会继承。为了确保所有元素都可以从全局作用域中继承这些属性,可以使用通配选择符和
inherit
关键字。这样,就可以最大程度地使用继承了。
* {
font-family: inherit;
line-height: inherit;
color: inherit;
}
html {
font-size: 125%;
font-family: sans-serif;
line-height: 1.5;
color: #222;
}
注意到我忽略了
font-size
。我不想直接继承
font-size
的原因是,它会将 heading 元素(译者注:如
h1
)、
small
元素以及其他一些元素的默认 user-agent 样式给覆盖掉。这么做我就可以节省一行代码,并且让 user-agent 决定想要什么样式。
另外一个我不想继承的属性是
font-style
:我不想重设
em
的斜体,然后再次添加上它。这将成为无谓的工作并会产生多余的代码。
现在,所有不管是可以继承或者是强制继承的字体样式都是我所期望的。我们已经花了很长时间只用两个声明区块来传递一个一致性的理念和作用域。从现在开始,除开一些例外情况,没有人会在构造组件的时候还需要去考虑
font-family
、
line-height
或者
color
了。这就是层叠的由来。
我可能想要主要的 heading 元素(
h1
)采用相同的
font-family
、
color
和
line-height
。使用继承就是很好的解决方案,但是我又想要它的
font-size
不一样。因为默认的 user-agent 样式已经给
h1
元素提供了一个大号的
font-size
(但这时它就会被我设置的相对基础字体大小为 125% 的样式覆盖掉),可能的话我不需要这里发生覆盖。
然而,难道我需要调整所有元素的字体大小吗?这时我就利用了全局作用域的优势,在局部作用域里只调整我需要调整的地方。
* {
font-family: inherit;
line-height: inherit;
color: inherit;
}
html {
font-size: 125%;
font-family: sans-serif;
line-height: 1.5;
color: #222;
}
h1 {
font-size: 3rem;
}
如果 CSS 元素的样式默认被封装,那么下面的情况就不可能了:需要明确地给
h1
添加所有字体样式。反而,我可以将样式分为几个单独的样式类,然后通过空格分隔来逐一给
h1
添加样式:
h1
。使用层叠,我已经给大部分元素赋上了想要的样式,并且只在一个方面使得
h1
成为一个例外。层叠作为一个过滤器,意味着样式只在添加新样式覆盖的时候才会发生改变。
我们已经开了个好头,但想要真正地掌握层叠,还需要尽可能多地给公共元素添加样式。为什么?因为我们的混合组件是由独立的 HTML 元素构成,并且一个屏幕阅读器友好的界面充分利用了语义化结构标记。
换句话说,让你的界面“分子化”(使用了 atomic 设计术语)的 “atoms” 样式应该在很大程度上可定位并且使用元素选择符。元素选择符的优先级很低,所以它们不会覆盖你之后可能加进来的基于类的样式。
首先应该做的事情就是给所有你即将需要使用的元素添加样式:
a { … }
p { … }
h1, h2, h3 { … }
input, textarea { … }
/* etc */
如果你想在无冗余的情况下有个一致性界面的话,那么下一步非常重要:每当你创建一个新组件的时候,如果它采用了一些新元素,那么就用元素选择符来给它们添加样式。现在不是时候去使用限制性、高优先级的选择符,也没有任何需要去编写一个样式类。语义化元素就使用其本身。
举个例子,如果我还没有给
button
元素 (就像前一个例子)添加样式,并且新组件加入了一个
button
元素,那么这就是一个给整个界面的
button
元素添加样式的好机会。
button {
padding: 0.75em;
background: #008;
color: #fff;
}
button:focus {
outline: 0.25em solid #dd0;
}
现在,当你想要再写一个新组件并且同样加入按钮的时候,就少了一件需要操心的事情了。在不同的命名空间下,不要去重写相同的 CSS,并且也没有类名需要记住或编写。CSS 本就应该总是致力于让事情变得简单和高效 – 它本身就是为此而设计的。
使用元素选择符有三个主要的优势:
生成的 HTML 更加简洁(没有多余的各种样式类)。
生成的样式表更加简洁(样式在组件间共享,不需要在每个组件里重写)。
生成的添加好样式的界面基于语义化 HTML。
使用类来专门提供样式常常被定义为“关注点分离”。这是对 W3C 的关注点分离原则的误解。它的目的是用 HTML 和 CSS 样式来描述整个结构。因为类专门是为了样式目的而制定,而且是在结构标记里出现,所以无论它们在哪里使用,技术上都是在打破分离,你不得不改变实质结构来得到样式。
不管在哪里都不要依赖表面的结构标记(样式类,内联样式),你的 CSS 应该兼容通用的结构和语义化的约定。这就可以简单地扩展内容和功能而无需它也变成一个样式的任务。同样在不同传统语义化结构的项目里,也可以让你的 CSS 变得更加可复用(但是这一点 CSS 的“方法论”可能会有所不同)。
特殊情况
在有人指责我过分简单化之前,我意识到界面上不是所有的按钮都做同样的事情,我还意识到做不同事情的按钮在某种程度上可能应该看起来不一样。
但这并不是说我们就需要用样式类、继承或者层叠来处理了。让一个界面上的按钮看起来完全不一样是在混淆你的用户。为了可访问性和一致性,大多数按钮在外观上只需要通过标签来进行区分。
记住样式并不是视觉上唯一的区分方法。内容同样可以在视觉上区分,而且在一定程度上它更加明确一些,因为你可是在文字上告诉了用户不同的地方。
大多数情况下,单独使用样式来区分内容都不是必要或者正确的。通常,样式区分应该是附加条件,比如一个红色背景或者一个带图标的文本标签。文本标签对那些使用声音激活的软件有着特定的效果:当说出 “red button” 或者 “button with cross icon” 的时候并没有引起软件的识别时。
我将在“工具类”部分探讨关于添加细微差别到看起来相似的元素上的话题。
语义化 HTML 并不仅仅关于元素。标签属性定义类型、样式属性和状态。这些对可访问性来说也很重要,所以它们需要写在 HTML 里合适的地方。而且因为都在 HTML 里,所以它们还提供了做样式钩子的机会。
举个例子,
input
元素有一个
type
属性,那么你应该想要利用它的好处,还有像
aria-invalid
属性是用来描述状态的。
input, textarea {
border: 2px solid;
padding: 0.5rem;
}
[aria-invalid] {
border-color: #c00;
padding-right: 1.5rem;
background: url(images/cross.svg) no-repeat center 0.5em;
}
这里有几点需要注意一下:
这里我不需要设置
color
、
font-family
或者
line-height
,因为这些都从
html
上继承了,得益于上面使用的
inherit
关键字。如果我想在整个应用的层面上改变
font-family
,只需要在
html
那一块对其中一个声明进行编辑就可以了。
border 的颜色关联到
color
,所以它同样是从全局
color
中继承。我只需声明 border 的宽度和风格。
[aria-invalid]
属性选择符是没有限制的。这意味着它有着更好的应用(它可以同时作用在
input
和
textarea
选择符)以及最低的优先级。简单的属性选择符和类选择符有着同样的优先级。无限制使用它们意味着之后任何写在层叠下的样式类都可以覆盖它们。
BEM 方法论通过一个修饰符类来解决这个问题,比如
input--invalid
。但是考虑到无效的状态应该只在可通信的时候起作用,
input--invalid
还是一定的冗余。换句话说,
aria-invalid
属性不得不写在那里,所以这个样式类的目的在哪里?
只写 HTML
在层叠方面关于大多数元素和属性选择符我绝对喜欢的事情是:组件的构造变成更少地了解公司或组织的命名约定,更多地关注 HTML。任何精通写出像样 HTML 的开发者被分配到项目中时,都会从已经写到位的继承样式当中获益。这些样式显著地减少了读文档和写新 CSS 的需要。大多数情况下,他们可以只写一些死记硬背应该知道的(meta)语言。Tim Baxter 同样为此在 Meaningful CSS: Style It Like You Mean It 里写了一个案例。
目前为止,我们还没有写任何指定组件的 CSS,但这并不是说我们还没有添加任何相关样式。所有组件都是 HTML 元素的组合。形成更复杂的组件主要是靠这些元素的组合顺序和排列。
这就给我们引出了布局这个概念。
主要我们需要处理流式布局 – 连续块元素之间的间距。你可能已经注意到目前为止我没有给任何元素设置任何的外边距。那是因为外边距不应该考虑成一个元素的属性,而应该是元素上下文的属性。也就是说,它们应该只在遇到元素的时候才起作用。
幸运的是,直接相邻选择符可以准确地描述这种关系。利用层叠,我们可以使用一个统一默认贯穿所有连续块级元素的选择符,只有少数例外情况。
* {
margin: 0;
}
* + * {
margin-top: 1.5em;
}
body, br, li, dt, dd, th, td, option {
margin-top: 0;
}
使用优先级极低的猫头鹰选择符确保了任意元素(除了那些公共的例外情况)都通过一行来间隔。这意味着在所有情况下都会有一个默认的白色间隔,所有编写组件流内容的开发者都将有一个合理的起点。
在大多数情况下,外边距只会关心它们自己。不过因为低优先级,很轻易就可以在需要的时候覆盖掉那基础的一行间隔。举个例子,我可能想要去掉标签和其相关元素之间的间隔,好表示它们是一对的。在下面的示例里,任意在标签之后的元素(
input
、
textarea
、
select
等等)都不会有间隔。
label {
display: block
}
label + * {
margin-top: 0.5rem;
}
再次,使用层叠意味着只需要在需要的时候写一些特定的样式就可以了,而其他的元素都符合一个合理的基准。
需要注意的是,因为外边距只在元素之间出现,所以它们不会和可能包括在容器内的内边距重叠。这也是一件不需要担心或者预防的事情。
还注意到不管你是否决定引入包装元素都得到了同样的间隔。就是说,你可以像下面这样做并实现相同的布局 – 外边距在
p
之间出现比在标签和输入框之间出现要好得多。
<form>
* + *
隐式控制的
first-child
这种例外情况:
color
、
background-color
以及其他属性建立独立的样式类,因为 atomic CSS 不会控制继承或者元素选择符。
html
)样式并强制继承,
流式布局方法及部分例外(使用猫头鹰选择符),
元素及属性样式。
我们还没有编写一个特定组件或者构思一个 CSS 样式类,但我们大部分的样式都已经写好了,前提是如果我们能够将样式类写得合理且可复用。
关于样式类它们有一个全局作用域:在 HTML 里任何地方使用,它们都会被关联的 CSS 所影响。对大多数人来说,这都被看做一个弊端,因为两个独立的开发者有可能以同样的命名来编写一个样式类,从而互相影响工作。
CSS modules 最近被用来解决这种情况,通过以程序来生成唯一的样式类名,绑定到它们的局部或组件作用域当中。
忽略掉生成代码的丑陋,你应该能够看到两个独立组件之间的不同,并且可以轻易地放在一起:唯一的标识符被用来区分同类的样式。在这么多更好的努力和冗余代码下,结果界面将要么不一致,要么一致。
没有理由对公共元素来进行唯一性区分。你应该对元素类型添加样式,而不是元素实例。谨记 “class” 意味着“某种可能存在很多的东西的类型”。换句话说,所有的样式类都应该是工具类:全局可复用。
当然,在这个示例里,总之
.button
类是冗余的:我们可以用
button
元素选择符来替代。但是如果有一种特殊类型的按钮呢?比如,我们可能编写一个
.danger
类来指明这个按钮是做危险性操作,比如删除数据:
.danger {
background: #c00;
color: #fff;
}
因为类选择符的优先级比元素选择符的优先级高,而和属性选择符优先级相同,所以这种方式添加在样式表后面的样式规则会覆盖前面元素和属性选择符的规则。所以,危险按钮会以红色背景配白色文本出现,但它其他的属性,比如内边距,聚焦轮廓以及外边距都会通过之前的流式布局方法添加,保持不变。
如果多位开发人员长时间在同样的代码基础上工作,那么偶尔就会发生命名冲突。但是有几种避免这种情况的方法,比如,噢,我不太知道,但对于你想要采用的名称我建议首先做一个文本搜索,看看是否已经存在了。因为你不知道,可能已经有人解决了你正在定位的问题。
局部作用域的各种工具类
对于工具类来说,我最喜欢做的事情就是把它们设置在容器上,然后用这个钩子去影响内部子元素的布局。举个例子,我可以快速对任意元素设置一个等间隔、响应式以及居中的布局。
.centered {
text-align: center;
margin-bottom: -1rem; /* adjusts for leftover bottom margin of children */
}
.centered > * {
display: inline-block;
margin: 0 0.5rem 1rem;
}
使用这个方法,我可以把列表项、按钮、按钮组合以及链接等随便什么元素居中展示。全靠
> *
的使用,在这个作用域中,它意味着带有
.centered
样式的元素下最近的子元素将会采用这些样式,并且还继承全局和父元素的样式。
而且我调整了外边距,好让元素可以自由进行包裹,而且不会破坏使用
* + *
选择符设置的垂直设定。这少量的代码通过对不同元素设置一个局部作用域,就提供了一个通用、响应式的布局解决方案。
我的一个小型(压缩后 93B)的基于 flexbox 网格布局系统 就是一个类似这种方法的工具类。它高度可复用,而且因为它使用了
flex-basis
,所以不需要断点干预。我只是用了 flexbox 布局的方法。
.fukol-grid {
display: flex;
flex-wrap: wrap;
margin: -0.5em; /* adjusting for gutters */
}
.fukol-grid > * {
flex: 1 0 5em; /* The 5em part is the basis (ideal width) */
margin: 0.5em; /* Half the gutter value */
}
使用 BEM 的方法,你会被鼓励在每个网格项上放置一个明确的“元素”样式类:
> *
所影响。仅有的区别就是充斥了大量样式类的标记。
所以,现在我们已经开始合并样式类,但只在通用性上合并,和它们所预期的效果一样。我们仍然还没有独立地给复杂组件添加样式。反而,我们在以一种可复用的方式解决一些系统性的问题。当然,你将需要在注释里写清楚这些样式类是如何使用的。
像这些的工具类同时采用了 CSS 的全局作用域、局部作用域、继承以及层叠的优点。这些样式类可以在各个地方使用,它们实例化局部作用域从而只影响它们的子元素,它们从父级或全局作用域中继承没有设置在自身的样式,而且我们没有过度使用元素或类选择符。
下面是现在我们的层叠看上去的样子:
全局(
html
)样式和强制性继承,
流式布局方法和一些例外(使用猫头鹰选择符),
元素和属性样式,
通用的工具类。
当然,可能没有必要去编写所有这些示例工具类。重点是,如果在使用组件的时候出现了需求,那么解决方案应该对所有组件都有效才行。一定要总是站在系统层面去思考。
特定组件样式
我们从一开始就已经给组件添加了样式,并且学习样式结合组件的方法,所以很多人有可能会忽略掉马上要讲到这个部分。但值得说明的是,任何不是从其他组件中创建的组件(甚至包括单个 HTML 元素)都是有必要存在的。它们是使用 ID 选择符的组件,以及有可能成为系统问题的风险。
事实上,一个好的实践是只使用 ID 来给复杂组件标识(“molecules”、“organisms”),并且不在 CSS 里使用这些 ID。比如,你可以在登录表单组件上写一个
#login
,那么你就不应该在 CSS 里以元素、属性或者流式布局方法的样式来使用
#login
,即使你可能会发现你在创造一个或两个可以在其他表单组件里使用的通用工具类。
如果你确实使用了
#login
,那么它只会影响那个组件。值得提醒的是如果这么做,那么你就已经偏离了开发一个设计系统方向,并且朝着只有不停纠结像素的冗长代码前进。
當我告訴人們我不使用諸如BEM 這樣的方法論或CSS 模組這樣的工具時,多數人會認為我會寫下面這樣的CSS:
header nav ul li {
display: inline-block;
}
header nav ul li a {
background: #008;
一份清晰的陳述已經在這裡了,還有我們需要小心去避免的事情也已經闡述了。只是想說明 BEM(還有 OOCSS、SMACSS、atomic CSS 等)並不是避免複雜、不可能被管理的 CSS 的唯一方法。
為了解決優先問題,許多方法論幾乎都選擇了使用類別選擇符。問題在於這產生了大量的樣式類:讓 HTML 標記變得臃腫的各種神奇程式碼,以及失去了對文件的注意力,這些都會讓新來的開發者對他們所處的系統感到困擾和迷惑。
透過大量使用樣式類,你還需要管理一個樣式系統,而且這個系統很大程度上是和 HTML 系統分開的。這種不太合適的所謂「關注點分離」可以造成冗餘,甚至更糟糕,導致不可訪問性:有可能會在可訪問的狀態下影響一個視覺上的樣式:
< input id="my-text" aria-invalid="false" class="text-input--invalid" />
為了替換掉大量的寫作和各種樣式類,我找到了其他一些方法:
為了一致性掌握繼承去設定一個前置條件;
充分使用元素和屬性選擇符去支援透明度和基於標準的組合樣式;
使用簡單的的串流佈局系統;
合併一些高度通用的工具類,解決影響多元素的共同佈局問題。
所有這些方法都是為了創建一個設計系統,使編寫一個新元件變得更簡單,以及當專案成熟的時候,減少添加新的 CSS 程式碼的依賴。而這並不是獲益於嚴格的命名和合併,反而是因為缺少了它們。
可能你會對我在這裡推薦的特殊技巧並不感冒,但我還是希望這篇文章至少可以讓你重新思考一下組件是什麼。它們不是你獨立創造的東西。有的時候,在標準 HTML 元素的情況下,它們甚至不是你所創造的東西。你的元件從其他元件拿來的東西越多,那麼介面的可訪問性和視覺上的一致性就會變得更好,最後會用更少的 CSS 去實現它們。
(這些問題)CSS 並沒有太多過錯。事實上,讓你做很多事情是非常好的,我們只是沒有利用罷了。
以上是CSS有關繼承的解析的詳細內容。更多資訊請關注PHP中文網其他相關文章!