React拾遗:从10种现在流行的 CSS 解决方案谈谈我的最爱 (上)

27,516 阅读8分钟

Strong opinions are very useful to others.

Those who were undecided or ambivalent can just adopt your stance.

But those who disagree can solidify their stance by arguing against yours

鲜明的观点非常有用。摇摆不定的人可以省心直接接受你的观点。不同意的人则可以通过讨论更加巩固自己的观点。

----Derek Sivers

前言

不得不承认 Vue 的css解决方式非常自然简洁,相比之下 css 一直是 React 的痛。 从旧宠 css modules 到 JSS 的各种衍生,到新宠 styled-components。几十种的解决方式,上百篇的教程和比较,已经说明了一切。大家一直在寻找最好的最适合自己的解决方式。 我试着先回顾一下一路下来用过和没用过的各种React的css解决方案,最后说说我最爱的方式。当然只要你喜欢,使用普通的 css 或者是 sass 来完成React的样式是完全可行的。用自己最喜欢的方式编程是最重要的。

普通 css 的不足

随着大家对新开发模式(组件化)下 css 使用的各种反思,个人总结主要有三个:

  1. 样式与状态相关的情况越来越多,需要动态、能直接访问组件state的css。
  2. 现代web开发已经是组件化的天下,而css并不是为组件化而生的语言。
  3. 一切样式都是全局,产生的各种命名的痛苦,BEM等命名规则能解决一部分问题,但当你使用三方插件时却无法避免命名冲突。

Vue 的解决法

<style>
/* 全局样式 */
</style>

<style scoped>
/* 本地样式 */
</style>

一旦加上 scoped 属性,css 就只作用于所在组件。简洁漂亮的解决。美中不足的是样式并不能直接访问组件状态,于是乎需要另外规定动态css的语法与此合并使用。

回顾 React 的解决法

1. 原生

const textStyles = {
  color: 'white',
  backgroundColor: this.state.bgColor
};

<p style={textStyles}>inline style</p>

原生的解决方式就是inline style,这种在旧式开发上不推崇的css写法却非常适合组件化开发。inline style解决了之前提到的三个问题。但相对的,个人觉得不喜欢的地方在于:

  1. 发明了一套新的 css-in-js 语法,使用驼峰化名称等一些规则,需要重新熟悉不说,也没有自动补完(方便讨论下面称这类写法jss)
  2. 并且并不支持所有的 css,例如媒体查询,:before:nth-child等 pseudo selectors
  3. inline 写法如果直接同行写影响代码阅读,如果提取出来再namespace,比起传统css要繁琐
  4. 第三方插件如果只接受 className 不接受 style 就没法了

由于1,3只是个人偏好问题,所以之后一批css-in-js库都坚持了inline和jss,只是致力于解决对css的不完全支持问题。这些虽然不是我的菜,但都是流行的解决方式:

2. Css-in-Js

JSS

专门针对原生方法不完全支持css的不足,完成的改良版。

// 支持 hover, sass的 &, media query 等。 
const styles = {
  button: {
    fontSize: 12,
    '&:hover': {
      background: 'blue'
    }
  },
  ctaButton: {
    extend: 'button',
    '&:hover': {
      background: color('blue')
        .darken(0.3)
        .hex()
    }
  },
  '@media (min-width: 1024px)': {
    button: {
      width: 200
    }
  }
}

JSS 是一个底层库,要在 React 中使用可以用 React-JSS, Styled-JSS 等。选择多样,是此类解决法里不错的一个选择。

Radium

import Radium from 'radium';

const Button = () => (
    <button
        style={styles.base}>
        {this.props.children}
    </button>;
)

var styles = {
  red: {
    backgroundColor: 'red'
  }
};

Button = Radium(Button);

多个样式使用数组方便合并

<button
    style={[styles.base,styles.primary]}>
    {this.props.children}
</button>

使用了HOC的方式注入样式,可以方便传入各种配置

Radium(config)(App)

Aphrodite

import React, { Component } from 'react';
import { StyleSheet, css } from 'aphrodite';

const Button = () => (
    <span className={css(styles.red)}>
        This is red.
    </span>
)

const styles = StyleSheet.create({
    red: {
        backgroundColor: 'red'
    },
});

并不是所有人都接受jss的书写。所以另一种解决方式是继续使用 css 的同时,解决样式的scope 问题,使得样式只作用于import它的组件。这类解决方法目前就一家

3. Css Modules

Css Modules 并不是React专用解决方法,适用于所有使用 webpack 等打包工具的开发环境。以 webpack 为例,在 css-loader 的 options 里打开modules:true 选项即可使用 Css Modules。
一般配置如下

{
  loader: "css-loader",
  options: {
    importLoaders: 1,
    modules: true,
    localIdentName: "[name]__[local]___[hash:base64:5]"  // 为了生成类名不是纯随机
  },
},

使用如下

import styles from './table.css';

    render () {
        return <div className={styles.table}>
            <div className={styles.row}>
                <div className={styles.cell}>A0</div>
                <div className={styles.cell}>B0</div>
            </div>
        </div>;
    }
/* table.css */
.table {}
.row {}
.cell {}

在解决了 scoped 的同时留下些许遗憾:

  1. class名必须是驼峰形式,否则不能正常在js里使用 styles.table 来引用
  2. 由于css模块化是默认,当你希望使用正常的全局css时,需要通过:local:global 切换,不方便
  3. 所有的 className 都必须使用 {style.className} 的形式

这个解决方法可以照常写css是一大优势,不过我对 2,3 比较不能容忍。一个轻量级的 babel-plugin-react-css-modules库提出了解决法,你可以照常写 'table-size' 之类带横杠的类名,在js里正常书写字符串类名,唯一的区别在于使用 styleName 关键字代替 className, 以上例,结果如下

import './table.css';

render () {
        return <div styleName='table'>
            <div styleName='row'>
                <div styleName='cell'>A0</div>
                <div styleName='cell'>B0</div>
            </div>
        </div>;
    }

使用styleName这一新关键字,甚至连局部css和全局css的区分也迎刃而解了。

<div className='global-css' styleName='local-module'></div>

使用时.babelrc配置:

{
  "plugins": [
    ["react-css-modules", {
      // options
    }]
  ]
}

这一解决法已经很接近我的喜好了,不过使用 styleName 遇到三方UI库该怎么办呢?

顺带一提,目前 create-react-app 还不支持 Css Modules,但处于 beta 的 create-react-app v2 已经支持。使用方法为一律将css文件命名为 XXX.modules.css, 以上例,即为 table.modules.css, 即可使用。这一解决法的优雅在于,全局的css可以正常使用,只有带.modules.css后缀的才会被modules化。

Css Modules还有一大缺憾:和Vue的解决一样,因为css写在css文件,无法处理动态css。

4. Css-in-Js 新浪潮

有很多人并不买JSS的帐(我算一个),Vue 的解决方式也算一个启发,于是新的库尝试了使用 ES6 的模板字符串,在js文件里写纯粹的css。

`
  .table {
      background: #333;
      color: rebeccapurple;
  }
`

这非常自然地解决了前面提到的原生方法缺陷之2:不支持所有css语法。这类解决法中最有名的是 styled-components,类似的还有 Emotion。起初这一方法的一大不便是编辑器不能格式化,lint和自动补完js中的css,但现在基本每个流行编辑器都能找到相应的插件解决这一问题。

styled-components

import styled from 'styled-components';

// `` 和 () 一样可以作为js里作为函数接受参数的标志,这个做法类似于HOC,包裹一层css到h1上生成新组件Title
const Title = styled.h1`
  font-size: 1.5em;
  text-align: center;
  color: palevioletred;
`;

// 在充分使用css全部功能的同时,非常方便的实现动态css, 甚至可以直接调用props!
const Wrapper = styled.section`
  padding: 4em;
  background: ${props => props.bgColor};
`;

const App = () => (
    <Wrapper bgColor='papayawhi'>
      <Title>Hello World, this is my first styled component!</Title>
    </Wrapper>
)

值得注意的是支持对其他元素引用,且语法相当自然:

const Link = styled.a`
  padding: 5px 10px;
  background: papayawhip;
  color: palevioletred;
`;

const Icon = styled.svg`
  transition: fill 0.25s;
  width: 48px;
  height: 48px;

  ${Link}:hover & {
    fill: rebeccapurple;
  }
`;

并且能方便的给暴露className props的三方UI库上样式:

const StyledButton = styled(Button)` ... `

有人喜欢给每个需要样式的标签重命名一个更有意义的名称的做法,但也有人觉得重命名非常繁琐。这类人可以试试Emotion

Emotion

import styled, { css } from 'react-emotion'

const Container = styled('div')`
  background: #333;
`
const myStyle = css`
  color: rebeccapurple;
`
const app = () => (
  <Container>
    <p className={myStyle}>Hello World</p>
  </Container>
)

Emotion 支持 styled-components 的样式注入方式,同时也可以用 css() 关键字直接注入。同时也支持JSS和& :hover等Sass语法,例如:

// sass
<div
    className={css`
      background-color: hotpink;
      &:hover {
        color: ${color};
      }
    `}
>
// JSS 
<div
    className={css({
      backgroundColor: 'hotpink',
      '&:hover': {
        color: 'lightgreen'
      }
    })}
>

之前没注意,Emotion 允许直接写子元素样式!

import { css } from 'emotion'

const paragraph = css`
  color: turquoise;

  a {
    border-bottom: 1px solid currentColor;
  }
`
render(
  <p className={paragraph}>
    Some text. <a>
      A link with a bottom border.
    </a>
  </p>
)

如果使用 babel-plugin-emotion,你甚至可以直接使用 css 作为 props:

<div
  css={`
    color: blue;
    font-size: ${props.fontSize}px;

    &:hover {
      color: green;
    }

    & .some-class {
      font-size: 20px;
    }
  `}
>

非常简洁也独具亮点,且提供了符合各种胃口的解决方式。

Glamorous

Glamorous 和前两个库有很多类似之处,最大的不同是它坚守了JSS的阵线。不支持模板字符串写纯css。 Glamorous 基础用法有三种选择:

import 
import glamor from 'glamorous'
// className 注入
const styles = glamor.css({
  fontSize: 20,
  textAlign: 'center',
})

<div
  className={styles}
/>

// 2. 类styled components
const MyStyledDiv = glamor.div({margin: 1, fontSize: 1, padding: 1})

// 3. 使用自带组件,接受样式名和css为props
const { Div } = glamor

<Div
  fontSize={20}
  textAlign="center"
  css={{
    display: 'flex',
    flexDirection: 'column',
    justifyContent: 'center'
  }}
>
  Hello world!
</Div>

// 4. 以及混用
const MyStyledDiv = glamor.div({margin: 1, fontSize: 1, padding: 1})
const myCustomGlamorStyles = glamor.css({fontSize: 2})
<MyStyledDiv className={`${myCustomGlamorStyles} custom-class`} />

最后谈谈,我的最爱

1. styled-jsx

Next.js 的 zeit 出品,必属精品。

2. 意想不到的解决方案 (个人大爱)

tachyons

tailwind.css

两个css库,与 bootstrap 走上完全不同的路线,所谓的“原子类”。写小项目和demo我基本上第一件事就是

yarn add tachyons

篇幅意想不到变得太长了,在下篇我会展开讲解这两个个人偏爱的解决方法。本篇对现行的流行解决法做了个小归纳。方便大家查询和选择。在本篇里我用的最多的还是styled-components,但现在已经切换到styled-jsx了。 之后最想尝试的应该是Emotion。说到底没有孰优孰劣,更多的是个人喜好。希望这篇归纳对大家有帮助。不足之处,也请大家能留言指出,互相学习,谢谢!

中篇:从10种现在流行的 CSS 解决方案谈谈我的最爱 (中)