踩了一圈 CSS 构建方案的坑
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 挺好用。
优点:结构非常清晰
缺点:
- 编译后的选择器很长一串,从浏览器渲染角度,匹配DOM是耗性能的
- 难以应对复杂项目 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 语法。
优点
- 灵活,快,好看
- 工具链齐全,可以裁剪掉不用的原子类。
缺点
- 稍微要写复杂一点的样式,DOM 就会被一大堆 class 埋没。
- 从浏览器渲染角度,匹配、合并大量 CSS 样式是需要更多性能开销的
- 要做到同种样式的复用,必须组合原子类,变成功能类或组件类,否则维护起来相当麻烦。这似乎违背了用 tailwind css 的初衷,熟悉了 css 的不如直接自己用 css 手撮功能类和组件类。
- 其实我是 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 的匹配)
优点:
灵活好拓展,比如主题管理不仅仅是颜色,还可以是图片资源一类的。
缺点:
- 因为是 JS 转 CSS,服务器编译慢和客户端渲染慢得选一个
- 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 写法和思路差不多。
缺点:
-
值复用靠变量,但是由于是 静态 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 属性其实已经不能算静态了。 -
组件间的样式复用方案只有原生的 CSS 方案,上述的奇妙客户端插值做不了这个需求。假设,你要做一个主题化的对话框的卡片阴影,只能使用原生 css 类中加原生 css 变量。上述动态改变样式的因为依赖 props,只能使用
styled
的写法,但这样就会把 html 标签了也继承了,不同的样式也无法随意组合。这也是为什么我说 Linaria 是原生 css 的替代,而不是 styled-components 的替代,构建方式就决定他们差得太了远。 -
基于2,导致你写组件又要检查
styled
又要确认css
类又要检查 JSX classname 的顺序。如果用组件继承会被迫连 DOM 类型都继承。 -
使用功能类有点像原子化,又完全不如 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 框架选择要素
- 样式复用
- 样式组合
- 动态样式
- 主题切换
- 代码提示
- 自动裁剪
- 随意重构
- 渲染性能
- 实践的统一性
最重要的还是自己的需求。