React拾遺:從10種現在流行的 CSS 解決方案談談我的最愛 (上)

FateRiddle發表於2018-07-04

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 解決方案談談我的最愛 (中)

相關文章