在 React 中使用 CSS

2020-04-08 11:08:02

由于 React 本身没有对 CSS 做特殊的支持或处理,因此在 React 中写 CSS 时可以使用常规的在普通 HTML 中使用的行内样式或 class 选择器,也可以使用各种社区解决方案,如 CSS Modules、CSS in JS 等。本文简单介绍一下各种方案。

1. 行内样式

与在普通 HTML 中写行内样式一样,添加一个 style 属性即可。但不同的是,在 HTML 中 style 的属性值是字符串,而在 JSX 中则是一个对象:

<!-- HTML -->
<h1 style="color: red">Heading 1</h1>
// JSX
<h1 style={{ color: 'red' }}>Heading 1</h1>

此外,JSX 中 style 对象的属性采用小写驼峰命名的方式,如 background-color 需要写为 backgroundColor。一般不建议在项目中大量使用这种方法。

2. class 选择器

在 JSX 中使用 class 选择器时,需要将 class 写为 className,值还是一个字符串,多个 class 时使用空格隔开。

<h1 className="h1 text-center">Heading 1</h1>

在 React 16 及以上的版本中,可以把 className 写为 class 了,但为了避免与 JS 中的 class 关键字混淆,还是建议写为 className 吧。

当有多个 class,并且需要动态变化时,可以使用 classnames 来简化代码。

至于 CSS 文件的处理,可以在 HTML 中直接引入,比如使用 bootstrap 时,可以直接在 HTML 中引入一个 CDN 地址;当使用自己编写的 CSS 文件时,建议将 CSS 文件对应 React 组件进行拆分,并在组件文件中使用 import 来导入,然后在 webpack 中配置 css-loader 进行处理。

src
 |-- components
          |-- Header
                |-- index.jsx
                |-- index.css
          |-- Footer
                |-- index.jsx
                |-- index.css
// src/components/Header/index.jsx
import React from 'react';
import './index.css';
 
const Header = () => {
  return (
    <h1 className="header">Heading 1</h1>
  );
};
 
export default Header;
/* src/components/Header/index.css */
.header {
  color: #333;
}

这里需要注意,组件拆分不等于样式隔离。当一个页面上同时引用多个组件时,会同时引用这些组件的 CSS 文件,因为原生 CSS 没有作用域的概念,所有 class 全局有效,所以这些 CSS 可能会发生冲突;如果配置了代码分割,在页面切换时,也可能会出现多个页面间的样式冲突,因为单页面应用本质上只是一个页面,当从 A 页面切换到 B 页面时加载了 B 页面的 CSS 文件,但并没有销毁 A 页面的 CSS,两者是共存的。

为了解决样式作用域的问题,可以使用 BEM 等命名规则来尽可能地避免命名重复,但这不能从根本上解决问题。CSS Modules 的诞生就是为了解决这个问题的。

3. CSS Modules

CSS Modules 是通过构建工具来实现 CSS 作用域的效果。原理很简单,就是把 class 名和动画名按照一定的规则做修改,使得 class 名全局唯一,从而避免命名冲突的问题。

/* App.css */
 
/* 编译前 */
.home {
  color: #333;
}
/* 编译后 */
.App_home__T45xz {
  color: #333;
}

使用也很简单,以 webpack 为例,在 css-loader 的配置项中添加一条 modules: true 即可。在代码中使用的时候也有点变化:

import React from 'react';
import styles from './App.css';
 
const App = () => {
  return (
    <div className={styles.home}>App</div>
  );
};
 
export default App;

导入的 styles 是个对象,是原 class 名和编译后 class 名的映射,如:{ home: 'App_home__T45xz' }

除了修改 class 名,CSS Modules 还有 class 组合、导入其他文件中的 class 等高级功能,并且可以通过 css-loader 的配置项来自定义命名规则等,具体内容可以参考 css-loader 的文档

不论是常规的导入一个 CSS 文件,还是使用 CSS Modules,都和 Less、Sass 等不冲突,在 webpack 配置中的 css-loader 前加一个对应的 loader 即可。

4. CSS in JS

前面的几种方法都比较常规,都没有脱离常规的 HTML + CSS 的心智模型。

在 jQuery 时代的 Web 开发中,我们注重“关注点分离” - 将 HTML、CSS 和 JavaScript 拆分开,互不耦合。但是 React 通过 JSX 把 HTML 和 JavaScript 结合到了一起,更注重组件化。那有没有用 JS 的方式来写 CSS 的解决方案,来实现 All in JS 呢?

CSS in JS 就是通过 JS 的方式来写 CSS 的解决方案的统称,不特指某一种解决方案。由于方案众多,本文只介绍两个。

4-1. styled-components

styled-components 使用模板字符串语法将 CSS 融入到 React 的组件系统,通过 JS 实现一些 CSS 原本不具备的功能,比如变量、循环和函数等。虽然这些功能可以通过 Less 或 Sass 实现,但 styled-components 的这些功能是通过 JS 实现的,学习成本很低,甚至没有学习成本。

推荐你在开始使用之前配置 babel-plugin-styled-components Babel 插件,它可以提供一些优化和 SSR 的支持,不过这个插件是可选的。

基本用法

import React from 'react';
import styled from 'styled-components';
 
const Title = styled.h1`
  color: #333;
`;
 
const App = () => (
  <Title>Heading 1</Title>
);
 
export default App;

在你定义样式的时候,其实是生成了一个包含样式的 React 组件。

动态样式

既然生成的是个 React 组件,那就可以传递 props,样式中可以读取到这些 props 值,并根据 props 值动态调整样式:

import React from 'react';
import styled from 'styled-components';
 
const Title = styled.h1`
  color: #333;
  font-weight: ${props => props.bold ? 700 : 400};
`;
 
const App = () => (
  <Title bold={true}>Heading 1</Title>
);
 
export default App;

扩展样式

上面的例子都是给一个 HTML 标签添加样式,也可以给一个组件添加样式,并且会覆盖已有的样式:

import React from 'react';
import styled from 'styled-components';
 
const Title = styled.h1`
  color: #333;
  font-weight: ${props => props.bold ? 700 : 400};
`;
 
const ItalicTitle = styled(Title)`
  font-style: italic;
`;
 
const App = () => (
  <>
    <Title bold={true}>Heading 1</Title>
    <ItalicTitle bold={true}>Heading 1</ItalicTitle>
  </>
);
 
export default App;

如果你不想修改样式,而是想修改 HTML 标签,可以传一个 as prop,值可以是一个 HTML 标签的字符串,也可以是一个 React 组件。

<Title as={'h2'} bold={true}>Heading 1</Title>

这样渲染出来的 DOM 节点就是 h2 标签。

更多更详细的功能可以查看 styled-components 的官方文档

4-2. styled-jsx

从名字能看出来,styled-components 是将样式与组件绑定,而 styled-jsx 则更进一步,直接将样式绑定到 JSX 中,作用域的控制粒度更细。

在开始使用之前需要先在 Babel 配置中添加 styled-jsx/babel 插件。

基本用法

import React from 'react';
 
const App = () => {
  return (
    <div>
      <h1>
        Heading 1
        <style jsx>{`
          h1 {
            color: red;
          }
        `}</style>
      </h1>
    </div>
  );
};
 
export default App;

现在 style 标签内定义的 h1 标签选择器的样式仅在包裹它的 h1 标签内生效,对外部再添加的 h1 标签不会生效。

动态样式

既然样式写在 JSX 里面,那它就能读取到当前组件的 props 和 state,进而根据状态来动态调整样式:

import React, { useState } from 'react';
 
const App = () => {
  const [bold, setBold] = useState(true);
  return (
    <div>
      <h1>
        Heading 1
        <style jsx>{`
          h1 {
            color: red;
            font-weight: ${bold ? 700 : 400};
          }
        `}</style>
      </h1>
      <button onClick={() => setBold(!bold)}>Toggle</button>
    </div>
  );
};
 
export default App;

全局样式

总会有些情况我们需要写一个全局样式,需要写全局样式时,给 style 标签添加个 global 属性即可:

<div>
  <style jsx global>
    body {
      margin: 0;
      padding: 0;
    }
  </style>
</div>

也可以给某个 class 添加个 :global(),仅对这一个 class 全局化,CSS Modules 中也有这种用法:

<div>
  <style jsx>
    :global(.header) {
      color: #333;
    }
  </style>
</div>

更多用法和配置可以查看 styled-jsx 的官方文档

5. 最后

有些人也会把 CSS Modules 归类为 CSS in JS,但是它并没有脱离 HTML 和 CSS 分离的心智模型,并且编译后没有 runtime,本文就不把它归类为 CSS in JS 了。

CSS in JS 解决方案众多,虽然用法不同,但基本思路都是将 JS 的能力赋予 CSS,其最大收益是可以实现动态调整样式,在切换主题、Dark Mode 等类似需求中尤其好用。由于他们在编译后也都存在 runtime,所以相较于传统方案性能会有所降低。

6. 参考链接