CSS组合,到底是啥?为啥大佬们总说它比继承香?

3,114字
13–20 分钟
in

把设计页面这事儿比作做饭,那组合就是准备一堆现成的菜码和调料,需要啥就往上搁啥。很多人觉得Tailwind那套在标签里堆一堆p-4border-2的玩法就是组合的终极形态,其实这玩意儿就跟直接把盐、糖、酱油的配方写进菜谱里没啥两样,都是把样式往元素上怼。

目录
<div class="p-4 border-2 border-blue-500"> ... </div>

同样是在拼凑样式,写成下面这种形式,本质上也是一种组合:

/* 这不也是把零件拼一块儿嘛 */
.card {
  padding: 1rem; 
  border: 2px solid var(--color-blue-500)
}

说到底,CSS这玩意儿打娘胎里就是为组合而生的,只是大伙儿平时不怎么把这事儿挂在嘴边,觉得它理所应当罢了。

组合从哪来

天生自带的属性

层叠样式表嘛,层叠这俩字本身就带着组合的基因。比如给按钮定了一套基础样式,这算是打了个底:

.button {
  display: inline-flex;
  padding: 0.75em 1.5em; 
  /* 其他打底样式 */
}

想换个皮肤?直接往上叠别的类就完事儿了,跟打工人套娃似的:

<button class="button primary">点我</button>
<button class="button secondary">点我</button>
.primary { background: orange; }
.secondary { background: pink; }

更有意思的是,这.button类还能直接甩给链接,让链接摇身一变成了按钮范儿:

<a href="#" class="button">戳这里</a>

这么一搞,其实干了俩事儿:一是把.button的样式组合到了<a>标签上,二是把.primary的颜色组合到了.button这个基础样式上。这招从CSS诞生那天就能玩,不是什么新鲜玩意儿。

文件里的混搭

一提到CSS组合,很多人脑子里只有HTML里那一长串类名。实际上,在样式文件里头用Sass的混入(mixin)来拼凑样式,也是实打实的组合,只是阵地转移了:

@mixin button () {
  display: inline-flex;
  padding: 0.75em 1.5em; 
  /* 其他样式 */
}

.button {
  @include button; 
}

这么干的好处是,样式文件里也能像搭积木一样,把常用的样式块(混入)拼到不同的选择器里去。

拆分四大块

如果把所有样式分门别类,基本上跑不出这四个筐:

类别负责
布局页面元素怎么搁
排版字体字号行距
主题颜色背景边框
特效阴影渐变动画

这四个筐里的东西基本井水不犯河水。比如font-weight只归排版管,color只属于主题范畴,不会互相掐架。

把这四类拆开,各自做一套可组合的类,到时候想用哪个就用哪个,跟玩乐高似的,想拼个房子就拼房子,想拼个车就拼车。甚至说,连小朋友玩的得宝大颗粒积木都比这复杂不到哪儿去:

<!-- 纯属虚构的类名,但意思就是那么个意思 -->
<div class="布局-1 布局-2 特效-1">
  <h2 class="排版-1 主题-1">标题来啦</h2>
  <div class="排版-2">正文走起</div>
</div>

拿实际点的例子来说,要是用了某些现成的样式库,最后HTML可能长这样:

<div class="card vertical elevation-3">
  <h2 class="inter-title">标题在这儿</h2>
  <div class="prose">正文内容</div>
</div>

card管框架,vertical管方向,elevation-3管阴影特效,inter-titleprose管排版样式。各管各的,互不干扰。

体积那点事儿

有人纠结用工具类(utility)堆HTML会不会导致HTML体积爆炸,用选择器组合(selector)会不会让CSS体积膨胀。说实话,这俩问题在大多时候都不是核心矛盾。

HTML本身就能扛一大堆东西,对性能影响微乎其微。CSS那边,500行撑死了也就12到15kb(大模型给的数据),随便一张图片就150kb往上走了。与其纠结这十几kb的样式是堆在HTML还是堆在CSS里,不如去优化几张图片来得实在。

费劲巴拉重构代码去缩减CSS体积,可能换来的只是加载时间快了那么两毫秒,体感基本为零。但如果重构是为了让代码更清晰、更好维护、让团队接手的人一眼就能看懂,那这笔买卖可就太值了。

类名叠穿的玩法

想把组合玩溜,关键是把样式拆得足够细,但又不至于细碎到没法管理。实操上,可以这么干:

第一步:打地基

先定义好最底层的样式,比如页面整体框架、字体家族:

/* 布局地基 */
.layout-stack {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

/* 排版地基 */
.typo-base {
  font-family: system-ui, -apple-system, 'Segoe UI', Roboto;
  line-height: 1.5;
}

第二步:做模块

针对不同场景,做专用的组合类。比如卡片模块,就可以把布局和排版的基础样式组合进来:

.card {
  @apply layout-stack;  /* 假设项目里用Tailwind的@apply,否则就直接写flex那一套 */
  background: white;
  border-radius: 0.5rem;
  padding: 1rem;
}

.card-title {
  @apply typo-base;
  font-weight: 600;
  font-size: 1.25rem;
}

第三步:上变体

给模块做变体,比如不同主题色的卡片:

.card-primary {
  border-top: 3px solid #3b82f6;
}

.card-primary .card-title {
  color: #1e3a8a;
}

.card-secondary {
  border-top: 3px solid #10b981;
}

.card-secondary .card-title {
  color: #065f46;
}

第四步:HTML里组合

到具体页面时,直接把这些零件组合起来用:

<div class="card card-primary">
  <h3 class="card-title">新功能上线</h3>
  <p class="typo-base">这功能绝了,用了都说好</p>
</div>

<div class="card card-secondary">
  <h3 class="card-title">限时优惠</h3>
  <p class="typo-base">错过等一年,赶紧上车</p>
</div>

这么一套流程下来,.card负责基础框架,.card-primary只负责改变颜色主题,.card-title管标题样式。改一个地方,所有用了这个组合的地方都会跟着变,比在HTML里写十来个类名再挨个找要痛快得多。

混入的进阶玩法

Sass的混入(mixin)是文件内组合的利器,可以做出带参数的组合块:

@mixin flex-center($direction: row) {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: $direction;
}

.header {
  @include flex-center;  // 默认横向居中
}

.sidebar {
  @include flex-center(column);  // 竖向居中
}

这样flex-center这个组合块就能按需取用,不用重复写那几行flex代码。文件里维护一处,所有引用的地方都跟着同步更新。