为什么你的 CSS 总是杂乱无章——理解 CSS 的复杂性
想到要在大型前端项目中进行细微的样式更新,你会犹豫吗?对我来说确实如此。尤其是当我修改另一个团队编写的项目时,即使改动很小,也会感到特别困难。
不知何故,CSS 样式表(无论是否预处理)总是充斥着一堆乱七八糟的类和 ID。为什么在同一个代码库中,既有干净的 JS/Python/PHP 代码,又有意大利面条式的 CSS 代码?
本周,我想借用 John Ousterhout 的杰作《软件设计哲学》中关于软件复杂性的本体论,来探究 CSS 的复杂性 。这篇文章将解释 CSS 为何如此混乱——下周我们将深入探讨设计模式及其解决方案。
什么是复杂性?
我们先来了解一下复杂性的定义。 《软件设计哲学》中有一句话:“复杂性是指任何与软件系统结构相关的因素,它使得系统难以理解和修改。”
这个定义非常适合 CSS。复杂的 CSS 难以理解,也更难修改——强制 链接到 CSS 凳子笑话。
Ousterhout 还指出了三种导致代码难以处理的复杂性症状:
变革放大
当一个看似简单的变化需要多个地方进行改变时,就会发生变化放大。
以 CSS 领域为例: 你的任务是将网站上所有段落文本的颜色从黑色改为深蓝色。在一个简单的 HTML 页面上,只需一行代码即可完成 p { color: navy; }
。在一个大型项目中,你花了一整天时间去修改所有页面上所有元素的最高优先级 <p>
,结果却发现结账小工具的发货确认模式中有一个段落元素,不知何故继承了蓝绿色标题的文本颜色。
认知负荷
认知负荷 是开发人员为了做出改变而需要记住的信息量。
CSS 领域示例: 您需要将 font-size
大视口中英雄版块 CTA 按钮的宽度从 20px 更改为 22px。以下是您可能需要注意的信息列表:
font-size
来自用户代理样式表的默认主体 。- 适用于小视口和中视口的现有按钮
font-size
。 font-size
英雄部分文本,因为该em
单位看起来很合适。- 也许我们
rem
改用,项目是否使用标准16px
rem尺寸或10px
rem尺寸以便于计算? - 按钮及其的潜在英雄部分变化
font-size
。 font-size
项目可能具有的任何 实用程序类。
未知的未知数
当您不清楚从哪里获取完成任务所需的信息或在哪里进行必要的更改时,您就会面临 未知的未知数。
CSS 领域示例:<span>
您正在更改 类别页面上登录小部件中 某个元素的错误消息颜色 。您是在utilities.css
、 form.css
、 widget.css
、 login.css
、 category.css
还是 中进行更改index.css
?根据您项目的 CSS 方法,这可能是组件变体、新组件或新的实用程序类。
为什么 CSS 变得复杂
接下来,我们来看看造成 CSS 复杂性的不同技术原因。
选择器空间
CSS 属性每次只属于一个匹配元素,而 CSS 选择器则会匹配 DOM 中的所有元素。因此,随着项目规模的扩大以及选择器总数(包括实际的和潜在的)的增长,CSS 的复杂性自然也会增加。
DOM 中的 CSS 属性总数随 DOM 大小线性增长 - 一个页面的 n
元素总数为 x
每个元素的属性最多为 nx
100 个可调整属性。
另一方面,潜在选择器集合的大小会随着 DOM 深度的增加而呈指数增长。假设每个元素只使用一个类,那么一个 m
深度 DOM 元素就会有 2^m
多个可用的基于类的选择器。
由于大型项目中潜在选择器的总数呈指数增长,它会影响认知负荷和变化放大——理解和修改其实施的子集。
如果 您想知道为什么前端开发人员花费大量时间修改和咒骂 CSS 选择器,那是因为任何给定更改的选择器解决方案空间都是巨大的。
特异性
以我的经验来看,没有什么比“权重升级”更能快速导致 CSS 变得难以管理了。由于任何中等复杂度的项目每次修改都会产生大量潜在的选择器,而且 CSS 选择器权重得分本质上是可加的,因此项目的平均 CSS 选择器权重会随着项目规模的扩大而增加。
正如 Chris Coyier 在关于CSS 代码异味的回答中指出的那样 ,当您在开发工具中看到类似的选择器时 #articles .comments ul > li > a.button
,已经太晚了。
与特异性相关的复杂性在这三个类别中都有体现。我们已经讨论过在变更放大的情况下创建更具体的 CSS 选择器的必要性——最终涵盖 id 选择器、 !important
和内联样式。
特异性升级还意味着阅读和修改 CSS 的开发人员现在需要在工作时在脑海中保留 DOM 的详细心理模型 - 它 .comments
在里面 #articles
还是 .comments
在 ul
里面 #articles
?
最后,选择器长度的不断增加意味着样式变更可能所属的位置数量也会线性增加。在上面的选择器示例中,变更应该放在 articles.css
、 中comments.css
, global.css
还是直接使用“在项目中查找”?如果您使用支持嵌套选择器的预处理器,情况又会如何呢?
源顺序
CSS 源码顺序使我们能够一致地预测当多个选择器具有相同的权重分数时,样式的解析方式。它还可以处理在同一作用域内为同一属性声明多个值的情况——这种情况下, CSS 简写方式的使用会非常棘手。
规则很简单,最后出现的值获胜。
很简单,对吧?其实不然。这意味着我们所做的每项更改都可能受到所有其他具有相同权重的选择器的影响。你的 选择器.nav .button
也可能受到影响 .header .button
。事实上,你需要注意每个选择器匹配的所有元素集合的交集,这些元素的权重分数与你正在处理的元素相同。我甚至无法一次性记住整个 DOM,更不用说所有样式表匹配的子集了。不难看出,这会造成巨大的认知负担,并且会随着项目规模的扩大而增长。
在大型项目中,每个页面包含多个样式表(无论是纯 CSS 还是通过预处理器导入)时,源代码顺序是一个更大的问题。假设您要将以下基于 BEM 的 DOM 结构中的 span 颜色更改为绿色:
<div class="widget">
<div class="widget__content">
<div class="media-box">
<span class="media-box__label">Change Me</span>
</div>
</div>
</div>
和 CSS 文件:
/* widget.css */
.widget .widget__content {
color: red;
}
/* media.css */
.media .media__label {
color: blue;
}
如果样式表导入的顺序无法保证,你会修改哪个文件?这显然是“未知的未知”现象,而且它会随着项目文件大小线性增长。
你会为了“安全起见”而同时改变两者,从而扩大变化范围吗?还是你会撰写文章 .widget .widget__content .media-box
,并进行细节升级?
遗产
CSS 继承控制着某些 CSS 属性(如果未指定)如何从其父元素继承值。一些常见的继承属性包括 color
、 font-size
和 line-height
。
继承不像迄今为止讨论的其他问题那样麻烦。它只是一个抽象层,对变更放大的影响很小,也不会产生未知的未知数。它会导致额外的认知负荷,即使这些问题大多是通过浏览器发现的——你能想象下面例子中的“hello world”是什么样子吗?
<div class="a">
<div class="b">
<div class="c">
<div class="d">
<p>Hello World</p>
</div>
</div>
</div>
</div>
.a {
color: blue;
font-size: 14px;
letter-spacing: 2px;
line-height: 3;
}
.b {
font-size: 12px;
font-weight: 800;
letter-spacing: -1px;
}
.c {
color: teal;
font-weight: 600;
font-family: 'Serif'
}
.d {
font-size: 18px;
font-family: 'arial'
}
最后的话
我希望我已经说服了你,CSS 本质上是复杂的,并且它的复杂性会随着项目规模的扩大而呈超线性增长。
下次当您对设计变更感到沮丧,觉得它花费的时间比预期的要长时,请知道这可能是由项目的 CSS 复杂性造成的。
下次您在代码审查中看到长选择器时,请将其调出并扼杀特异性升级。
如果你正在开始一个新项目,我希望这篇文章能进一步说服你采用一种久经考验的 CSS 方法论,并严格执行——真正地执行它。没有必要把 CSS 弄得比它应该的更复杂。
下周,我将介绍一些历史上成功的对抗 CSS 复杂性的模式、它们的权衡以及使用它们的方法。
如果您觉得这篇文章有帮助,请分享给其他人。您也可以关注我@itstrueintheory或在 Twitter 上关注我 @itstrueintheory,获取最新的博客更新。
链接:https://dev.to/itstrueintheory/why-your-css-is-always-messy-and-chaotic-understanding-css-complexity-3o94