我酷愛模組化設計。長期以來我都熱衷於將網站分離成元件,而不是頁面,並且動態地將那些元件合併到介面上。這種做法靈活,高效且易於維護。
但是我不想我的設計看起來是由一些不相關的東西組成的。我是在創造一個介面,而不是一張超現實主義的照片。
# 很幸運的是,已經有一項叫做 CSS 的技術,就是特意設計用來解決這個問題的。使用 CSS,我就可以在 HTML 元件之間到處傳遞樣式,從而以最小的代價來保證一致性的設計。這很大程度上要感謝兩個 CSS 特性:
繼承,
# 層疊 (CSS 當中的 C,cascade)。
儘管這些特性讓我們能夠以一種 DRY 且有效率的方式來為 Web 文件添加樣式,同時也是 CSS 存在的原因,但很明顯,它們已經不再受到青睞。在一些 CSS 方法論裡,如 BEM 和 Atomic CSS 這些透過程式化封裝 CSS 模組的方法,許多都盡力去規避或抑制這些特性。這也讓開發者有了更多機會控制他們的 CSS,但這只是基於頻繁幹預的專案控制。
我準備帶著對模組化介面設計的尊重在此重新審視繼承、層疊和作用域。我想要的告訴你的是如何利用這些特性讓你的 CSS 程式碼更簡潔,實現更好的自適應,並且提高頁面的可擴展性。
儘管許多人在抱怨 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: 1.5; } /* This rule is not needed ↷ p a { line-height: 1.5; } */
繼承最大的好處就是你可以用很少量的程式碼為一致性的視覺化設計建立一個基礎。而這些樣式甚至會作用到你還沒寫的 HTML 上。我們在討論不會過時的程式碼!
# 當然有另一種方式提供公用樣式。例如,我可以建立一個 .sans-serif 類別...
.sans-serif { font-family: sans-serif; }
...並將它套用到任意我想要它有這個樣式的元素上去:
<p class="sans-serif">Lorem ipsum.</p>
這種方法提供了一些控制上的權利:我可以準確地挑選決定哪些元素應用這個樣式,哪些元素不用。
任何能夠控制的機會都是很吸引人的,但有一些明顯的問題。我不僅需要手動地為需要應用樣式的元素添加類別名稱(這也意味著我要先確定這個樣式類別是什麼效果),而且在這種情況下也已經有效地放棄了支援動態內容的可能性:不管是富文本編輯器還是Markdown 解析器都沒辦法給任意的p 元素提供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; }
某些類型的屬性是不會預設繼承的,而某些元素則不會繼承某些屬性。但是在某些情況下,可以使用 [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 class="Ff(sans) Fs(3) Lh(1point5) C(darkGrey)">Hello World</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 的“方法论”可能会有所不同)。
在有人指责我过分简单化之前,我意识到界面上不是所有的按钮都做同样的事情,我还意识到做不同事情的按钮在某种程度上可能应该看起来不一样。
但这并不是说我们就需要用样式类、继承或者层叠来处理了。让一个界面上的按钮看起来完全不一样是在混淆你的用户。为了可访问性和一致性,大多数按钮在外观上只需要通过标签来进行区分。
<button>create</button> <button>edit</button> <button>delete</button>
记住样式并不是视觉上唯一的区分方法。内容同样可以在视觉上区分,而且在一定程度上它更加明确一些,因为你可是在文字上告诉了用户不同的地方。
大多数情况下,单独使用样式来区分内容都不是必要或者正确的。通常,样式区分应该是附加条件,比如一个红色背景或者一个带图标的文本标签。文本标签对那些使用声音激活的软件有着特定的效果:当说出 “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 的开发者被分配到项目中时,都会从已经写到位的继承样式当中获益。这些样式显著地减少了读文档和写新 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> <p> <label for="one">Label one</label> <input id="one" name="one" type="text"> </p> <p> <label for="two">Label two</label> <input id="two" name="two" type="text"> </p> <button type="submit">Submit</button> </form>
用像 atomic CSS 这样的方法能实现同样的效果,只需组合各种外边距相关的样式类并在各种情况下手动添加它们,包括被 * + * 隐式控制的 first-child 这种例外情况:
<form class="Mtop(1point5)"> <p class="Mtop(0)"> <label for="one" class="Mtop(0)">Label one</label> <input id="one" name="one" type="text" class="Mtop(0point75)"> </p> <p class="Mtop(1point5)"> <label for="two" class="Mtop(0)">Label two</label> <input id="two" name="two" type="text" class="Mtop(0point75)"> </p> <button type="submit" class="Mtop(1point5)">Submit</button> </form>
记住如果坚持使用 atomic CSS 的话,像上面那么写只会覆盖到顶部外边距的情况。你必须还要为 color、background-color 以及其他属性建立独立的样式类,因为 atomic CSS 不会控制继承或者元素选择符。
<form class="Mtop(1point5) Bdc(#ccc) P(1point5)"> <p class="Mtop(0)"> <label for="one" class="Mtop(0) C(brandColor) Fs(bold)">Label one</label> <input id="one" name="one" type="text" class="Mtop(0point75) C(brandColor) Bdc(#fff) B(2) P(1)"> </p> <p class="Mtop(1point5)"> <label for="two" class="Mtop(0) C(brandColor) Fs(bold)">Label two</label> <input id="two" name="two" type="text" class="Mtop(0point75) C(brandColor) Bdc(#fff) B(2) P(1)"> </p> <button type="submit" class="Mtop(1point5) C(#fff) Bdc(blue) P(1)">Submit</button> </form>
Atomic CSS 使开发者可以直接控制样式而不再使用内联样式,内联样式不像样式类一样可以复用。通过为各种独立的属性提供样式类,减少了样式表中的重复声明。
但是,它需要直接介入标记从而实现这些目的。这就要求学习并投入它那冗长的 API,同样还需要编写大量额外的 HTML 代码。
相反,如果只用来对任意 HTML 元素及其空间关系设计样式的话,那么 CSS “方法论”就要被大范围弃用了。使用一致性设计的系统有着很大的优势,相比一个叠加样式的 HTML 系统更方便考虑和分开管理。
无论如何,下面是我们的 CSS 架构和流式布局内容解决方案应该具备的特征:
全局(html)样式并强制继承,
流式布局方法及部分例外(使用猫头鹰选择符),
元素及属性样式。
我们还没有编写一个特定组件或者构思一个 CSS 样式类,但我们大部分的样式都已经写好了,前提是如果我们能够将样式类写得合理且可复用。
关于样式类它们有一个全局作用域:在 HTML 里任何地方使用,它们都会被关联的 CSS 所影响。对大多数人来说,这都被看做一个弊端,因为两个独立的开发者有可能以同样的命名来编写一个样式类,从而互相影响工作。
CSS modules 最近被用来解决这种情况,通过以程序来生成唯一的样式类名,绑定到它们的局部或组件作用域当中。
<!-- my module's button --> <button class="button_dysuhe027653">Press me</button> <!-- their module's button --> <button class="button_hydsth971283">Hit me</button>
忽略掉生成代码的丑陋,你应该能够看到两个独立组件之间的不同,并且可以轻易地放在一起:唯一的标识符被用来区分同类的样式。在这么多更好的努力和冗余代码下,结果界面将要么不一致,要么一致。
没有理由对公共元素来进行唯一性区分。你应该对元素类型添加样式,而不是元素实例。谨记 “class” 意味着“某种可能存在很多的东西的类型”。换句话说,所有的样式类都应该是工具类:全局可复用。
当然,在这个示例里,总之 .button 类是冗余的:我们可以用 button 元素选择符来替代。但是如果有一种特殊类型的按钮呢?比如,我们可能编写一个 .danger 类来指明这个按钮是做危险性操作,比如删除数据:
.danger { background: #c00; color: #fff; }
因为类选择符的优先级比元素选择符的优先级高,而和属性选择符优先级相同,所以这种方式添加在样式表后面的样式规则会覆盖前面元素和属性选择符的规则。所以,危险按钮会以红色背景配白色文本出现,但它其他的属性,比如内边距,聚焦轮廓以及外边距都会通过之前的流式布局方法添加,保持不变。
<button class="danger">delete</button>
如果多位开发人员长时间在同样的代码基础上工作,那么偶尔就会发生命名冲突。但是有几种避免这种情况的方法,比如,噢,我不太知道,但对于你想要采用的名称我建议首先做一个文本搜索,看看是否已经存在了。因为你不知道,可能已经有人解决了你正在定位的问题。
对于工具类来说,我最喜欢做的事情就是把它们设置在容器上,然后用这个钩子去影响内部子元素的布局。举个例子,我可以快速对任意元素设置一个等间隔、响应式以及居中的布局。
.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 的方法,你会被鼓励在每个网格项上放置一个明确的“元素”样式类:
<p class="fukol"> <!-- the outer container, needed for vertical rhythm --> <ul class="fukol-grid"> <li class="fukol-grid__item"></li> <li class="fukol-grid__item"></li> <li class="fukol-grid__item"></li> <li class="fukol-grid__item"></li> </ul> </p>
但这不是必要的。只需一个标识符去实例化本地作用域。这里的列表项相比起我版本当中的列表项,不再受外部影响的保护,也不应该被 > * 所影响。仅有的区别就是充斥了大量样式类的标记。
所以,现在我们已经开始合并样式类,但只在通用性上合并,和它们所预期的效果一样。我们仍然还没有独立地给复杂组件添加样式。反而,我们在以一种可复用的方式解决一些系统性的问题。当然,你将需要在注释里写清楚这些样式类是如何使用的。
像这些的工具类同时采用了 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中文網其他相關文章!