总结CSS组件化演进之路:命名规范/CSS Modules/CSS in JS/原子化CSS

关注点分离

众所周知,前端开发代码有三大部分:

  • HTML 负责页面的结构
  • CSS 负责页面的视觉样式
  • JavaScript 负责页面的交互行为

这三大部分属于三种不同的"编程语言",有一定的隔离性,但在逻辑上又互相关联,因此如何组织这些代码,是前端开发的一个重要问题。在较早期的前端开发中是三种代码分离的,即HTML中只写标签,不写CSS和JavaScript,这两个在单独的区域/文件中实现。这种做法叫做"关注点分离"。

html 复制代码
<!-- 三种代码合并 -->
<div style="color: red" onclick="console.log('click')"> 你好 </div>

<!-- 三种代码分离 -->
<div class="texts" onclick="handleClick()"> 你好 </div>

<style>
.texts {
  color: red;
}
</style>

<script>
function handleClick() {
  console.log('click');
}
</script>

关注点分离的代码结构清晰,每种代码的耦合性少,因此在早期受到推崇。但这种方式在代码组织上有很大的问题,即虽然三种技术互相独立,但在逻辑上却是相互关联的:

  • CSS样式是和HTML标签关联的,标签绑定样式才能发挥效果
  • JavaScript对数据和交互的处理会造成HTML元素的展示和CSS样式变化

尤其是前端交互逻辑和数据处理越来越重,将三类代码分离对开发有很多不便之处。试想如果要调整一个模块的逻辑和展示,需要三个甚至更多文件同时查看对比才能搞清楚,这会造成开发效率太低,逻辑不清晰,代码维护性差等很多问题。

组件化框架

后来出现了组件化的思想,将同一个模块的HTML, JavaScript, CSS代码放在一起,组成一个组件。组件本身可以自由组合和复用,组件内部的代码和外部隔离。开源社区中涌现了很多组件化的前端工具和框架,例如知名的React和Vue。现在这种组件化的模式,已经成为了前端开发的主流。

Vue模式

Vue框架中的单文件组件形式,既保留了HTML, JavaScript, CSS代码的独立性(虽然HTML部分演变为了生成HTML的模板),依旧保留关注点分离的特性,又将它们组合到了同一个文件中,HTML模板可以直接使用JavaScript的数据来更新元素。这样使得同属于一个组件的代码逻辑清晰展示,同时不会影响到其它组件。前面的代码使用Vue的单文件组件改写后如下:

vue 复制代码
<script setup>
function handleClick() {
  console.log('click');
}
</script>

<template>
  <div class="texts" @click="handleClick"> 你好 </div>
</template>

<style>
.texts {
  color: red;
}
</style>

React模式

React框架则首先发明了JSX语法,可以在JavaScript中编写标签模板。这样使得JavaScript和HTML的关系非常紧密:

jsx 复制代码
import './index.css';

function Comp() {
  const handleClick = () => {
    console.log('click');
  };
  const styles = { fontSize: '14px' };
  return <div className="texts" style={styles} onClick={handleClick}> 你好 </div>;
}

可以看到,React中一个函数即为一个组件,组件函数直接返回JSX标签语法,作为HTML渲染的模板。虽然Vue等组件后来也支持JSX,但还是React的使用最广泛。并且它们的标签语法都是类似于"HTML模板",而不是一种真正的HTML。

React中的CSS可以直接作为内联style属性的数据,在JavaScript中控制样式。React对于CSS的封装比较弱,因此开发者还是必须使用独立的CSS等样式文件来控制样式。例如:

css 复制代码
/* index.css */
.texts {
  color: red;
}

但这样事实上就造成了React组件中HTML和JavaScript的代码位置联系紧密,但与CSS的联系却有些松散。再加上在组件中引入的CSS规则不仅在组件内生效,而且对于页面全局都生效,这样会造成不同组件的样式冲突和污染。为了解决这些问题,使得CSS和组件紧密联系,开源社区中涌现了很多关于CSS的组件化技术。

组件化方案

前面我花了不少时间,根据技术发展的顺序学习了CSS组件化相关的技术,写了四篇文章:

这四种方案分别是CSS命名规范/CSS Modules/CSS in JS/原子化CSS,它们都尝试着解决组件化开发中 CSS 的问题,我们在这里再简单描述一下。

CSS命名规范

CSS命名规范有非常多,最知名的是BEM命名规范。BEM的全称为Block Element Modifier,翻译成中文就是块,元素和修饰符。BEM使用这三种层级来规范CSS的命名:

  • Block 区块 表示页面中一个独立可复用的模块或者组件
  • Element 元素 表示区块中的一个组成元素
  • Modifier 修饰符 修饰元素的状态或者行为

元素不能独立存在,必须依附于区块内。修饰符则必须跟在元素或者区块后面。因此可以这样组合命名:

block 单区块 block__element 区块+元素 block--modifier 区块+修饰符 block__element--modifier 区块+元素+修饰符

再列举一下其他的CSS命名规范:

  • OOCSS: 面向对象的CSS,将CSS类封装为基类和子类等,例如分离结构和皮肤,分离容器和内容等
  • SMACSS: 可扩展和模块化的CSS结构,主要将CSS规则分为五种类型:基础样式/布局样式/模块样式/状态样式/主题样式
  • ITCSS: 把CSS规则分成了七层:Settings/Tools/Generic/Elements/Objects/Components/Trumps,并在不同的位置放置
  • AMCSS: 使用HTML的属性key和值用来组织CSS选择器,有三种类型:Modules模块/Variations变体/Traits特征
  • SUITCSS:组件化的样式工具,不仅包含CSS命名规范,也提供CSS预设包。有公共样式和组件样式等。

CSS Modules

CSS Modules中文叫做CSS模块。它通过自动的方式,完全避免组件内的类名与其它组件重复。默认情况下,我们定义的CSS类名标识符是全局的。使用CSS Modules之后,每个类名将变为唯一的全局名称,包含不会重复的哈希值。这里首先创建一个CSS文件,名称为App.module.css。

css 复制代码
.class1 {
  color: red;
}
.class2 {
  color: blue;
}
:global(.class3) {
  border: 1px solid yellow;
}

引入CSS文件时,我们可以拿到CSS文件导出的类名到全局名称的对应关系,从而在HTML中提供相应的类名。

jsx 复制代码
import styles from './App.module.css';
import cn from 'classnames';

export default function App() {
  return (
    <div>
      <div className={styles.class1}>test1</div>
      <div className='class3'>test2</div>
      <div className={cn(styles.class2, 'class3')}>test3</div>
    </div>
  )
}

CSS in JS

CSS in JS可以让我们在JavaScript中编写CSS代码,抛弃CSS文件。它的背后依然是通过JavaScript中写的CSS规则生成哈希类名,放到HTML元素的class上。

CSS in JS的工具非常多,主要分为两种类型:运行时和编译时。运行时可以在前端代码在用户访问时,动态生成CSS规则,优点是灵活,缺点是工具需要打包进代码,造成体积增大,且执行效率低一点。编译时是在打包时将所有CSS规则编译为类名,不允许执行时动态生成。

CSS in JS有许多使用方式,有模板字符串,style对象,css属性等。这里我们以Emotion为例,简单给出使用Demo:

jsx 复制代码
// 模板字符串组件使用方式
import styled from "@emotion/styled";

const Div = styled.div`
  color: red;
  background: ${(props) => props.bg};
`;

function App() {
  return (
    <div>
      <Div bg="blue">你好,jzplp</Div>
      <Div bg="yellow">你好,jzplp</Div>
    </div>
  );
}

// style对象使用方式
import styled from "@emotion/styled";

const Div1 = styled.div((props) => {
  return {
    color: "red",
    background: props.bg,
  };
});

function App() {
  return (
    <div>
      <Div1 bg="yellow">你好,jzplp1</Div1>
      <Div1 bg="green">你好,jzplp2</Div1>
    </div>
  );
}

// css属性使用方式
function App() {
  return (
    <div
      css={{ color: "red",
        "&:hover": { background: "green" },
      }}
    >
      你好 jzplp
    </div>
  );
}

原子化CSS

原子化CSS有点像一种编译时的CSS in JS:它是在编译时查找,生成对应的CSS规则,避免了直接写CSS文件。但与CSS in JS不同的是,它不需要写CSS语句,而是将一个一个CSS属性封装为预设的类名。我们代码中直接写类名即可。同时原子化CSS还支持扩展,例如主题,规则,变体等等。Tailwind CSS是最知名的原子化CSS工具,这里我们举例一下使用方法:

js 复制代码
// 直接使用预设类名
function App() {
  return <div className='text-xl font-bold text-orange-500'>jzplp1</div>;
}

不同的类名编译后效果不同,这里列举一些组合类名和编译后的CSS规则:

css 复制代码
/* 类名:not-focus:bg-amber-500 */
.not-focus\:bg-amber-500 {
  &:not(*:focus) {
    background-color: var(--color-amber-500) /* oklch(76.9% 0.188 70.08) */;
  }
}

/* 类名:sm:text-center */
.sm\:text-center {
  @media (width >= 40rem) {
    text-align: center;
  }
}

/* 类名:bg-[red] */
.bg-\[red\] {
  background-color: red;
}

特点和总结

前面我们根据技术发展的顺序,简单描述了CSS命名规范/CSS Modules/CSS in JS/原子化CSS。它们有些技术的出现是比组件化要早的,例如部分命名规范,但实际上也是和组件化一样解决类似的问题。这里我们简单对这些CSS组件化技术做一些特点和总结。

  • 百花齐放,各种技术方案层出不穷
    • 这里仅仅描述了四种主要技术,事实上CSS组件化的技术还有很多,例如Vue使用的组件作用域CSS
    • 在每种技术类型内部,也有各种各样的工具和技术方案,例如有很多种CSS命名规范,也有很多种CSS in JS技术
  • 优缺点明显,没有哪种技术能解决所有问题
    • CSS命名规范是由人为规定方案,完全依赖于管理
    • CSS Modules在避免类名冲突方面非常好,方案也被其它技术采用,但没解决组件化的其它问题
    • CSS in JS成功将CSS代码与组件写在一起,但是代码繁琐(例如styled组件式写法)而且有性能损失
    • 原子化CSS不需要写CSS代码,也实现了组件化,但不能运行时生成CSS规则,元素上太多类名时看起来也不直观
  • 后面的新技术会吸取旧技术的优点
    • CSS命名规范是由人手动约定类名避免重复,但CSS Modules直接由代码生成哈希类名,无重复的可能
    • CSS in JS利用了CSS Modules中生成哈希类名的方式避免重复
    • 原子化CSS则参考了编译时CSS in JS技术,在编译时生成CSS规则

虽然技术在不断的演进,但可以看到依然没有一种技术可以解决所有问题,每种技术的优缺点都很明显。这就造成了喜欢这些优点的开发者愿意用这些技术,而讨厌这些缺点的开发者就不愿意用这些技术。萝卜青菜,各有所爱。具体用哪种技术,还是要看具体项目的要求和约定形式。

对我自己来说,像是简单的小项目,原子化CSS非常不错。如果只是避免类名冲突,可以使用CSS Modules。至于CSS in JS我觉得有点麻烦,不太喜欢。如果是需要规范约定类名的项目,例如组件库等,可以参考BEM等CSS命名规范。

参考

相关推荐
踩着两条虫2 小时前
「AI + 低代码」的可视化设计器
开发语言·前端·低代码·设计模式·架构
Jagger_2 小时前
项目上线忙碌结束之后,为什么总想找点事做?
前端
GalenZhang8882 小时前
OpenClaw 配置多个飞书账号实战指南
前端·chrome·飞书·openclaw
steven~~~3 小时前
为什么mq报错
javascript
萌新小码农‍4 小时前
python装饰器
开发语言·前端·python
threelab4 小时前
Three.js 初中数学函数可视化 | 三维可视化 / AI 提示词
开发语言·前端·javascript·人工智能·3d·着色器
爱学习的程序媛4 小时前
浏览器工作原理全景解析
前端·浏览器·web
凉辰5 小时前
解决 H5 键盘遮挡与页面上推
开发语言·javascript·计算机外设
我是若尘5 小时前
用 Git Worktree 同时开多个需求,不用来回 stash
前端