All Articles

React 组件样式方案对比-CSS Modules & Styled Components

如何给 React 组件应用样式,是个有争议性的话题。使用 JavaScript 将 CSS 内置的 CSS in JS 与传统的导入外置的 CSS 文件来应用样式,哪种方案才是最好的方法?两种方案一直存在着争议。

在本文中,让我们看看这些方法的不同点,进而讨论它们的优缺点。文章的最后,你会对每种技术方案都有所了解,进而找到最适合你自己的方案并应用到你自己的项目中。

Vanilla CSS

先让我们尝试使用引用外置的 CSS 样式表文件的方式,当我们开发 React 应用时,会像下面这样:

import React from "react";

import "./box.css";

const Box = () => (
  <div className="box">
    <p className="box-text">Hello, World</p>
  </div>
);

export default Box;

样式部分由一个完全分离的 box.css 文件导入进来:

.box {
  border: 1px solid #f7f7f7;
  border-radius: 5px;
  padding: 20px;
}

.box-text {
  font-size: 15px;
  text-align: center;
}

构建工具会收集所引入的全部外置 CSS 文件,合并后最终链接到你的 HTML 文件中。(比如使用 create-react-app )。

优点

因为是 Vanilla CSS ,所以你可以无限制地使用所有的 CSS 功能,比如:

  • 媒体查询(Media queries)
  • 帧动画(Keyframe animations)
  • 伪元素(Pseudo-elements)(eg. :before, :after)
  • 伪类(Pseudo-selectors)(eg. :hover, :nth-child)

你正在使用纯 CSS,Web 页面结构中最最基础的一部分。这意味着,你不用在项目中添加任何的依赖。你同时也不用担心 CSS 部分在不远的将来将会淘汰,就像本文下面介绍的那些方案。

如果你喜欢,你还可以使用你喜爱的 CSS 预处理器,比如 Sass 或者 Stylus,这些预处理器可以为你提供更强大的诸如 mixins、变量等高级功能,只要在构建过程中稍微添加下配置即可。

缺点

首先,你需要确认你的构建过程可以识别 CSS 的引用。幸运的是,如果你正在使用 create-react-app,这部分功能已经集成完备了。

另一方面,与下面其他几种方案相比,Vanilla CSS 方案无法使用 JavaScript 基于组件变量和属性来直接定制样式。这种样式方案,你需要采取一个别扭的方法(比如条件语句)来应用不同的样式类,比如下面的例子:

const Button = props => {
  const classNames = ["button"];
  if (props.large) classNames.push("button-large");
  if (props.rounded) classNames.push("button-rounded");
  if (props.color) classNames.push(`button-${props.color}`);

  return <button className={classNames.join(" ")} />;
};

在这个示例中,我不得不添加了一些条件来更改组件的 className 属性,再通过 prop 传入组件。稍后我们还要回来看下这个例子,演示下相对于其他 React 样式方案,本例还可以是什么样。

但最重要的问题是,CSS 本身不是基于组件架构而设计的。CSS 是基于页面文档和 Web 页来设计的,在这些环境下,CSS 的全局命名空间和层叠书写方式(cascade)是有力的工具。但在基于组件化的应用中,全局命名空间反而成了累赘和经常发生问题的地方。

在 React 应用程序中直接使用 CSS 文件的这种方式,会让我们遇到诸如类名冲突、样式错乱等问题,都可以归咎于全局样式污染。使用 BEM 命名方案,可以一定程度上缓解这种问题,但你还是无法保证第三方的(样式)代码不会影响你自己的样式。

更好的解决方案是可以直接作用于组件内的样式。这不仅可以解决全局命名空间问题,而且可以让开发者专注于组件,进而让我们可以干净整齐地打包样式代码,不必担心影响到其他组件。

(译者注:原文下方评论区有人提到了 classnames 包来处理 className,作者也做了相应解释)

Inline styles

下一个赋予 React 组件样式的方式是使用 Inline styles。

在组件中,通过 style 属性,传入使用驼峰风格的 JavasScript 代码设置好的样式规则,如下:

import React from "react";

const boxStyle = {
  border: "1px solid #f7f7f7",
  borderRadius: "5px",
  padding: "20px"
};

const boxTextStyle = {
  fontSize: "15px",
  textAlign: "center"
};

const Box = () => (
  <div style={boxStyle}>
    <p style={boxTextStyle}>Hello, World</p>
  </div>
);

export default Box;

优点

这种方式的优点是,你不需要任何额外的依赖和构建步骤,你仅仅就是在用 React。这种方式可以最快速的开始项目,更适合小项目或者可以表现作者的意图的示例代码(小 Demo)。你的样式声明仅在组件内部,所以不用担心全局样式污染或者层级错误导致的问题。

因为使用 Javascript,我们便可以加入一些功能和逻辑到我们的样式中。尝试用 inline styles 这种新的方式改写上面一段 Vanilla CSS 版本的按钮例子,如下:

const styles = props => ({
  fontSize: props.large ? "20px" : "14px",
  borderRadius: props.rounded ? "10px" : "0",
  background: props.color
});

const Button = props => {
  return <button style={styles(props)} />;
};

你可以看到,这个版本比上一个纯 CSS 版本更具描述性,而不是类似循环的一步步应用某些样式,所需要的样式可以直接作用于组件。

缺点

在实际项目中,我是不会推荐使用 inline styles 方式的。这是因为通过 JS 来实现 CSS ,会使我们失去许多 CSS 最好用最强大的部分:

  • 我们无法做到媒体查询
  • 我们失去了帧动画功能
  • 我们失去了伪元素功能(比如 :before, :after),你不得不把这些内容写到你的 JSX 中
  • 我们失去了伪类功能(比如 :hover, :nth-child),你需要使用 mouseovermouseleave 事件来模拟 :hover 行为

缺失任何一个重点都有可能是严重的问题,而且你还会失去在构建步骤中转换 CSS 的福利,比如自动添加厂商前缀的功能。主观地将,这方案让人感觉到笨拙且不爽。

CSS Modules

下一个方案,让我们来关注 CSS Modules。CSS Modules 就像是第一个 Vanilla CSS 方案的改进型,其将所有的类与动画名都固定在自己的作用域内。这意味着,你完全可以规避全局命名空间导致的那些问题。

import React from "react";
import styles from "./box.css";

const Box = () => (
  <div className={styles.box}>
    <p className={styles.boxText}>Hello, World</p>
  </div>
);

export default Box;

你引入了一个 box.css 外部文件,就像上面第一个 Vanilla CSS 方案一样。但这一次的引入,我们给他赋予了 styles 名字,此模块名作为一个属性 key 在组件中的 className 处使用,用以应用不同的类名。而引入的 CSS 文件就是一个普通的样式文件。

.box {
  border: 1px solid #f7f7f7;
  border-radius: 5px;
  padding: 20px;
}

.boxText {
  font-size: 15px;
  text-align: center;
}

优点

CSS Modules 更改了 CSS 文件中的类名并将他们变为了唯一名,这样将有效的将样式作用域化,使它们更适合基于组件的 Web 应用开发。CSS Modules 的使用,在保持了 Vanilla CSS 方案所有优点的同时,更帮助我们完全地规避了最大的(全局命名空间)问题。你仍旧可以完全控制 CSS 文件中所有的内容,而且还可以进一步的使用 CSS 预处理器(这需要升级构建步骤来支持)。

因为你的代码库内仍然包含若干纯 CSS 文件,所以哪怕是添加构建步骤,并小幅度地更改导入和应用样式的方式,对项目代码的修改量也不大。这意味着如果你将来需要抛弃 CSS Modules 方式,这种过渡也是影响最小的。

缺点

CSS modules 使用作用域类解决了全局 CSS 的问题,但是它还是没有解决纯 CSS 所遇到的同样的问题。

  • 你需要设置你的构建步骤来支持 CSS modules (如果你同时还在使用 Sass / Stylus)。create-react-app 已经支持了 CSS modules。
  • 像 CSS 一样,你无法通过 JavaScript 加入任何的逻辑判断

Styled Components

Styled Components 是一个用于 React 的 CSS in JS 库,它允许你在 JS 中添加本地组件范围的样式。但区别于 inline styles,styled components 最终会将 JavaScript 编译到 CSS 中。这个库使用 template tag literal 语法在组件中应用样式,就像下面这样:

import React from "react";
import styled from "styled-components";

const BoxWrapper = styled.div`
  border: 1px solid #f7f7f7;
  border-radius: 5px;
  padding: 20px;
`;

const BoxText = styled.p`
  border: 1px solid #f7f7f7;
  border-radius: 5px;
  padding: 20px;
`;

const Box = () => (
  <BoxWrapper>
    <BoxText>Hello, World</BoxText>
  </BoxWrapper>
);

export default Box;

优点

因为最终 JavaScript 会转换成实际的 CSS,所以你可以在 styled components 中无限制的使用 CSS 任何能力。这包括媒体查询、伪元素、动画等等。相较于(第二种 Inline styles 方式)驼峰属性,我们使用的就是常规的样式写法,styled components 得以让你书写普通的 CSS,这对于熟悉 CSS 工程师来说,这种方式真的极易上手。

因为我们在使用一种 CSS in JS 方案,所以我们可以使用 JavaScript 语言的全部特性,可基于传入属性(props)来应用不同的样式。组件的传入属性(props)实际上被组件的 styled 的调用,这点非常强大:

import React from "react";
import styled from "styled-components";

const Button = styled.button`
  background: ${props => props.color};
  font-size: ${props => props.large ? "18px" : "14px"};
  border-radius: ${props => props.rounded ? "10px" : "0"};
`;

export default props => <Button {...props} />;

与 Vanilla CSS 不同,样式与声明它们时创建的组件紧密相关,也解决了全局命名空间问题。

如果你喜欢 Styled Components 这种方式,那么还有其他可选的实现方案,你可以查看 Glamorous,看看你喜欢哪个。 (译者注:Glamorous 已经停止维护了,作者已经转向了 emotion

缺点

使用 Styled Components 需要在你的项目中添加额外的依赖。CSS in JS 世界的变化实在是很快,可以遇见在不远的将来,很有可能会出现更好的库,使用 styled components 的同时,你已经给自己背负了技术债务。

还有另外一件事,当新人加入团队时,你需要让他们加速赶上技术革新的进度。除此之外,考虑下我在文章开始时提到的辩论,为什么使用 CSS in JS 不是一个好主意?

CSS in JS是一个好主意么?(Is CSS in JS a good idea?)

在读过整篇文章后,你可能更倾向于使用 Styled Components 或者 CSS modules。Inline styles 对于大多数应用来说功能性不够,而且如果你还要 JS 路由的话 Styled Components 是个更好的选择。相反的,如果你倾向于传统的 CSS 方式,CSS Modules 给你一些 Vanilla CSS 无法实现的功能 - 像是作用域选择器。

就像我在文章开头提到的那样,使用 CSS in JS 是一个相当有争议性的话题,Styled Components 和 CSS Modules 的选择,二者孰优孰劣的讨论超出了本文的范围,如果你关心这场辩论,请移步这里 Stop using CSS in JavaScript for web development。记住,凡事都有两方面,本文只是探讨了 Styled Components 的一些优势。

结论(The Verdict)

本文目的是简要的介绍了几种为 React 组件设置样式的方法的关键不同点。所以,我没有深入每种方法探讨它们的细节,如果你偏爱其中某种方式,你完全可以自己去做更深入的研究。

从现开始,在我自己的项目中,我准备应用 styled components 方案,我只是觉得这个方案提供更加灵活且更加符合组件化概念,使用它可以让我的代码尽可能的保持干净整洁。

原文:https://www.bhnywl.com/blog/styling-react-components/