[译] 什么是模块化 CSS?

模块化 CSS 是一组编写代码的原则,基于这个原则编写的代码具备高性能和可维护性。它起源于雅虎和 Yandex 的开发人员,目的是迎接维护大型代码库带来的挑战。有些规则在提出之初稍有争议,但后来被认为是最佳实践。css

目录:html

  1. 大规模 CSS 的处理难点
  2. 什么是模块化
  3. 模块化框架
    1. OOCSS
    2. BEM
    3. SMACSS
  4. 共享模块化原则
  5. FAQ
  6. 总结,模块化 CSS 太美妙啦

(偷偷告诉你:若是你对这篇文章的篇幅感到不知所措,观看视频可能更适合你,这篇文章来源于此演讲。)前端

大规模 CSS 的处理难点

模块化 CSS 使用的主要场景是棘手的大规模 CSS。正如 Nicholas Gallagher 所说的android

“Replace ‘can you build this?’ with ‘can you maintain this without losing your minds?’” —Nicolas Gallagher

来源:Nicholas Gallagher,图:dotCSSios

这句话直指大规模 CSS 问题的核心。写代码并不难,难的是在不让你的代码随着时间的推移成为拖累你的“技术债”。git

难以理解

如下是 CSS Guidelines 中的一个示例,这个示例展现了一个问题:除了写这段代码的人,没有人知道这段代码是干什么的。github

<div class="box profile pro-user">
  <img class="avatar image" />
  <p class="bio">...</p>
</div>
复制代码

boxprofile 有什么关系?profileavatar 有什么关系?或者他们之间真的有关系吗?你应该在 bio 旁边添加 pro-user 吗?imageprofile 写在同一部分 CSS 吗?能够在其余地方使用 avatar 吗?web

光看代码没法回答这些问题,你必须在 CSS 代码中推理他们的做用。编程

难以复用

复用代码会很是棘手。假设你要在另外一个页面上复用某个页面上的样式,但你想这么作的时候,会发现那个样式是专为第一个页面而写的。代码的做者认为它只用在某个特定元素中,或者它是从页面继承某些类,在其余环境中根本不起做用。你不想修改原来的内容,而后直接复制了代码。后端

如今你有两个问题:一份原始代码,一份重复代码,你的维护负担直接增长了一倍。

难以维护

大规模的 CSS 也难以维护。你改变了一个标签,样式就会像纸牌屋同样崩溃。你想更新一个页面上的样式,却破坏了另外一个页面的样式。你试图覆盖其余页面,但又深陷于优先度问题。

它让我想起了我最喜欢的 CSS 笑话之一:

什么是模块化

那么咱们如何解决这些问题呢?答案在于模块化这个概念,但这是什么呢?咱们先看看 Harry Roberts关注点分离的看法:

“Code which adheres to the separation of concerns can be much more confidently modified, edited, extended, and maintained because we know how far its responsibilities reach. We know that modifying layout, for example, will only ever modify layout—nothing else.” —Harry Roberts

来源:Harry Roberts,图:CSSwizardry.com

这是一个常见编程习惯,可是许多 CSS 开发者不太熟悉。这个思想是确保你所写的东西不会比你想要作的更多。

举个例子,说明我在学习模块化 CSS 以前的工做方式。设计师给我这样的草图:

Illustration of a design comp for a bookstore website

图:Yandex

我会以为:“好吧,这是一个书店页面,侧边栏中有一些小部件,右侧列出了大概是书籍封面的清单,一个精选书评,下面还有其余的评论。”

我当时认为一个页面是一个完整的单元,页面里的较小部分从属于页面。这是一种自上而下的思考方法,这致使大量只服务于单个页面的一次性代码,不利于编写可复用代码。

Illustration of a design comp for a bookstore with the components highlighted

图:Yandex

模块化 CSS 须要你换一个角度看问题,不从页面级别考虑,而是关注组成页面的小块。这不是一个页面而是一个组件的集合。

你会发现页面里包含的是 logo,搜索栏,导航,照片列表,辅助导航,标签框,视频播放器等。这些是能够网站的任何位置均可以独立使用的内容。它们只是碰巧在这个特定页面以这种方式组合。

模块化 CSS 是自下而上的思惟,须要从构建整个站点的可复用构建模块开始。

Image of workers building with Lego bricks

图:BEM Method

这会让你想起乐高?应该的!几乎全部撰写有关模块化 CSS 的人都使用乐高进行类比。使用标准化,易于理解,而且不依赖上下文的块来构建 UI 的是一个很好的思路。

这样的“块”最著名的例子之一是由 Nicole Sullivan 定义的“媒体对象”,她认为这种对象是你将在任何网站上找到的最小的组件之一。

An example of the media object

它将固定宽度的图像组合到灵活宽度的容器的一侧,如今处处均可以看到这个模式。她撰写了一篇名为 The Media Object Saves Hundreds of Lines of Code 的案例研究,谈到将此模式应用于大型网站,最大的例子之一即是 Facebook:

The media object highlighted in red on the facebook homepage

图:Nicole Sullivan

这里高亮显示了 Facebook 流中的全部媒体对象。左上角我的信息,右侧导航元素,订阅的每一个帖子,甚至是广告都是媒体对象。有时它们彼此嵌套。虽然使用目的不一样,但它们都共享相同的基础模式:固定宽度的图像,弹性宽度的文本。

她的观点是,以 Facebook 的规模运营时,媒体对象就不止几十个,这样的页面上有数百上千个。所以,能够想象若是为复用样式做优化,能够节省大量代码,这能够带来真正的高性能和低成本。

模块化框架

那么,既然咱们已经明确了模块化的概念,那么让咱们看看这些年来推崇这一律念的三大框架:

OOCSS

面向对象的 CSS(Object-Oriented CSS)/ OOCSS 是模块化 CSS 的起源,由 Nicole Sullivan 于 2009 年提出,这基于她在雅虎的工做。这个框架的核心思想是 —— 对象是可重用的模式(pattern),其视觉外观不禁上下文决定。

  • 有人质疑雅虎的能力,雅虎的前端团队当时研发的 YUI library 是很是前沿的技术。在 2009 年,雅虎不是一家没有前途的科技公司。

“a CSS ‘object’ is a repeating visual pattern, that can be abstracted into an independent snippet of HTML, CSS, and possibly JavaScript. That object can then be reused throughout a site.” —Nicole Sullivan

来源:Nicole Sullivan,图:John Morrison

正如她在 2009 年所定义的那样,这就是模块化 CSS 的起源。除此以外,OOCSS 可归结为几个核心原则:

上下文无关

首先,不管你把它放在哪里,一个对象都应该看起来无差异,不该根据对象的上下文设置对象的样式。

例如,不是将侧边栏中的全部按钮都设置为橙色,将主区域中的全部按钮设置为蓝色,而是应该建立一个蓝色的按钮类,以及一个橙色的 modifier。这样作橙色按钮能够在任何地方使用,它们没有被绑定在侧边栏上,它们只是你的按钮样式之一。

皮肤(主题)

她谈到的另外一个概念,是如何从正在应用的皮肤中抽象出对象的结构

咱们能够回到媒体对象的例子。它的样式与标签结构无关。有一个容器,一个固定宽度的图像和内容。你能够应用不一样的样式,可是不管样式如何改变,标签结构都是同样的。

她建议的其余方法是为常见的视觉模式建立可复用的类。她给出了一个例子,在 2009 年的亚马逊网站几乎全部的东西都有阴影,但由于它们由不一样设计师创做,因此类似却不相同。经过标准化这些元素阴影,能够优化代码并使网站更高效。

使用 Class

当时她提出了一个很是具备争议性的规则,但后来被广为接受:使用 class 来命名对象及其子元素,这样能够在不影响样式的状况下修改 HTML 标签。

她不但愿 CSS 由 HTML 标签来肯定,这样的话若是将标题从“h1”更改成“h4”,则没必要更新 CSS。不管选择哪一个标签,该标题应该有一个固有的 class。例如,你的导航应该相似于 .site-nav 而不是 #header ul

不使用 ID

既然建议“老是使用 class”,那么天然禁止使用 ID 选择器。这与当时使用 ID 做为命名空间的常见实践相违背,直接引用嵌套在其中的元素。

ID 会扰乱 CSS 优先度,这是其次,对象必须是可复用的。根据定义,ID 是惟一的。所以,若是在对象上设置 ID,则没法在同一页面上重复使用它,缺乏了模块化对象的要点。

BEM

接下来介绍下一个弘扬模块化 CSS 精神的框架。BEM,三个字母分别表明 Block、Element、Modifier,BEM 也是在 2009 年提出,起源于 Yandex(能够说是俄语版的 Google),除搜索业务外还运营网络邮件程序,所以在编程上他们也须要解决与雅虎相同规模的难题。

他们提出了一套很是相似的代码原则。他们的核心概念是 —— 块(block)(Nicole 称之为“物体(object)”)由子元素(element)构成,而且能够修改(modified)(或“主题化”)。

如下是其中一位负责 BEM 的开发者 Varya Stepanova 对 BEM 的描述:

“BEM is a way to modularize development of web pages. By breaking your web interface into components… you can have your interface divided into independent parts, each one with its own development cycle.” —Varya Stepanova

来源:Varya Stepanova,图:ScotlandJS

BEM 由 3 部分组成:

块(Block)

块是网页逻辑和功能的独立组件。BEM 的发起人对其提出了更详尽的定义:

首先,块是可嵌套的。它们应该能被包含在另外一个块中,而不会破坏任何样式。例如,可能在侧栏中有一个标签界面小部件的块,该块可能包含按钮,这些按钮也是一种单独的块。按钮的样式和选项卡式元素的样式不会相互影响,一个嵌套在另外一个中,仅此而已。

其次,块是可重复的。界面应该可以包含同一块的多个实例。就像 Nicole 所说的媒体对象同样,复用块能够节省大量代码。

元素(Element)

元素是块的组成部分,它不能在块以外使用。一个不错的例子:一个导航菜单,它包含的项目在菜单的上下文以外没有意义。你不会为菜单项定义块,菜单自己应定义为块,而菜单项是其子元素。

修饰符(Modifier)

修饰符定义块的外观和行为。例如,菜单块的外观的垂直或水平,取决于所使用的修饰符。

命名约定

BEM 所作的另外一件事是定义了很是严格的命名约定:

.block-name__element--modifier

这看起来有点复杂,我来分解一下:

  • 名称以小写字母书写
  • 名称中的单词用连字符(-)分隔
  • 元素由双下划线(__)分隔
  • 修饰符由双连字符(--)分隔

这么说也有点抽象,举一个例子:

Example of .minifig to indicate a lego minifig

如今咱们有一个标准的乐高 minifig。他是一个蓝色的宇航员。咱们将使用 .minifig 类来区分他。

Example of .minifig module with child elements such as .minifig__head and .minifig__legs

能够看到 .minifig 块由较小的元素组成,例如 .minifig__head.minifig__legs。如今咱们添加一个修饰符:

Example of .minifig--red module modifier, turning the minifig red

经过添加 .minifig--red 修饰符,咱们建立了标准蓝色宇航员的红色版本。

Example of a .minifig--yellow-new module modifier, turning the minifig yellow

或者,咱们可使用 .minifig--yellow-new 修饰符将咱们的宇航员改成新式黄制服版。

Example of a .minifig--batman module modifier making a drastic change in the appearance of the minifig

你可使用一样的方式进行更夸张的修改。经过使用 .minifig--batman 修饰符,咱们只用一个类就改变了 minifig 的每一个部分的外观。

这是实践中的 BEM 语法例子:

<button class="btn btn--big btn--orange">
  <span class="btn__price">$9.99</span>
  <span class="btn__text">Subscribe</span>
</button>
复制代码

即便不看样式代码,你也能够一眼就看出这段代码会建立一个既大又橙的价格按钮。不管你是否喜欢带有连字符和下划线的这种风格,拥有严格的命名约定是模块化 CSS 向前迈出的一大步,这让代码带有自文档的效果!

不嵌套 CSS

就像 OOCSS 建议使用 class 而不使用 ID 同样,BEM 也为代码风格做了一些限制。最值得注意的是,他们认为不该该嵌套 CSS 选择器。嵌套选择器扰乱了优先度,使得重用代码变得更加困难。例如,只需使用 .btn__price 而不是 .btn .btn__price

注意:这里的嵌套指实践中在 Sass 或 Less 嵌套选择器的作法,但即便你没有使用预处理器也适用,由于这关乎选择器优先度问题。

这个原则不出问题是由于严格的命名约定。咱们曾经使用嵌套选择器将它们隔离在命名空间的上下文中。而 BEM 的命名约定自己就提供了命名空间,所以咱们再也不须要嵌套。即便 CSS 的根级别的全部内容都是单个类,但这些名称的具体程度足以免冲突。

通常来讲,选择器能够在没有嵌套的状况下生效,就不要嵌套它。 BEM 容许此规则的惟一例外是基于块状态或其修饰符的样式元素。例如,可使用 .btn__text 而后用 .btn--orange .btn__text 来覆盖应用了修饰符按钮的文本颜色。

SMACSS

咱们最后要讨论的框架是 SMACSS,含义是 CSS 的可扩展性和模块化架构(Scalable & Modular Architecture)。Jonathan Snook 于 2011 年提出了 SMACSS,当时他在雅虎工做,为 Yahoo Mail 编写 CSS。

“At the very core of SMACSS is categorization. By categorizing CSS rules, we begin to see patterns and can define better practices around each of these patterns.” —Jonathan Snook

来源:Jonathan Snook,图:Elida Arrizza

他在 OOCSS 和 BEM 的基础上添加的关键概念是,不一样类别的组件须要以不一样的方式处理。

类别(Categories)

如下是他为 CSS 系统可能包含的规则定义的类别:

  1. 基础(Base) 规则是HTML元素的默认样式,如连接,段落和标题。
  2. 布局(Layout) 规则将页面分红几个部分,并将一个或多个模块组合在一块儿。它们只定义布局,而无论颜色或排版。
  3. 模块(Module)(又名“对象”或“块”)是可重用的,设计中的一个模块。例如,按钮,媒体对象,产品列表等。
  4. 状态(State) 规则描述了模块或布局在特定状态下的外观。一般使用 JavaScript 应用或删除。例如,隐藏,扩展,激活等。
  5. 主题(Theme) 规则描述了模块或布局在主题应用时的外观,例如,在 Yahoo Mail 中,可使用用户主题,这会影响页面上的每一个模块。(这很是适用于像雅虎这样的应用程序,但大多数网站都不会使用此类别。)

命名约定前缀

下一个原则是使用前缀来区分类别,他喜欢 BEM 明确的命名约定,但他还但愿可以一目了然地看出模块的类型。

  • l- 用做布局规则的前缀:l-inline
  • m- 用做模块规则的前缀:m-callout
  • is- 用做状态规则的前缀:is-collapsed

(基础规则没有前缀,由于它们直接应用于 HTML 元素而不使用类。)

共享模块化原则

这些框架的相同之处远胜于其不一样之处。我看到从 OOCSS 到 BEM 再到 SMACSS 的明确发展。它们的发展表明了咱们行业在性能和大规模 CSS 领域不断增加的经验。

你没必要选择其中一个框架,相反,咱们能够尝试定义模块化 CSS 的通用规则。让咱们看看这些框架共用和保留的最佳部分。

模块化元素

模块化系统由如下元素组成:

  • 模块(Module):(又名对象,块或组件)一种可复用且自成一体的模式。如媒体对象,导航和页眉。
  • 子元素(Child Element): 一个不能独立存在的小块,属于模块的一部分。如媒体对象中的图像,导航选项卡和页眉 logo。
  • 模块修改器(Module Modifier):(又名皮肤或主题)改变模块的视觉外观。如左/右对齐的媒体对象,垂直/水平导航。

模块化类别

模块化系统中的样式能够分为如下几类:

  • 基础(Base) 规则是 HTML 元素的默认样式,如:alih1
  • 布局(Layout) 规则控制模块的布局方式,但不控制视觉外观,如:.l-centered.l-grid.l-fixed-top
  • 模块(Modules) 是可复用的,独立的 UI 组件视觉样式,如:.m-profile.m-card.m-modal
  • 状态(State) 规则由 JavaScript 添加,如:.is-hidden.is-collapsed.is-active
  • 助手(Helper)(又名功能)规则适用范围小,独立于模块,如:.h-uppercase.h-nowrap.h-muted

模块化规则

在模块化系统中编写样式时,请遵循如下规则:

  • 不要使用 ID
  • CSS 嵌套不要超过一层
  • 为子元素添加类名
  • 遵循命名约定
  • 为类名添加前缀

FAQ

这么作 HTML 不会有不少类吗?

模块化 CSS 最多见的反对意见就是,它会在 HTML 中产生许多类。我认为这是由于长期以来 CSS 的最佳实践都认为应该避免大量 class 使用。早在 2011 年,Nicole Sullivan 就写了一篇很棒的博文 Our (CSS) Best Practices are Killing Us,明确驳斥了这个想法。

我看到一些开发人员提倡使用预处理器的 extend 函数将多个样式链接成一个类名。我建议不要这样作,由于它会使你的代码不那么灵活。他们不能让其余开发者以新的方式组合你的乐高积木,而是固定了你定义的几种组合。

BEM 的类名又长又丑!

不要由于类名太长而惧怕,他们是自文档的!当我看到 BEM 风格的类名(或任何其余模块化命名约定)时,我会以为很愉悦,由于只要看一眼就能知道这些类的含义。你能够在 HTML 中清晰理解它们。

孙元素的命名如何约定?

长话短说:没这回事。

模块化 CSS 初学者能够快速掌握子元素的概念:minifig__armminifig 的一部分。然而,有时候他们处理 CSS 中的 DOM 结构时,会疑问如何做深层嵌套,好比 minifig__arm__hand

没有必要这样作。请记住,这个思路是要将样式与标记分离。不管 handminifig 的直接子元素仍是嵌套了多少层,都可有可无。CSS 关心的只有 handminifig 的孩子。

.minifig {}
  .minifig__arm {}
      .minifig__arm__hand {} /* don't do this */ .minifig__hand {} /* do this instead */ 复制代码

模块冲突怎么办?

模块化 CSS 初学者比较关注的另外一件事是模块之间的冲突。例如,若是我将 l-card 模块和 m-author-profile 模块同时应用于同一个元素,是否会致使问题?

答案是:理想状况下,模块不该该重叠太多。在这个例子中,l-card 模块关注布局,而 m-author-profile 模块关注样式,你可能会看到 l-card 设置宽度和边距,而 m-author-profile 设置背景颜色和字体。

测试模块是否冲突的一种方法是以随机顺序加载它们。你能够将项目构建配置中设定为在构建时随机交换样式位置。若是看到bug,就证实你的 CSS 须要以特定顺序加载。

若是你发现须要将两个模块应用于同一个元素而且它们存在冲突,请考虑它们是否真的是两个独立的模块。也许它们能够用一个修饰符组合成一个模块?

该规则的最后一个例外是“helper”或“utility”类可能会发生冲突,在这些状况下,你能够安全地考虑使用 !important。我知道,你曾被告知 !important 不是什么好东西,永远不该该被使用,但咱们的作法有细微的差异:主动使用它来确保 helper 类老是优先仍是不错的。 (Harry Roberts has more to say on this topic in the CSS Guidelines。)

总结,模块化 CSS 太美妙啦

咱们来简要回顾一下,还记得这段代码吗?

<div class="box profile pro-user">
  <img class="avatar image" />
  <p class="bio">...</p>
</div>
复制代码

boxprofile 有什么关系?profileavatar 有什么关系?或者他们之间有关系吗?你应该在 bio 旁边添加 pro-user 吗?imageprofile 写在同一部分 CSS 吗?能够在其余地方使用 avatar 吗?

如今咱们知道如何解决这些问题了。经过编写模块化 CSS 并使用适当的命名约定,咱们能够编写自文档的代码:

<div class="l-box m-profile m-profile--is-pro-user">
  <img class="m-avatar m-profile__image" />
  <p class="m-profile__bio">...</p>
</div>
复制代码

咱们能够看到哪些类彼此相关,哪些类彼此不相关,以及如何相关。咱们知道在这个组件的范围以外咱们不能使用哪些类,固然,咱们还知道哪些类能够在其余地方复用。

模块化 CSS 简化了代码并推动了重构,产出自文档的代码,这样的代码不影响外部做用域且可复用。

或者换句话说,模块化 CSS 是可预测的,可维护的而且是高性能的。

如今咱们能够重温那个老笑话,结局发生了变化:

Two CSS properties walk into a bar. Everything is fine, thanks to modular code and proper namespacing.

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏