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

FateRiddle發表於2018-07-14

If you are not paying for the product, you are the product.
當一個商品是“免費”的,那往往你成了商品。

終於有時間靜下心學點東西,把這個系列最後一篇填上。 中篇 介紹了 tachyons, 本篇介紹個人的最愛,沒有之一:styled-jsx。

選擇標準

在打算使用一個新 css 框架前,需要好好想清楚它是否在你的所有使用場景裡圓滿完成任務,我總結了幾點:

  • 是否解決了React開發的痛點:區域性css,動態css?
  • 是否支援所有css甚至是sass用法?偽類,巢狀,動畫,媒體查詢?
  • 是否相容你需要使用的第三方UI庫?
  • 是否能和純css,或者是其他css框架很好共存,以備遇到特殊情況可以有方案B?
  • 效能?大小?

styled-jsx

zeit 的一系列產品從 now,到 next.js,我算是一個腦殘粉。簡潔好用是我對 zeit 的專案的印象。而且一套庫自成系統,styled-jsx 和 next.js 完美相容。

1. 基礎用法

styled-jsx 概括第一印象就是 React css 的 vue 解決。
yarn add styled-jsx 安裝後,不用import,而是一個babel外掛,.babelrc配置:

{
  "plugins": [
    "styled-jsx/babel"
  ]
}
複製程式碼

然後就直接用了


render () {
    return <div className='table'>
        <div className='row'>
            <div className='cell'>A0</div>
            <div className='cell'>B0</div>
        </div>
        <style jsx>{`
          .table {
            margin: 10px;
          }
          .row {
            border: 1px solid black;
          }
          .cell {
            color: red;
          }
    `}</style>
    </div>;
}
複製程式碼
  • <style>的位置可以按喜好自定,樣式總是作用於元件的所有元素
  • 樣式只作用於本元件,不影響全域性也不影響子元件
  • 實現方式大致是給元件內所有標籤自動加上了一個獨特className 例如
const App = () => (
  <div>
    <p>只有這個p會被上樣式</p>
    <style jsx>{`
      p {
        color: red;
      }
    `}</style>
  </div>
)
複製程式碼

會被轉化成

import _JSXStyle from 'styled-jsx/style'

const App = () => (
  <div className='jsx-123'>
    <p className='jsx-123'>只有這個p會被上樣式</p>
    <_JSXStyle styleId='123' css={`p.jsx-123 {color: red;}`} />
  </div>
)
複製程式碼

從實現到原理,對比vue是不是非常像呢?如果你不喜歡將樣式寫在 render 裡,styled-jsx 提供了一個 css 的工具函式:

import css from 'styled-jsx/css'

export default () => (
  <div>
    <button>styled-jsx</button>
    <style jsx>{button}</style>
  </div>
)

const button = css`button { color: hotpink; }`
複製程式碼

非常完美的css解決方案。 下面針對“選擇標準”裡提到的各個方面考察一下 styled-jsx

2. 動態css

和 styled-components,Motion 等模板字串的解決方案一樣,動態css輕而易舉

export default (props) => (
  <div>
    <button>styled-jsx</button>
    <style jsx>{
      `button { color: ${props.color}; }`
    }</style>
  </div>
)
複製程式碼

同個元件裡可以寫無限個<style>標籤,這裡的最佳實踐是將靜態的css放一個標籤,動態的放另一個,每次渲染時只有動態的重新計算和渲染

const Button = (props) => (
  <button>
     { props.children }
     <style jsx>{`
        button {
          color: #999;
          display: inline-block;
          font-size: 2em;
        }
     `}</style>
     <style jsx>{`
        button {
          padding: ${ 'large' in props ? '50' : '20' }px;
          background: ${props.theme.background};
        }
     `}</style>
  </button>
)
複製程式碼

兩點注意:

  • 只有寫在 render 函式裡的style是動態的,所以動態css不能如第二例那樣提取出來
  • 原聲解決方法的 style props的樣式會覆蓋 styled-jsx 的樣式

3. 如何使用Sass

兩個字,外掛。

一應俱全。以Sass為例:

yarn add -D node-sass styled-jsx-plugin-sass
複製程式碼

.babelrc配置

{
  "plugins": [
    [
      "styled-jsx/babel",
      { "plugins": ["styled-jsx-plugin-sass"] }
    ]
  ]
}
複製程式碼

即可使用Sass。

4. 全域性css

如同 Vue 以 scoped 為關鍵字,styled-jsx 以 global 為關鍵字。

export default () => (
  <div>
    <style jsx global>{`
      body {
        background: red
      }
    `}</style>
  </div>
)
複製程式碼

全域性樣式註明 global 即可。

有極少情況(比如傳一個全域性類給三方庫)需要使單個選擇器全域性化,語法類似 css-module

div :global(.react-select) {
    color: red
}
複製程式碼

5. 三方UI庫的支援

相對有點繁瑣,思想是取得styled-jsx轉化過後的類名,注入到三方庫的className props裡,這樣即解決了支援,又保全了區域性css,程式碼如下

import Link from 'react-router-dom' // 例子,給Link元件新增樣式

const scoped = resolveScopedStyles(
  <scope>
    <style jsx>{'.link { color: green }'}</style>
  </scope>
)

const App = ({ children }) => (
  <div>
    {children}
    <Link to="/about" className={`link ${scoped.className}`}>
      About
    </Link>
    
    {scoped.styles}
  </div>
);

function resolveScopedStyles(scope) {
  return {
    className: scope.props.className, //就是被styled-jsx新增的獨特className
    styles: scope.props.children      //就是style,注入到App元件中
  }
}
複製程式碼

當然,如果你不介意區域性不區域性,可以使用上面提到的:global() 語法

// 比如Form元件有類名form-item
export default () => (
  <div>
    <Form />
    <style jsx>{`
      div > :global(.form-item) {
        color: red
      }
    `}</style>
  </div>
)
複製程式碼

6. 語法高亮與補完

我使用VSCode:

其他的見 github 上 readme。

7. 大小?效能?

  • 可使用所有css語法
  • 3kb的gzip大小
  • 瀏覽器的prefix自動加了
  • 有source-map 支援,方便debug
  • 據作者說效能也很高

8. style-jsx vs styled-components

謝謝ziven27提議,我試著說說和現在最流行的 styled-components 庫的區別。(方便討論,“前者”代指style-jsx,“後者”代指style-components)

  • 最大區別在於前者樣式作用於整個元件,後者的樣式只作用於所包裹的元素。打個比方就如同vue和inline css的區別。但其實只要使用sass的&巢狀,後者也是可以包裹元件最外層元素然後將其他元素樣式統統寫入的。雖然感覺不是後者的初衷。
  • 後者與提供className props的三方庫完美整合(比前者簡潔太多):
const RedButton = styled(Button)`background: red;`
複製程式碼

但對不提供className props的三方庫一籌莫展(當然這種情況不太出現)。

  • 後者由於使用了HOC模式,與其他所有HOC一樣要處理ref的問題(包裹層的ref不同於原本元素的ref),多一事。
  • 個人覺得前者的規則簡潔明瞭(css怎麼寫就怎麼寫),後者有不少“魔術”的部分(比如props,theme),也引入了很多高階用法如.extends 樣式繼承以及.attrs封裝公用props。

這裡提到一個話題,到底是“魔術”好呢,還是樸實好呢?舉個例子,比如theme(主題):

// styled-components 使用Provider提供主題,主題可以是樣式,也可以是函式
const Button = styled.button`
  color: ${props => props.theme.fg};
  border: 2px solid ${props => props.theme.fg};
  background: ${props => props.theme.bg};

  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
  border-radius: 3px;
`;

// 主題的樣式
const theme = {
  fg: 'palevioletred',
  bg: 'white'
};

// 換前景色和背景色的函式
const invertTheme = ({ fg, bg }) => ({
  fg: bg,
  bg: fg
});

render(
  <ThemeProvider theme={theme}>
    <div>
      <Button>Default Theme</Button>

      <ThemeProvider theme={invertTheme}>
        <Button>Inverted Theme</Button>
      </ThemeProvider>
    </div>
  </ThemeProvider>
);
複製程式碼

甚至還提供了一個HOC,在styled-components外使用theme

import { withTheme } from 'styled-components'
複製程式碼

強大不強大?對比之下,styled-jsx沒有任何與theme相關的api,只是樸樸實實地靠props傳遞達成主題:

// styled-jsx
import { colors, spacing } from '../theme'
import { invertColor } from '../theme/utils'

const Button = ({ children }) => (
  <button>
     { children }
     <style jsx>{`
        button {
          padding: ${ spacing.medium };
          background: ${ colors.primary };
          color: ${ invertColor(colors.primary) };
        }
     `}</style>
  </button>
)
複製程式碼

這個問題要看應用場景和個人喜好吧,我屬於不喜歡過多“魔術”,愛簡單api(哪怕多點程式碼)的那派。大家有啥想法和指正,多多留言。

最後

有2個細小缺點

  1. 和CRA整合時,css-in-js 的解決方式不會熱載入,每次都重新整理頁面。
  2. 和CRA整合時,由於是babel外掛,需要eject或者使用react-app-rewired來加入 babel plugin 的設定。

之前有小夥伴留言表示有坑(謝謝,看到這樣的留言覺得寫blog的決定太對了)

同樣覺得styled-jsx非常好,最近一直在用。
但目前還是有些不成熟的地方,主要是:
1、區域性樣式無法支援第三方元件標籤,只能支援普通html標籤
2、對stylelint缺乏支援,官方出的stylelint外掛只是一個demo。在vscode中用stylelint外掛時也是有很大的坑。
複製程式碼

個人覺得1的場景已經在上面討論了,可以相容。2的話,本人上個專案裡裸prettier,所以沒試過那個外掛,在這裡提一下。

總之目前用過一次,我很喜歡,也很滿意。希望大家也找到自己喜歡的一個或者是一套解決方法。

最近面試,明白了自己有許多需要重點研究的課題。看了些 PWA 相關的文和視訊,已經上船了。一句話:PWA 是未來,而且是近在眼前的未來。之後打算分享一下自己的學習筆記。

我寫的其他專欄文章列表 傳送門

相關文章