维护大型CSS项目很困难。多年来,我们见证了旨在简化编写可扩展CSS过程的不同方法。最终,我们都试图实现以下两个目标
- 效率:我们希望减少花费在思考如何做事的时间,并增加实际做事的时间。
- 一致性:我们希望确保所有开发人员都步调一致。
在过去一年半的时间里,我一直在开发一个 组件库 和一个名为 CodyFrame 的前端框架。我们目前拥有220多个组件。这些组件不是孤立的模块:它们是可重用的模式,通常相互合并以创建复杂的模板。
这个项目的挑战迫使我们的团队开发了一种构建可扩展CSS架构的方法。这种方法依赖于CSS全局样式、BEM和实用类。
我很乐意分享它!👇
30秒了解CSS全局样式
全局样式是包含适用于所有组件的规则的CSS文件(例如,间距比例、排版比例、颜色等)。全局样式使用令牌来保持所有组件之间设计的一致性,并减少其CSS的大小。
这是一个排版全局规则的示例
/* Typography | Global */
:root {
/* body font size */
--text-base-size: 1em;
/* type scale */
--text-scale-ratio: 1.2;
--text-xs: calc((--text-base-size / var(--text-scale-ratio)) / var(--text-scale-ratio));
--text-sm: calc(var(--text-xs) * var(--text-scale-ratio));
--text-md: calc(var(--text-sm) * var(--text-scale-ratio) * var(--text-scale-ratio));
--text-lg: calc(var(--text-md) * var(--text-scale-ratio));
--text-xl: calc(var(--text-lg) * var(--text-scale-ratio));
--text-xxl: calc(var(--text-xl) * var(--text-scale-ratio));
}
@media (min-width: 64rem) { /* responsive decision applied to all text elements */
:root {
--text-base-size: 1.25em;
--text-scale-ratio: 1.25;
}
}
h1, .text-xxl { font-size: var(--text-xxl, 2.074em); }
h2, .text-xl { font-size: var(--text-xl, 1.728em); }
h3, .text-lg { font-size: var(--text-lg, 1.44em); }
h4, .text-md { font-size: var(--text-md, 1.2em); }
.text-base { font-size: --text-base-size; }
small, .text-sm { font-size: var(--text-sm, 0.833em); }
.text-xs { font-size: var(--text-xs, 0.694em); }
30秒了解BEM
BEM(块、元素、修饰符)是一种旨在创建可重用组件的命名方法。
这是一个示例
<header class="header">
<a href="#0" class="header__logo"><!-- ... --></a>
<nav class="header__nav">
<ul>
<li><a href="#0" class="header__link header__link--active">Homepage</a></li>
<li><a href="#0" class="header__link">About</a></li>
<li><a href="#0" class="header__link">Contact</a></li>
</ul>
</nav>
</header>
- 一个块是一个可重用组件
- 一个元素是块的子元素(例如,
.block__element
) - 一个修饰符是块/元素的变体(例如,
.block--modifier
,.block__element--modifier
)。
30秒了解实用类
实用类是一种只做一件事的CSS类。例如
<section class="padding-md">
<h1>Title</h1>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
</section>
<style>
.padding-sm { padding: 0.75em; }
.padding-md { padding: 1.25em; }
.padding-lg { padding: 2em; }
</style>
您可以使用实用类构建整个组件
<article class="padding-md bg radius-md shadow-md">
<h1 class="text-lg color-contrast-higher">Title</h1>
<p class="text-sm color-contrast-medium">Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
</article>
您可以将实用类连接到CSS全局样式
/* Spacing | Global */
:root {
--space-unit: 1em;
--space-xs: calc(0.5 * var(--space-unit));
--space-sm: calc(0.75 * var(--space-unit));
--space-md: calc(1.25 * var(--space-unit));
--space-lg: calc(2 * var(--space-unit));
--space-xl: calc(3.25 * var(--space-unit));
}
/* responsive rule affecting all spacing variables */
@media (min-width: 64rem) {
:root {
--space-unit: 1.25em; /* 👇 this responsive decision affects all margins and paddings */
}
}
/* margin and padding util classes - apply spacing variables */
.margin-xs { margin: var(--space-xs); }
.margin-sm { margin: var(--space-sm); }
.margin-md { margin: var(--space-md); }
.margin-lg { margin: var(--space-lg); }
.margin-xl { margin: var(--space-xl); }
.padding-xs { padding: var(--space-xs); }
.padding-sm { padding: var(--space-sm); }
.padding-md { padding: var(--space-md); }
.padding-lg { padding: var(--space-lg); }
.padding-xl { padding: var(--space-xl); }
一个真实案例
使用基本示例解释方法并不能揭示实际问题,也不能体现该方法本身的优势。
让我们一起构建一些东西!
我们将创建一个卡片元素的画廊。首先,我们只使用BEM方法来实现,并指出仅使用BEM可能面临的问题。接下来,我们将了解全局样式如何减少CSS的大小。最后,我们将通过引入实用类来使组件可定制。
这是最终结果的预览
让我们从只使用BEM创建画廊开始这个实验
<div class="grid">
<article class="card">
<a class="card__link" href="#0">
<figure>
<img class="card__img" src="/image.jpg" alt="Image description">
</figure>
<div class="card__content">
<h1 class="card__title-wrapper"><span class="card__title">Title of the card</span></h1>
<p class="card__description">Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?</p>
</div>
<div class="card__icon-wrapper" aria-hidden="true">
<svg class="card__icon" viewBox="0 0 24 24"><!-- icon --></svg>
</div>
</a>
</article>
<article class="card"><!-- card --></article>
<article class="card"><!-- card --></article>
<article class="card"><!-- card --></article>
</div>
在这个示例中,我们有两个组件:.grid
和 .card
。第一个用于创建画廊布局。第二个是卡片组件。
首先,我想指出使用BEM的主要优势:低特异性和作用域。
/* without BEM */
.grid {}
.card {}
.card > a {}
.card img {}
.card-content {}
.card .title {}
.card .description {}
/* with BEM */
.grid {}
.card {}
.card__link {}
.card__img {}
.card__content {}
.card__title {}
.card__description {}
如果您不使用BEM(或类似的命名方法),最终会创建继承关系(.card > a
)。
/* without BEM */
.card > a.active {} /* high specificity */
/* without BEM, when things go really bad */
div.container main .card.is-featured > a.active {} /* good luck with that 😦 */
/* with BEM */
.card__link--active {} /* low specificity */
在大型项目中处理继承和特异性很痛苦。当您的CSS似乎不起作用,并且您发现它被另一个类覆盖时,那种感觉😡!另一方面,BEM为您的组件创建了一种作用域,并保持了较低的特异性。
但是……仅使用BEM有两个主要缺点:
- 命名太多东西令人沮丧
- 进行细微的自定义并不容易或难以维护
在我们的示例中,为了设置组件的样式,我们创建了以下类
.grid {}
.card {}
.card__link {}
.card__img {}
.card__content {}
.card__title-wrapper {}
.card__title {}
.card__description {}
.card__icon-wrapper {}
.card__icon {}
类的数量不是问题。问题在于想出如此多的有意义的名称(并让所有团队成员使用相同的命名标准)。
例如,假设您必须修改卡片组件,并在其中包含一个额外的较小的段落
<div class="card__content">
<h1 class="card__title-wrapper"><span class="card__title">Title of the card</span></h1>
<p class="card__description">Lorem ipsum dolor...</p>
<p class="card__description card__description--small">Lorem ipsum dolor...</p> <!-- 👈 -->
</div>
您如何称呼它?您可以将其视为.card__description
元素的变体,并使用.card__description .card__description--small
。或者,您可以创建一个新元素,例如.card__small, .card__small-p
或.card__tag
。明白我的意思了吗?没有人想花时间思考类名。只要您不必命名太多东西,BEM就非常棒。
第二个问题是处理细微的自定义。例如,假设您必须创建一个卡片组件的变体,其中文本居中对齐。
您可能会这样做
<div class="card__content card__content--center"> <!-- 👈 -->
<h1 class="card__title-wrapper"><span class="card__title">Title of the card</span></h1>
<p class="card__description">Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?</p>
</div>
<style>
.card__content--center { text-align: center; }
</style>
您的一个团队成员在处理另一个组件(.banner
)时也遇到了同样的问题。他们也为其组件创建了一个变体
<div class="banner banner--text-center"></div>
<style>
.banner--text-center { text-align: center; }
</style>
现在假设您必须将横幅组件包含到页面中。您需要文本居中对齐的变体。在不检查横幅组件的CSS的情况下,您可能会本能地写banner banner--center
到您的HTML中,因为在创建文本居中对齐的变体时,您总是使用--center
。不起作用!您唯一的选择是打开横幅组件的CSS文件,检查代码,并找出应该应用哪个类才能使文本居中对齐。
需要多长时间,5分钟?将5分钟乘以您和所有团队成员每天发生这种情况的次数,您就会意识到浪费了多少时间。此外,添加执行相同操作的新类会导致CSS膨胀。
CSS全局样式和实用类来拯救
设置全局样式的第一个好处是有一组应用于所有组件的CSS规则。
例如,如果我们在间距和排版全局样式中设置响应式规则,这些规则也将影响网格和卡片组件。在CodyFrame中,我们在特定断点处增加正文字体大小;因为我们对所有边距和填充使用“em”单位,所以整个间距系统会立即更新,从而产生 级联效果。

因此,在大多数情况下,您无需使用媒体查询来增加字体大小或边距和填充的值!
/* without globals */
.card { padding: 1em; }
@media (min-width: 48rem) {
.card { padding: 2em; }
.card__content { font-size: 1.25em; }
}
/* with globals (responsive rules intrinsically applied) */
.card { padding: var(--space-md); }
不仅如此!您可以使用全局样式来存储可以与所有其他组件组合的行为组件。例如,在CodyFrame中,我们定义了一个 .text-component
类用作“文本包装器”。它负责行高、垂直间距、基本样式和其他内容。
如果我们回到我们的卡片示例,.card__content
元素可以用以下内容替换
<!-- without globals -->
<div class="card__content">
<h1 class="card__title-wrapper"><span class="card__title">Title of the card</span></h1>
<p class="card__description">Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?</p>
</div>
<!-- with globals -->
<div class="text-component">
<h1 class="text-lg"><span class="card__title">Title of the card</span></h1>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?</p>
</div>
文本组件将负责文本格式,并使项目中所有文本块保持一致。此外,我们已经消除了几个BEM类。
最后,让我们将实用类引入组合中!
如果您希望能够以后自定义组件而不必检查其CSS,则实用类特别有用。
如果我们将一些BEM类替换为实用类,则卡片组件的结构将发生如下变化
<article class="card radius-lg">
<a href="#0" class="block color-inherit text-decoration-none">
<figure>
<img class="block width-100%" src="image.jpg" alt="Image description">
</figure>
<div class="text-component padding-md">
<h1 class="text-lg"><span class="card__title">Title of the card</span></h1>
<p class="color-contrast-medium">Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?</p>
</div>
<div class="card__icon-wrapper" aria-hidden="true">
<svg class="icon icon--sm color-white" viewBox="0 0 24 24"><!-- icon --></svg>
</div>
</a>
</article>
BEM(组件)类的数量已从9个减少到3个
.card {}
.card__title {}
.card__icon-wrapper {}
这意味着您无需过多地处理命名。也就是说,我们无法完全避免命名问题:即使您使用实用类创建Vue/React/其他框架组件,您仍然需要命名组件。
所有其他 BEM 类已被实用程序类替换。如果您需要制作一个标题更大的卡片变体怎么办?将 text-lg 替换为 text-xl。如果要更改图标颜色怎么办?将 color-white 替换为 color-primary。如何将文本居中对齐?将 text-center 添加到文本组件元素中。减少思考时间,增加行动时间!
为什么我们不只使用实用程序类呢?
实用程序类可以加快设计流程,并使自定义操作更容易。那么,为什么我们不忘记 BEM,只使用实用程序类呢?主要有两个原因。
通过将 BEM 与实用程序类一起使用,HTML 更易于阅读和自定义。
使用 BEM 用于
- 使 HTML 中不打算自定义的 CSS 代码干燥(例如,类似于行为的 CSS 过渡、定位、悬停/焦点效果),
- 高级动画/效果。
使用实用程序类用于
- “经常自定义”的属性,通常用于创建组件变体(如填充、边距、文本对齐等),
- 难以使用新的有意义的类名识别的元素(例如,您需要一个具有
position: relative
的父元素 → 创建<div class="position-relative"><div class="my-component"></div></div>
)。
示例:
<!-- use only Utility classes -->
<article class="position-relative overflow-hidden bg radius-lg transition-all duration-300 hover:shadow-md col-6@sm col-4@md">
<!-- card content -->
</article>
<!-- use BEM + Utility classes -->
<article class="card radius-lg col-6@sm col-4@md">
<!-- card content -->
</article>
由于这些原因,我们建议您不要将 !important 规则添加到您的实用程序类中。使用实用程序类不必像使用锤子一样。您认为在 HTML 中访问和修改 CSS 属性是否有益?使用实用程序类。您是否需要一堆不需要编辑的规则?在您的 CSS 中编写它们。此过程不必在您第一次执行时就完美无缺:如果需要,您可以在以后调整组件。听起来“不得不决定”很费力,但当您付诸实践时,它非常简单明了。
在创建独特的特效/动画方面,实用程序类并不是您的最佳盟友。
考虑使用伪元素,或制作需要自定义贝塞尔曲线的独特运动效果。对于这些,您仍然需要打开您的 CSS 文件。
例如,考虑一下我们设计的卡片的动画背景效果。使用实用程序类创建这样的效果有多难?
图标动画也是如此,它需要动画关键帧才能工作。
.card:hover .card__title {
background-size: 100% 100%;
}
.card:hover .card__icon-wrapper .icon {
animation: card-icon-animation .3s;
}
.card__title {
background-image: linear-gradient(transparent 50%, alpha(var(--color-primary), 0.2) 50%);
background-repeat: no-repeat;
background-position: left center;
background-size: 0% 100%;
transition: background .3s;
}
.card__icon-wrapper {
position: absolute;
top: 0;
right: 0;
width: 3em;
height: 3em;
background-color: alpha(var(--color-black), 0.85);
border-bottom-left-radius: var(--radius-lg);
display: flex;
justify-content: center;
align-items: center;
}
@keyframes card-icon-animation {
0%, 100% {
opacity: 1;
transform: translateX(0%);
}
50% {
opacity: 0;
transform: translateX(100%);
}
51% {
opacity: 0;
transform: translateX(-100%);
}
}
最终结果
这是卡片画廊的最终版本。它还包括网格实用程序类以自定义布局。
文件结构
以下是使用本文中描述的方法构建的项目的结构示例。
project/
└── main/
├── assets/
│ ├── css/
│ │ ├── components/
│ │ │ ├── _card.scss
│ │ │ ├── _footer.scss
│ │ │ └── _header.scss
│ │ ├── globals/
│ │ │ ├── _accessibility.scss
│ │ │ ├── _breakpoints.scss
│ │ │ ├── _buttons.scss
│ │ │ ├── _colors.scss
│ │ │ ├── _forms.scss
│ │ │ ├── _grid-layout.scss
│ │ │ ├── _icons.scss
│ │ │ ├── _reset.scss
│ │ │ ├── _spacing.scss
│ │ │ ├── _typography.scss
│ │ │ ├── _util.scss
│ │ │ ├── _visibility.scss
│ │ │ └── _z-index.scss
│ │ ├── _globals.scss
│ │ ├── style.css
│ │ └── style.scss
│ └── js/
│ ├── components/
│ │ └── _header.js
│ └── util.js
└── index.html
您可以将每个组件的 CSS(或 SCSS)存储到一个单独的文件中(并且,可以选择使用 PostCSS 插件将每个新的 /component/componentName.css
文件编译到 style.css
中)。您可以随意组织全局样式,您也可以创建一个 globals.css
文件,避免将全局样式分离到不同的文件中。
结论
如果您希望在几个月后打开文件并且不会迷路,那么处理大型项目需要一个可靠的架构。有很多方法可以解决这个问题(CSS-in-JS、实用优先、原子设计等)。
我今天与您分享的方法依赖于创建交叉规则(全局样式),使用实用程序类进行快速开发,以及使用 BEM 进行模块化(行为)类。
您可以在 CodyHouse 上详细了解此方法。欢迎任何反馈!
自从我发现 Tailwind 以来,我一直完全支持实用程序类。我认为您对是什么以及为什么的评估非常好。它没有考虑到与 BEM 的结合,但这是一个有趣的思路。
谢谢,Doug!
好文章,谢谢!我使用这种方法已经有一段时间了。我混合使用 ABEM 和 Bootstrap 4 实用程序类,发现它是一个相当不错的组合 :)
谢谢!ABEM 也很棒。
我也是!并且我像作者一样偶然发现了这种模式,使用 BEM 并厌倦了使用相同的修饰符命名事物,然后将其提取到一个实用程序类中。
我倾向于使用实用程序混入,因为我想避免 Bootstrap 地狱,以检查元素内部的大量类,但仍然保持某种集中控制点。
好的,最终 CSS 会稍微重一些(由于 gzip 压缩,不会重太多),但我实际上可以在不知道它在哪里的情况下对 HTML 进行样式设置,就是这样。
有趣!我想您正在使用诸如 .foo { @include radius; @include padding-top;} 之类的东西以便在单独的 _mixins.scss 文件中控制所有内容。即使我从未使用过这种方法,我也可以看到走这条路有很多好处。感谢分享。
我喜欢这种方法。
https://npmjs.net.cn/package/@michu1234/goel?activeTab=readme
我使用类似的方法:https://www.xfive.co/blog/itcss-scalable-maintainable-css-architecture/
我还为每个类添加前缀以表示其类型。例如,c-card 和 g-radius-lg。
它对两个方面有益
1. 如果某个不了解框架的人创建了一个名为 card 的类,它不会覆盖您的 c-card。
2. c-card g-radius-lg g-col-6@sm g-col-4@md -> 更容易知道什么是组件和全局样式。
这大致是我得出的相同结论!首先,我使用类和通用标签选择器对内容进行样式设置,您知道,通常的做法。
但是,如果您在该卡片中还有另一个段落,则这些第一个规则会将其弄乱,因此您将其命名为 .card-text 等。
在使用 Bootstrap 一段时间后,然后想学习更多内容,我偶然发现了 BEM 并开始越来越多地采用 BEM。不幸的是,一开始我试图尽可能多地使用 BEM,并且可能只有 5-6 个实用程序类。
但后来随着我获得更多经验,我意识到如果我仅依赖 BEM,它对我不起作用,因此我决定尝试添加更多实用程序类到组合中。
最后,我意识到将 BEM 用于大部分工作并将实用程序类用于一些常见的样式(如排版、间距、可能的颜色和其他一些内容)使我能够更快、更高效地工作,尤其是在我必须在某些应用程序中创建新屏幕时。间距(填充/边距)实用程序类在此处非常有助于快速原型设计和建立一致的布局。
因此,我一段时间以来一直在使用 BEM + 实用程序类。虽然不像您此处设置的所有全局样式和所有内容那样,但我找到了适合我的设置。
不幸的是,设计师并不总是使所有东西都超级一致,而这正是实用类帮助我很多的一个领域。我相信我们都遇到过设计师只是要求你“移除这里的边距”或“这段文字需要用主色突出显示”的情况,而使用实用类,这种更改就变得轻而易举了。
我只是喜欢这种方法赋予我的灵活性。
全局上,这里采用了类似的方法:所有内容都使用BEM,并在此处和彼处使用一些实用类(我能想到的最小的例子在Can We中)。对我来说,这是强制执行HTML和CSS的可维护性的最佳方法。
我开始走BEM的道路,并且已经使用实用类一段时间了。我想知道是否坚持使用BEM的双破折号修饰符约定作为实用类是一个好主意?例如,对于像.color-attention或.text-lg这样的类,将其命名为.–color-attention或.–text-lg。
它们本质上是修饰符,并且会坚持BEM约定。有什么想法吗?
使用此方法的先决条件是什么?我假设需要使用样式指南或已有的系统设计。