🎉 欢迎订阅 React 技术专栏:
- 知其所以然!探索 React 渲染工作原理 🔍
- 叮!React 必须掌握的“状态管理”知识大放送 📣
- 关于 React 副作用你必须知道的三两事 🫵
- React 减少无效的 re-render,提升应用的性能 🚀
- React 组件还能这么写!手把手教你写复合组件 🧱
- React 开发编写 CSS 样式不完全指南 🧭
前言
在 Vue 开发中,CSS 样式的编写相对较为统一且直观,开发者以贴近原生 CSS 使用的方式在单文件组件(SFC)中通过使用 <style>
标签编写样式。同时,Vue 允许开发者通过添加 scoped 属性来控制样式的作用域,决定其是全局应用还是仅限于当前组件。此外,Vue 还支持通过 lang 属性指定 CSS 预处理器,如 less 或 sass 等。相比之下,在 React 开发中,CSS 样式编写呈现出一种多元化的生态,其提供了众多的解决方案,每种方案都有其特定的使用场景和学习曲线,这使得开发者在选择样式编写方案时,往往需要根据项目需求、团队习惯和个人偏好来做出决策。这种多样性也带来了一定的复杂性,这使得在 React 项目中管理样式成为了一个需要深思熟虑的问题。本文梳理了在 React 开发中广泛采用的几种 CSS 样式编写方法,旨在为读者在选择 CSS 样式方案时提供有价值的参考和指导。
方式一:Inline CSS
React 编写 CSS 样式最简单的方法就是行内样式,与在 HTML 中编写行内样式类似,style 属性接受一个采用驼峰形式命名属性的 JS 对象,注意这里传入的是 JS 对象,所以 CSS 属性是放在双大括号之间,并且属性名是驼峰式的,例如在 CSS 写法中是 font-size,而在这里我们要写成 fontSize,示例如下:
const commonStyles = {
fontSize: "16px",
};
function InlineCSS() {
const [isHighlighted, setIsHighlighted] = useState(false);
return (
<div
style={{
...commonStyles,
color: "red",
backgroundColor: isHighlighted ? "yellow" : "initial",
}}
>
Hello! Welcome to my world!
</div>
);
}
优点:
- 行内样式的写法使得 CSS 样式只作用当前作用的元素上,天然具备样式隔离;
- 可以与 state 进行联动,根据不同的 state 可设置不同的样式,如上示例的背景颜色;
- 基于 JS 对象我们可以将公共样式进行抽离以实现复用,如上示例的commonStyles;
缺点:
- 当页面样式复杂的时候,JSX/TSX 中会存在大量样式代码,内容混乱难以阅读;
- 如果样式对象较为复杂,在一定程度上会影响性能,每次重新渲染都会生成新的对象;
- 行内样式不支持编写伪类和伪元素等内容;
方式二:CSS File
在常规 CSS 样式编写方法中,除行内样式之外,就是“引入 CSS 样式文件 + CSS 类名”的方式,在 React 开发中同样适用。当然,我们也可以使用 less 和 sass 等方法进行样式文件的编写。除此之外,需注意在 React 中 CSS 类名属性不是 class,而是 className。示例如下:
/* file-css.css */
.red {
color: red;
}
import "./file-css.css";
function FileCss() {
return <div className="red">Hello! Welcome to my world!</div>;
}
这种方案最显著的一个问题就是“全局生效,样式污染”。直接引入 CSS 文件一经生效则将作用于全局,无法进行样式隔离,会对组件之外的内容产生不可预知的影响。当然,我们可以对类名进行一定的规范约束(例如 BEM 命名规范),CSS 选择器写的尽可能精准且唯一,但这都是治标不治本的方法,无法从根本上解决问题。幸运的是,CSS 模块(CSS Modules)为我们提供了一种更为有效的解决方案。
方式三:CSS Modules
CSS Modules 允许我们将样式封装在组件内部,使得样式作用域仅限于该组件,从而避免了全局样式污染。通过这种方式,开发者可以更加自由地编写样式,而不必担心它们会影响到其他组件或页面元素。CSS 模块的实现通常依赖于构建工具,它们在编译过程中自动处理 CSS 文件,将类名转换为唯一的标识符。在使用 Vite 构建的项目中,任何以.module.css
结尾的 CSS 文件都将被视为 CSS 模块文件,导入这样的文件将返回相应的模块对象。示例如下:
/* example.module.css */
.red {
color: red;
}
.red .blue {
color: blue;
}
.font-bold {
font-weight: bold;
}
import { useState } from "react";
import classes from "./example.module.css";
function ModulesCSS() {
const [isHighlighted, setIsHighlighted] = useState(false);
return (
<div className={`${classes.red} ${isHighlighted ? classes["font-bold"] : ""}`}>
Hello! <span className={classes.blue}>Welcome to my world!</span>
</div>
);
}
如上面示例所示,将 CSS 文件引入为 JS 对象后,通过classes.red
来应用red
类名,打开控制台,我们可以看到 Vite 自动将类名red
被编译成了带哈希的._red_11cni_1
,这就产生了独一无二的类名,但与此同时,这种带哈希的类名也对我们在控制台中调试样式带来了一定的阻碍。
方式四:CSS-in-JS
在传统的前端开发实践中,我们将 HTML、CSS 和 JavaScript 分别作为独立的模块进行开发。然而,在 React 的开发哲学中,逻辑与用户界面是密不可分的,这种思想催生了 JSX 语言,它允许开发者以类似于 HTML 的方式编写组件结构,同时又嵌入 JavaScript 逻辑。CSS-in-JS 模式正是基于这种思想,它是一种将 CSS 样式直接嵌入 JS 文件的技术。这种方法不仅简化了样式管理,还提供了一种更为紧密的组件与样式的耦合方式。开发者可以在 JavaScript 组件内部定义样式,实现样式与组件逻辑的同步开发,而不依赖于外部的 CSS 文件。通过 CSS-in-JS,样式的作用域被限定在组件内部,从而避免了全局样式污染的问题。这种封装机制使得样式更加安全,也更易于维护。同时,CSS in JS 支持动态样式的生成,允许开发者根据组件的状态和属性来动态调整样式,这为构建交互式和响应式界面提供了极大的便利。目前,业界有非常多 CSS-in-JS 库可供使用,例如 styled-components、inaria、emotion、glamorous 等,快速尝试 👉 CSS-in-JS Playground。本文以 styled-components 为例,示例如下:
import { useState } from "react";
import styled, { css } from "styled-components";
const Title = styled.h1<{ $isHighlight?: boolean }>`
font-size: 1.5em;
color: red;
font-weight: ${(props) => (props.$isHighlight ? "bold" : "normal")};
&:hover {
color: yellow;
}
.small {
font-size: 0.5em;
}
${(props) =>
props.$isHighlight &&
css`
background-color: blue;
`}
`;
function StyledComponentsCSS() {
const [isHighlighted, setIsHighlighted] = useState(false);
return (
<>
<Title $isHighlight={isHighlighted}>
Hello! <span className="small">Welcome to my world!</span>
</Title>
<button onClick={() => setIsHighlighted(!isHighlighted)}>click</button>
</>
);
}
如上示例所示,Styled-components 与 React 紧密集成,我们使用模版字符串进行样式编写,并且设置自定义属性 $isHighlight 用以展示不同的样式(使用transient-props),最终我们会得到一个具备样式的组件,直接在 JSX/TSX 中使用即可。打开浏览器控制台,我们可以看到类名是一个哈希值,实现了局部 CSS 作用域的效果,避免样式污染。通过 CSS-in-JS 这种方法,我们可以较好地解决样式全局污染的问题,样式与组件紧密结合,当组件文件删除时样式代码也就被删除了,不会在项目中存在较多无效的样式代码。但是,与 CSS Modules 一样,这种带哈希的类名我们调试起来是比较费劲的,并且当组件的样式较为复杂时,大量的 CSS 代码对阅读逻辑代码较不友好。推荐阅读:CSS in JS 的好与坏,关于性能的对比推荐阅读:Real-world CSS vs. CSS-in-JS performance comparison。
方式五:Utility-first CSS
Utility-first CSS 是一种 CSS 编写方法论,它强调使用实用工具类(Utility classes)来构建用户界面,而不是传统的 BEM(Block Element Modifier)或 OOCSS(Object-Oriented CSS)方法。这种方法的核心思想是将样式分解成一系列独立的、可复用的类,每个类只负责一个单一的样式属性或功能。Utility-first CSS 的一个典型例子是 Tailwind CSS,这是一个流行的 CSS 框架,它完全基于实用工具类。使用 Tailwind CSS,开发者可以通过组合不同的工具类来快速构建界面,而不需要编写自定义的 CSS 代码。示例如下:
function TailwindCSS() {
return <h1 className="text-3xl font-bold underline">Hello world!</h1>;
}
利用 Tailwind CSS,我们能够直接在 HTML 中应用一系列预定义的类,从而轻松设置元素的样式,这种方法使得我们不再为取什么类名而苦恼。同时,与传统的 CSS 开发方式相比,当我们引入新功能时,不再会导致 CSS 文件体积膨胀。使用 Tailwind CSS,我们很少需要编写新的 CSS 代码;相反,我们仅需通过指定相应的类名来快速实现样式。我们通常对 CSS 的全局影响感到忧虑,因为样式的变更可能会带来不可预测的后果。然而,通过使用 Tailwind CSS,我们通过类名应用样式的方式,实际上避免了直接修改 CSS 规则。这种方式确保了样式的应用是局部化的,从而减少了样式冲突的风险,让我们的样式管理更为精确和可控。
原子 CSS 就像是 Utility-first CSS 的一个极端版本: 所有 CSS 类都有一个唯一的 CSS 规则,可参考 UnoCSS。除此之外,还可以参考 Windi CSS,但该项目目前已经终止,UnoCSS 集成了 Windi CSS 的许多强大功能。
总结
在 React 开发领域,对于 CSS 解决方案的寻求通常聚焦于以下几个核心需求:首先,CSS 样式需要具备局部作用域的特性,确保它们不会渗透至其他组件,影响其元素的独立性;其次,样式应能响应属性或状态的变化,实现动态的视觉效果;再者,解决方案应全面支持伪类、伪元素以及动画等 CSS 的高级特性。本文总结了五种广泛采用的 CSS 应用策略:Inline CSS、CSS Files、CSS Modules、CSS-in-JS 以及 Utility-first CSS,这些方法在应用方式、影响范围和使用场景上各有千秋,开发者可以根据项目的具体需求和上下文环境做出恰当的选择。本文示例源码:react-css
One More Thing:本文一直在强调样式隔离,那样式隔离都有哪些方案呢?BEM、shadow DOM、Vue scoped(简单省事👍)、CSS-in-JS、CSS Modules 等 😊