SANSUI'S BLOG

系统外观
分类标签
RSS
Sansui 2025
All rights reserved
人活着就是为了卡卡西

踩了一圈 CSS 构建方案的坑

7 月 12 日, 2025

css 的写法一直算比较混乱的。层叠的样式表与 DOM 结构的分离看似清晰,但也因此容易产生屎山,组合太自由,哪些选择器用了哪些选择器没用,共用的嵌套的,分离的。今天小编就带你一探究竟(……)

CSS类复用粒度

我自己把 css 选择器(类)的复用粒度分三个层级。

组件类

粒度最大的层级,通常按组件级别语义化。选择器一般是下面这些名字

.wrapper
.container
.list-item

组件化的选择器下面通常有很多条的 css。

功能类

通常是共用的样式或状态,比如

.open
.close
.light
.dark
.glass-effect

这个看起来好像和组件类不冲突,但硬说的话组件类其实应该是这样

.container.open { 此处将 .open 的所有样式全覆盖 }
.container.close { 此处将 .close 的所有样式全覆盖 }
.container.light { 此处将 .light 的所有样式全覆盖 }
.container.dark { 此处将 .dark 的所有样式全覆盖 }

组件类的状态严格在组件的 scope 下。功能类则是可以不限 Scope 的复用。

这 CSS 容易混乱的根源。在工程维护角度,功能类是最不敢乱动的类,不知道动了后哪里样式就会出问题。但在设计角度,用功能类复用一些状态又确实很方便,统一设计也好用。比如增加统一的圆角、描边、阴影样式。

功能类的优缺点是一体两面——图像的只有主观的好看与否,没有客观的对错。

原子类

定义海量常用的基础样式类,在 class 上直接写类名就能获得对应效果。就是 tailwind css。

原子类相较于功能类粒度更小,也不会轻易改动 css 属性。

.flex
.col-1
.text-sm

方案

通常来说,一个库的样式会着重在一个某一个粒度上。

原生 css

用原生 css 时通常会以 组件化 的粒度为主,带极少的功能类。现在配合 css 变量使用。早期的网页简单,一个 CSS 文件就能搞定全站,设计上并没有考虑项目变得越来越复杂后的实践。

优点:性能好,扁平的结构利好小项目。适合写研究新样式。

缺点:过于扁平,大量工程化后易屎山,存在样式与 DOM 分离带来的维护混乱。

SCSS

古法预处理器,可能多层嵌套 css,可组合。是 组件化 的粒度。在 CSS.module 出来前,用 SCSS 分割 Scope 挺好用。

优点:结构非常清晰

缺点

  1. 编译后的选择器很长一串,从浏览器渲染角度,匹配DOM是耗性能的
  2. 难以应对复杂项目 DOM 结构的改变,需要考虑扁平化 + 命名,但这样做和原生 CSS 的维护体验也不相上下。

CSS Module

CSS Module 是完全 组件化 的粒度。相比起 SCSS 的样式与 DOM 分离,CSS Module 为组件内部样式耦合,组件间样式分离。

优点: 在组件粒度分割合理的情况下,清晰易维护。

缺点:依赖预构建,写类名写起来太磨叽了。整体我用得不多没法评价。

const Button = () => {
  return (
    <button className={styles.button}>
      Click me
    </button>
  );
};

BootStrap

组件化 为主,少量原子化修饰的预制样式库,拿来即用是不错的。早期 CSS 框架大多是指预制样式,和预构建的库有本质区别。

Tailwind css

完全原子化的神奇之库,通过编译可以有功能类和组件类。它更像是重新定义了 css 语法。

优点

  1. 灵活,快,好看
  2. 工具链齐全,可以裁剪掉不用的原子类。

缺点

  1. 稍微要写复杂一点的样式,DOM 就会被一大堆 class 埋没。
  2. 从浏览器渲染角度,匹配、合并大量 CSS 样式是需要更多性能开销的
  3. 要做到同种样式的复用,必须组合原子类,变成功能类或组件类,否则维护起来相当麻烦。这似乎违背了用 tailwind css 的初衷,熟悉了 css 的不如直接自己用 css 手撮功能类和组件类。
  4. 其实我是 tailwind 黑,嗯。但无法否认开发时确实很快很方便。
function Card({ title, description, imageUrl, imageAlt }) {
  return (
    <div className="max-w-sm rounded overflow-hidden shadow-lg">
      <img className="w-full" src={imageUrl} alt={imageAlt} />
      <div className="px-6 py-4">
        <div className="font-bold text-xl mb-2">{title}</div>
        <p className="text-gray-700 text-base">{description}</p>
      </div>
      <div className="px-6 pt-4 pb-2">
        <span className="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">#photography</span>
        <span className="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">#travel</span>
        <span className="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">#adventure</span>
      </div>
    </div>
  );
}

原生 css in js

指 JS Object 转译为 CSS。由于写起来太不像 CSS,复杂的功能写起来过于不直观 ,我直接 PASS。

const InlineStyleExample = () => {
  const myStyle = {
    color: 'blue',
    backgroundColor: 'lightgray',
    padding: '10px',
    borderRadius: '5px'
  };

  return (
    <div style={myStyle}>
      <p style={{ fontSize: '18px', fontWeight: 'bold' }}>
        This text is styled with inline CSS.
      </p>
    </div>
  );
};

Styled-components

组件化的 CSS in JS 方案,写起来像 CSS 实际是 JS。支持客户端动态修改 CSS 具体属性(其他方案做状态改变主要依靠 selector 的匹配)

优点

灵活好拓展,比如主题管理不仅仅是颜色,还可以是图片资源一类的。

缺点

  1. 因为是 JS 转 CSS,服务器编译慢和客户端渲染慢得选一个
  2. React 的 useContext 要被废弃了,而 styled-components 严重依赖此 hook,导致进入了维护状态。JS 框架发展太快了。
import styled from 'styled-components';

// Create a styled button component
const StyledButton = styled.button`
  background-color: blue;
  font-size: 16px;
  padding: 10px 20px;
  border-radius: 5px;

  &:hover {
    background-color: darkblue;
  }
`;

function App() {
  return (
    <div>
      <StyledButton>Click Me</StyledButton>
    </div>
  );
}

Linaria

自定义 功能类 的 CSS in js 方案,同时也支持 组件化 写法。生成的是完全静态的 css,样式值的复用靠变量,片段的复用靠 css 生成的类。

优点

是预构建方案,在服务端渲染。和原始的 CSS 写法和思路差不多。

缺点

  1. 值复用靠变量,但是由于是 静态 css,这个并不会变。所以变量插值其实是常量。比如下面的 font-size 并不会变化。

    const fontSize = 16;
    const Title = styled.h1`
      font-size: ${fontSize}px;
    `
    

    也就是说,你如果想在客户端随意改变字体,用 context/zustand 这种 runtime 的 fontSize,这样写报错。不过,Linaria 允许你借助 react 的 props 和 styled 组件来实现客户端的值变化。

    const Title = styled.h1`
      font-size: ${props => props.size}px;
    `
    export default function MyComponent() {
      const fontSize = useAppStore(state => state.fontSize)
      return <Title fontSize={fontSize}>Hello</Title>
    }
    

    相当于生成

    <h1
      className="_title_xyz"
      style={{ '--linaria-font-size': `${size}px` }}
    >
    

    那这和 styled-components 写起来已经差不多了。而且要做主题化的值都得用快要废弃的 useContext API。只不过 linaria 改的 style 属性,styled 是改的 css API。改 style 属性其实已经不能算静态了。

  2. 组件间的样式复用方案只有原生的 CSS 方案,上述的奇妙客户端插值做不了这个需求。假设,你要做一个主题化的对话框的卡片阴影,只能使用原生 css 类中加原生 css 变量。上述动态改变样式的因为依赖 props,只能使用 styled 的写法,但这样就会把 html 标签了也继承了,不同的样式也无法随意组合。这也是为什么我说 Linaria 是原生 css 的替代,而不是 styled-components 的替代,构建方式就决定他们差得太了远。

  3. 基于2,导致你写组件又要检查 styled 又要确认 css 类又要检查 JSX classname 的顺序。如果用组件继承会被迫连 DOM 类型都继承。

  4. 使用功能类有点像原子化,又完全不如 tailwind 已经给你预设好一堆东西的效率。写类名和 cssmodule 一样,太磨叽了

我博客本想迁移至此方案,但由于工作量实在巨大而放弃。linaria 主要还是解决了个命名空间冲突的问题,想用得更深入一点就会四不像。

import { css } from '@linaria/core';

const eleStyle = css`
  color: red;
  font-size: 3rem;
  &:hover {
    color: blue;
  }
`;

function App() {
  return <h1 className={eleStyle}>Hello Linaria!</h1>;
}

export default App;

构建组建库

每一个 CSS 方案都有对应的构建组件库的实践。 shadcn 是基于 tailwind 构建组件库实践。

CSS 框架选择要素

  1. 样式复用
  2. 样式组合
  3. 动态样式
  4. 主题切换
  5. 代码提示
  6. 自动裁剪
  7. 随意重构
  8. 渲染性能
  9. 实践的统一性

最重要的还是自己的需求。

更新于 2025-07-12 20:49
Waline