Skip to content
This page has been auto-translated and may contain errors.View in English

组织和扩展 CSS

docs.scrimba.com

在新项目的第一份样式表上工作是件乐事。写到几百行时,情况开始改变:你改变了一个按钮的颜色,另外三个按钮也跟着改变了,一个标题拒绝移动,直到你在它旁边贴上 !important,每条新规则都可能破坏旧的。CSS 本身仍然是正确的。缺少的是承载它的结构,正是这个结构,而不是语法,决定了一份样式表在一千行还是十万行时是否仍可维护。

为什么 CSS 难以扩展

CSS 默认是全局的。你写的每条规则都可以影响到页面上任何匹配的元素。写 p { color: navy; } 的话,整个项目中的每个段落都会变成深蓝色,无论你是否有意。在小页面上这很便利。随着项目增长,这就是大多数问题的根源。

问题表现为规则冲突。两份样式表都针对 p,或一条通用规则和一条具体规则都应用到同一个元素,现在你必须弄清楚哪一个胜出。你在 CSS 如何工作 中看到了决定这一点的机制:浏览器通过特异性和顺序解决冲突。扩展 CSS 的关键在于首先不创建那些冲突。

CSS 没有内置作用域。一条规则是**全局的**:它应用到页面加载的每个文件中所有匹配其选择器的元素。没有什么阻止一个组件的样式进入另一个组件,因为这种语言没有"这条规则属于那个组件"的概念。这种自由正是让 CSS 易于开始而难以增长的原因。

随着代码库增长,两种力量推动你。规则冲突,因为更多的选择器意味着更多的重叠,浏览器用 CSS 如何工作 中的规则解决每次冲突:特异性优先,然后是源顺序。特异性往往会逐渐增加,因为赢得今天的冲突最快的方法是写一个稍微更具体的选择器,这提高了明天任何需要覆盖它的东西的门槛。如果不加以控制,你最终会得到没人能在不使用 !important 的情况下战胜的选择器,这时样式表不再感觉可维护。

CSS 是一个平坦的全局命名空间。每个选择器在同一空间竞争,级联(当多个声明应用到一个元素时选择获胜声明的算法)通过来源、特异性,然后源顺序解决每次冲突。语言本身没有模块边界,所以为一个组件写的选择器可以自由匹配任何其他组件中的元素。本章中的每一个扩展技术都存在于施加一个语言没有提供的边界。

失败模式值得精确命名,因为下面的习惯都是对它的防御。在压力下,最快的修复一条不会应用的规则的方法是让它的选择器更具体:添加一个父级,链接另一个类,回到一个 id。每一个都赢了即时冲突,提高了下游所有东西的特异性下限,所以下一个覆盖必须更具体。这是一个**特异性战争**:一个单向的棘轮,选择器只能向上攀升,以 !important 结尾,因为没有其他东西可以战胜它们。逃脱的方式不是更聪明地赢得这些战斗,而是保持特异性低且平坦,使得它们很少开始,以及,在语言现在允许的地方,用级联层将排序移出特异性,这是本章的最后主题。

css
/* 一条全局规则影响页面上的每个段落 */
p {
  color: navy;
}

/* 另一条规则,在其他地方,竞争相同的元素 */
.notice p {
  color: crimson;   /* 在 .notice 内胜出:更具体 */
}
Juno为什么 CSS 难以扩展 CSS 规则是全局的:一条规则可以样式化页面各处的元素,这一开始很方便,后来会很混乱。随着项目增长,规则开始冲突,浏览器必须使用特异性和顺序选择获胜者。组织 CSS 的大部分工作是关于首先编写不彼此冲突的规则。
Juno为什么 CSS 难以扩展 CSS 中没有作用域,所以每条规则都是全局的,可以自由地到达任何匹配的元素。随着代码库增长,两件事产生影响:规则冲突,特异性上升,因为对冲突的快速修复总是一个更具体的选择器。控制住这个上升,扩展的其余部分就会容易得多。
Juno为什么 CSS 难以扩展 CSS 是一个平坦的全局命名空间,没有模块边界,所以级联通过来源、特异性,然后顺序解决每次冲突。陷阱是特异性棘轮:每个快速覆盖都提高了下限,所以下一个必须爬得更高,最后你陷入了 !important。本章中的每一个技术都是保持特异性足够低,使棘轮永远不会开始转动的方式。

保持特异性低且平坦

最有用的单一习惯是用****样式化,大多情况下一次一个类。像 .card 这样的类很容易应用,可重用,如果以后需要,也可以毫无痛感地覆盖,因为单个类是一个低而温和的特异性水平。

麻烦来自两件事:id 和长的选择器链。像 #header 这样的 id 比类更难覆盖,像 .sidebar ul li a 这样的链被绑定到一个精确的结构。更喜欢你可以放在任何地方的普通类:

保持 CSS 可维护的核心习惯是保持特异性**低且平坦**:用单个类样式化,避免两件提高特异性的东西,id 和深的后代链。单个类是最佳点。它足够具体,可以针对你的意思,但足够薄弱,另一个单个类稍后可以在没有战斗的情况下覆盖它。

Id 的得分远高于类,所以 #id 规则很难覆盖,强制你向上。长的后代链导致不同的问题:.sidebar nav ul li a 既提高特异性,又将规则焊接到一个精确的 HTML 结构,所以标记改变的瞬间就会破坏。给元素它自己的类并直接针对那个。

保持特异性**低且平坦**是防止特异性战争而非赢得它们的习惯。低意味着每条规则得分尽可能少,同时仍然正确选择;平坦意味着规则聚集在相同的低权重周围,所以任何一条都可以通过源顺序单独覆盖任何其他。几乎所有东西都是单类选择器的样式表具有这个性质:没有什么难以战胜,因为没有什么得分在它的邻居之上。

两个构造会破坏平坦性,两个都值得默认避免。Id 贡献的特异性比类多一个数量级,所以单个 #id 规则坐在任何类规则的堆上方,只能被另一个 id 或 !important 战胜;用类样式化,为片段链接和 JavaScript 钩保留 id。深后代链提高每个选择器的特异性并且将规则与固定的祖先耦合,所以 .sidebar nav ul li a 既难以覆盖又脆弱:改变标记,它悄悄停止匹配。下一节的方法论在很大程度上存在是让你直接命名一个元素,所以你永远不会为了找到它而求助于链。

css
/* 首选:单个类,低且平坦 */
.nav-link {
  color: navy;
}

/* 避免:id,稍后难以覆盖 */
#nav-link {
  color: navy;
}

/* 避免:深链,脆弱且更高的特异性 */
.sidebar nav ul li a {
  color: navy;
}
Juno保持特异性低且平坦 用类样式化,大多情况下一次一个类,像 .card.nav-link。单个类快速可重用,稍后覆盖毫无痛感,正是你想要的。远离 #header 之类的 id 和 .sidebar ul li a 之类的长链,因为两者都更难改变。
Juno保持特异性低且平坦 单个类是最佳点:具体到足以击中你的意思,薄弱到另一个类可以在没有战斗的情况下覆盖它。Id 提高特异性并拖拽你向上,深链像 .sidebar nav ul li a 在标记改变的瞬间就会破坏。当一条规则难以放置时,给元素它自己的类并针对那个。
Juno保持特异性低且平坦 低且平坦意味着每条规则的得分接近相同的小权重,所以源顺序单独可以解决任何冲突。Id 坐在类上方一个数量级,只有 !important 或另一个 id 战胜它们,所以保留它们用于片段链接和 JS 钩。深链既提高特异性又将规则焊接到一个 HTML 形状,这就是直接命名元素而不是通过祖先搜索它的原因。

命名约定

一旦你用类样式化,下一个问题是如何命名它们。像 .blue.thing2 这样的名字很快就会失效,因为它们没有说明类的目的。一个**命名约定**是一个约定的命名类的方式,使得名字告诉你类做什么以及它属于哪里。

广泛使用的叫做 BEM,代表块、元素、修饰符。一个块是像卡这样的组件。一个元素是其中的一个部分,用两个下划线写。一个修饰符是一个变化,用两个破折号写:

以类为样式单元,命名成为保持它们组织的东西。一个**命名约定**是类名的共享模式,它的工作是使名字可预测和无冲突的:你可以从类名告诉什么组件它属于以及什么部分它样式化,两个组件永远不会意外地重用相同的名字。

最广泛采用的约定是 BEM(块、元素、修饰符)。块是组件(.card),一个元素是它的一部分,用两个下划线连接(.card__title),修饰符是一个变化,用两个破折号连接(.card--featured)。好处是平坦的特异性:因为每个部分得到它自己的单个类,你永远不需要一个后代链来到达 .card__title,所以 BEM 和低且平坦的习惯彼此强化。

一个**命名约定**替代了语言缺失的作用域。通过将组件边界编码到类名本身中,它给你无冲突、自文档化的名字,不需要任何语言特性:读一个类,你知道它的组件、它的部分和它的变化。任何一致的约定都提供这个;价值在于一致性,不是精确的标点。

BEM(块、元素、修饰符)是最广泛使用的。块命名组件(.card),一个元素用双下划线命名一个部分(.card__title),修饰符用双破折号命名一个变化(.card--featured)。使 BEM 发挥其作用超出了命名的东西是它通过构造保持特异性平坦:每个元素得到它自己的单类选择器,所以你直接针对 .card__title 而不是写 .card .title,组件中的每条规则都坐在相同的权重上。那是前一部分的相同低且平坦的性质,现在从命名方案自由掉落。代价是 HTML 中的详细类列表,大多数团队接受这个权衡以换取它购买的可预测性。

css
/* 块:组件本身 */
.card { }

/* 元素:块的一部分,两个下划线 */
.card__title { }
.card__body { }

/* 修饰符:块的变化,两个破折号 */
.card--featured { }
html
<article class="card card--featured">
  <h2 class="card__title">周末工坊</h2>
  <p class="card__body">一个简短的布局介绍。</p>
</article>
Juno命名约定 命名约定是一个约定的命名类的方式,使得名字告诉你它的目的。BEM 是常见的:像 .card 的块,像 .card__title 的其中的元素带两个下划线,像 .card--featured 的变化带两个破折号。你不必使用 BEM,但选择某个一致的方案并坚持它。
Juno命名约定 约定使类名可预测和无冲突,所以一个名字告诉你它的组件和它的部分。BEM 是广泛使用的:块 .card,元素 .card__title,修饰符 .card--featured。它也保持特异性平坦,因为每个部分得到它自己的单类而不是后代链。
Juno命名约定 约定代替了 CSS 永远没给你的作用域:组件边界住在类名中,所以名字保持无冲突和自文档化。BEM 将块、元素和修饰符编码为 .card.card__title.card--featured,它真正的回报是通过构造的平坦特异性,因为每个部分是单个类。代价是标记中的详细类列表,这通常值得可预测性。

结构化文件

随着样式表增长,一个长文件变得难以移动。常见的修复是将 CSS 分成几个文件夹,按照规则做什么:**基础**样式(body 和标题之类的普通元素的默认值)、组件(卡、按钮和其他片段)和实用程序(像间距或文本对齐这样的微小单一目的帮手)。

当你分割文件时,一个细节很重要:你加载它们的顺序。当两条规则具有相同的特异性时,后来的一条胜出,所以稍后加载的样式表可以覆盖更早的一个。从最通用加载到最具体:

将 CSS 按角色分成文件夹使增长的代码库可以导航。常见的结构是三组:基础(重置和元素默认值如 body、标题和链接)、组件(自包含的片段如 .card.btn)、实用程序(单目的帮手如 .text-center.mt-4)。

你连接或导入这些文件的顺序不是表面的,因为源顺序是级联平手破坏者:当两条规则具有相等的特异性时,后来的一条胜出。所以你从最不具体加载到最具体,基础优先,然后组件,然后实用程序最后,所以实用程序可以覆盖组件,组件可以覆盖基础默认,都不需要任何人提高特异性来强制它。让顺序正确是什么让你保持一切在单类特异性并且仍然有覆盖落在你期望的地方。

按角色结构化文件是你在规模上保持低且平坦 CSS 可以导航的方式,加载顺序是承重的。常见的分割是**基础**(重置和裸元素默认值)、组件(封装的片段,每个文件一个)和实用程序(原子单属性帮手)。排序规则直接从级联追随:有意地保持特异性平坦,源顺序成为主要平手破坏者,所以文件加载的序列决定哪个等重规则胜出。

这就是为什么顺序从最不具体运行到最具体在意图上,基础、然后组件、然后实用程序,所以后面的组可以覆盖更早的组,不需要任何特异性提升。一个 .text-center 实用程序必须战胜组件自己的文本对齐,它可以,纯粹因为它加载最后在相同权重。这工作,但它是一个由纪律执行的约定:语言中没有什么阻止某人在组件之前导入实用程序并悄悄倒转整个方案。跨一个大团队依赖源顺序对于完全那个原因是脆弱的,这正是什么激励使排序显式而不是位置的,最后一部分的主题。

css
/* main.css:顺序从最不具体运行到最具体 */
@import "base/reset.css";        /* 元素默认值 */
@import "base/typography.css";

@import "components/card.css";    /* 自包含的片段 */
@import "components/button.css";

@import "utilities/spacing.css";  /* 加载最后所以它们可以覆盖 */
@import "utilities/text.css";
Juno结构化文件 将 CSS 按规则做什么分成文件夹:基础用于元素默认值,组件用于卡和按钮之类的片段,实用程序用于微小的帮手。然后注意你加载它们的顺序,因为当特异性打结时,稍后的规则胜出。加载通用优先和具体最后,所以帮手可以覆盖片段。
Juno结构化文件 按角色分组文件:基础、组件、然后实用程序。从最不具体加载到最具体,因为源顺序是特异性相等时的平手破坏者,所以最后加载的实用程序可以不需要任何特异性提升地覆盖组件。让顺序正确是什么让你保持一切在单类权重并且仍然有覆盖落在。
Juno结构化文件 一旦特异性有意地平坦,源顺序成为主要平手破坏者,所以文件加载顺序决定哪个等重规则胜出。基础、然后组件、然后实用程序意味着后面的组覆盖更早的组免费,不需要特异性提升。陷阱是它是由纪律保持的约定:导入实用程序太早,你倒转整个方案,这正是下一步使排序显式的原因。

使级联显式

到目前为止的一切通过约定保持级联可管理:低特异性、好名字、谨慎的文件顺序。在你走得更深时有更多东西在这里,值得知道旅行的方向。现代 CSS 让你直接控制排序而不是依赖文件碰巧坐在哪里,它给你方式使级联可预测,当更多人在同一样式表上工作。你会在项目增长时遇到这些工具,上面的习惯正是什么为它们做准备。

迄今为止的技术通过特异性和源顺序间接管理级联。更新的 CSS 让你直接管理它。一个**级联层**,用 @layer 规则写,是一个命名的排序桶:你声明层前期以你想让它们胜出的顺序,每条在一层内的规则战胜在早期层中的每条规则,不管特异性。

css
/* 声明一次获胜顺序,最亏损到最获胜 */
@layer base, components, utilities;

@layer components {
  .card { padding: 16px; }
}

@layer utilities {
  .p-0 { padding: 0; }   /* 胜出 .card 尽管特异性相等 */
}

因为一个后期层总是胜出,你不再需要文件顺序完美,并且你很少需要 !important 来强制覆盖。那是实际好处:层移动排序出脆弱的"哪个文件加载优先"领地,进一个显式声明每个人都可以读。

源顺序是脆弱的,因为它是位置的:它工作直到某人重新排序导入。级联层@layer 规则,用坐在特异性之上的显式排序替代那个。你声明层顺序一次,级联查询层顺序在它曾看特异性之前,所以一个在后期层中的规则战胜一个在早期层中的规则,甚至当早期规则更具体时。排序成为一个命名、可读的决定而不是文件位置的副作用。

css
/* 一行为整个代码库修复获胜顺序 */
@layer reset, base, components, utilities;

@layer components {
  .card__title { font-size: 1.25rem; }
}

@layer utilities {
  .text-lg { font-size: 1.5rem; }   /* 胜出:utilities 是后期层 */
}

这重新塑造两个习惯。首先,它排除特异性战争:因为层顺序击败特异性,你停止为了赢得冲突而到达更具体的选择器,并且你几乎永远不需要 !important,其全部工作是逃脱你无法以其他方式战胜的特异性。(层甚至驯服 !important 本身,反转它的优先顺序,所以 早期层中的重要声明胜出,但目标是需要它是的很少,这保持琐碎。)其次,它澄清了长期运行在两个组织样式之间的选择:一个实用程序优先方法,从许多原子单属性类组合页面,和一个组件方法,在一个语义类后面每片打包样式。大多生产代码库都运行两个,层让它们通过设计共存:把组件放在一个 components 层,实用程序放在后期 utilities 层,一个实用程序可靠地覆盖组件,不需要任何一方提高特异性。决定停止"哪一个赢级联战斗"并成为"哪一个为这部分 UI 读得更好",因为级联由层顺序解决,不是谁写了更强推的选择器。那可预测性是真正的奖品,当团队增长时:获胜顺序是一个声明每个人都可以读,不是关于导入序列的部落知识。当你需要像颜色和间距这样的共享值跨这些层,自定义属性让你定义它们一次而不是复制它们。

Juno使级联显式 到目前为止的一切用习惯驯服级联:低特异性、清晰的名字、谨慎的加载顺序。随着你走得更远,CSS 给你工具直接设置获胜顺序而不是依赖哪个文件加载优先。你不需要它们还,这章中的习惯正是什么使你为它们准备。
Juno使级联显式 一个带 @layer 的级联层是一个命名的排序桶:声明层顺序前期,后期层战胜早期层,不管特异性。那意味着文件顺序停止不得不完美,并且你很少需要 !important 来强制胜利。它移动排序出脆弱的文件位置进一行每个人都可以读。
Juno使级联显式 级联层坐在特异性之上,所以后期层赢甚至对抗更具体的规则,它释放特异性棘轮,并退出你的大多 !important 使用。它们也让实用程序优先和组件样式共存:组件在一层,实用程序在后期,一个实用程序覆盖组件,不需要任何一方提高。赢当团队增长时是排序是一个可读的声明,不是关于导入序列的部落知识。