如何在 React 中优雅的写 CSS

  • 2019 年 12 月 20 日
  • 筆記

本文首发于政采云前端团队博客:如何在 React 中优雅的写 CSS https://www.zoo.team/article/react-css

引言

问题:CSS 文件分离 != CSS 作用域隔离

看下这样的目录结构:

├── src  │   ├──......                   # 公共组件目录  │   ├── components              # 组件  │   │   └──comA                 # 组件A  │   │       ├──comA.js  │   │       ├──comA.css  │   │       └── index.js  │   │   └──comB                 # 组件B  │   │       ├──comB.js  │   │       ├──comB.css  │   │       └── index.js  │   ├── routes                  # 页面模块  │   │   └── modulesA            # 模块A  │   │       ├──pageA.js         # pageA JS 代码  │   │       ├──pageA.css        # pageA CSS 代码  

看目录结构清晰明了,由于“ CSS 文件分离 != CSS 作用域隔离”这样的机制,如果我们不通过一些工具或规范来解决 CSS 的作用域污染问题,会产生非预期的页面样式渲染结果。

假设我们在组件 A 和组件 B import 引入 comA.css 和 comB.css。

comA.css

.title {      color: red;  }  

comB.css

.title {      font-size: 14px;  }  

最后打包出来的结果为:

.title {      color: red;  }  .title {      font-size: 14px;  }  

我们希望,comA.css 两者互不影响,可以发现,虽然 A、B 两个组件分别只引用了自己的 CSS 文件,但是 CSS 并没有隔离,两个 CSS 文件是相互影响的!

随着 SPA 的流行,JS 可以组件化,按需加载(路由按需加载、组件的 CSS 和 JS 都按需加载),这种情况下 CSS 作用域污染的问题被放大,CSS 被按需加载后由于 CSS 全局污染的问题,在加载出其他一部分代码后,可能导致现有的页面上会出现诡异的样式变动。这样的问题加大了发布的风险以及 debugger 的成本。

小编我从写 Vue 到写 React , Vue 的 scoped 完美的解决了 CSS 的作用域问题,那么 React 如何解决 CSS 的作用域问题呢?

解决 React 的 CSS 作用域污染方案:

  • 方案一:namespaces
  • 方案二:CSS in JS
  • 方案三:CSS Modules

方案一:namespaces

“利用约定好的命名来隔离 CSS 的作用域

comA.css

.comA .title {      color: red;  }  .comA .……{      ……  }  

comB.css

.comB .title {      font-size: 14px;  }  .comB .……{      ……  }  

嗯,用 CSS 写命名空间写起来貌似有点累。

没事我们有 CSS 预处理器,利用 less、sass、stylus 等预处理器,代码依然简洁。

A.less

.comA {      .title {          color: red;      }        .…… {          ……      }  }  

B.less

.comB {      .title {          font-size: 14px;      }        .…… {          ……      }  }  

貌似很完美解决了 CSS 的作用域问题,但是问题来了,假设 AB 组件是嵌套组件。

那么最后的渲染 DOM 结构为:

<div class="comA">      <h1 class="title">组件A的title</h1>      <div class="comB">          <h1 class="title">组件组件的title</h1>      </div>  </div>  

comA 的样式又成功作用在了组件 B 上。

没关系,还有解,所有的 class 名以命名空间为前缀。

<div class="comA">      <h1 class="comA__title">组件A的title</h1>      <div class="comB">          <h1 class="comB__title">组件组件的title</h1>      </div>  </div>  

A.less

.comA {      &__title {          color: red;      }  }  

B.less

.comB {      &__title {          font-size: 14px;      }  }  

如果,我们的样式还遵循 BEM (Block, Element, Modifier) 规范,那么,样式名简直不要太长!但是问题确实也解决了,但约定毕竟是约定,靠约定和自觉来解决问题毕竟不是好方法,在多人维护的业务代码中这种约定来解决 CSS 污染问题也变得很难。

方案二:CSS in JS

“使用 JS 语言写 CSS,也是 React 官方有推荐的一种方式。

从 React 文档进入

https://github.com/MicheleBertoli/css-in-js ,可以发现目前的 CSS in JS 的第三方库有 60 余种。

看两个比较大众的库:

  • reactCSS
  • styled-components

reactCSS

“支持 React 、Redux、React Native、autoprefixed、Hover、伪元素和媒体查询(http://reactcss.com/)

看下官网文档 :

const styles = reactCSS({    'default': {      card: {        background: '#fff',        boxShadow: '0 2px 4px rgba(0,0,0,.15)',      },    },    'zIndex-2': {      card: {        boxShadow: '0 4px 8px rgba(0,0,0,.15)',      },    },  }, {    'zIndex-2': props.zIndex === 2,  })  
class Component extends React.Component {    render() {      const styles = reactCSS({        'default': {          card: {            background: '#fff',            boxShadow: '0 2px 4px rgba(0,0,0,.15)',          },          title: {            fontSize: '2.8rem',            color: this.props.color,          },        },      })      return (        <div style={ styles.card }>          <div style={ styles.title }>            { this.props.title }          </div>          { this.props.children }        </div>      )    }  }  

可以看出,CSS 都转化成了 JS 的写法,虽然没有学习成本,但是这种转变还是有一丝不适。

styled-components

“styled-components,目前社区里最受欢迎的一款 CSS in JS 方案(https://www.styled-components.com/)

const Button = styled.a`    /* This renders the buttons above... Edit me! */    display: inline-block;    border-radius: 3px;    padding: 0.5rem 0;    margin: 0.5rem 1rem;    width: 11rem;    background: transparent;    color: white;    border: 2px solid white;    /* The GitHub button is a primary button     * edit this to target it specifically! */    ${props => props.primary && css`      background: white;      color: palevioletred;    `}  `  render(    <div>      <Button        href="https://github.com/styled-components/styled-components"        target="_blank"        rel="noopener"        primary      >        GitHub      </Button>      <Button as={Link} href="/docs" prefetch>        Documentation      </Button>    </div>  )  

与 reactCSS 不同,styled-components 使用了模板字符串,写法更接近 CSS 的写法。

方案三:CSS Modules

“利用 webpack 等构建工具使 class 作用域为局部。

CSS 依然是还是 CSS

例如 webpack,配置 css-loader 的 options modules: true。

module.exports = {    module: {      rules: [        {          test: /.css$/,          loader: 'css-loader',          options: {            modules: true,          },        },      ],    },  };  

modules 更具体的配置项参考:https://www.npmjs.com/package/css-loader

loader 会用唯一的标识符 (identifier) 来替换局部选择器。所选择的唯一标识符以模块形式暴露出去。

示例:

webpack css-loader options

options: {    ...,    modules: {      mode: 'local',      // 样式名规则配置      localIdentName: '[name]__[local]--[hash:base64:5]',    },  },  ...  

App.js

...  import styles from "./App.css";  ...  <div>    <header className={styles["header__wrapper"]}>      <h1 className={styles["title"]}>标题</h1>      <div className={styles["sub-title"]}>描述</div>    </header>  </div>  

App.css

.header__wrapper {    text-align: center;  }    .title {    color: gray;    font-size: 34px;    font-weight: bold;  }    .sub-title {    color: green;    font-size: 16px;  }  

编译后端的 CSS,classname 增加了 hash 值。

.App__header__wrapper--TW7BP {    text-align: center;  }    .App__title--2qYnk {    color: gray;    font-size: 34px;    font-weight: bold;  }    .App__sub-title--3k88A {    color: green;    font-size: 16px;  }  

总结

(1)如果是 ui 组件库中使用

“建议使用 namespaces 方案

原因:

  • ui 组件库维护人员基本固定,遵守约定的规范较为容易,可通过约定规范来解决不同组件 CSS 相互影响问题
  • 由于 ui 组件库会应用于整个公司的产品,在真正的业务场景中,虽然不建议,但是可能无法避免需要覆盖组件样式的特殊场景,如使用其他两种方式,不能支持组件样式覆盖

(2)如果是业务代码/业务组件中使用

“CSS in JS / CSS Modules

业务代码维护人员较多且不固定、代码水平不一致,只通过规范来约束不靠谱,无法保证开发人员严格遵守规范,不能根治 CSS 交叉影响问题,但是从 debug 角度考虑,建议组件外层都添加一个 namespaces 方面定位组件。然后加之 CSS in JS 或 CSS Modules 方案来解决 CSS 交叉影响问题。

CSS in JS 和 CSS Modules 谁优谁胜?

CSS Modules 会比 CSS in JS 的侵入性更小,CSS in JS 可以和 JS 共享变量,但个人更喜欢 CSS Modules ,但是谁优谁胜无法武断。

  • 如果你的团队还没有使用这任一技术,需要考虑的是团队成员的感受
  • 如果已经在使用其中某一种方案,保持一致性即可,相信并这样走下去