CSS in JS的好與壞

是什麼

CSS-in-JS是一種技術(technique),而不是一個具體的庫實現(library)。簡單來講CSS-in-JS就是將應用的CSS樣式寫在JavaScript文件裏面,而不是獨立爲一些.css.scss或者less之類的文件,這樣你就能夠在CSS中使用一些屬於JS的諸如模塊聲明,變量定義,函數調用和條件判斷等語言特性來提供靈活的可擴展的樣式定義。值得一提的是,雖然CSS-in-JS不是一種很新的技術,但是它在國內普及度好像並非很高,它當初的出現是由於一些component-based的Web框架(例如React,Vue和Angular)的逐漸流行,使得開發者也想將組件的CSS樣式也一塊封裝到組件中去以解決原生CSS寫法的一系列問題。還有就是CSS-in-JS在React社區的熱度是最高的,這是由於React自己不會管用戶怎麼去爲組件定義樣式的問題,而Vue和Angular都有屬於框架本身的一套定義樣式的方案。javascript

本文將經過分析CSS-in-JS這項技術帶來的好處以及它存在的一些問題來幫助你們判斷本身是否是要在項目中使用CSS-in-JS。css

<!-- more-->html

不一樣的實現

實現了CSS-in-JS的庫有不少,據統計如今已經超過了61種。雖然每一個庫解決的問題都差很少,但是它們的實現方法和語法卻截然不同。從實現方法上區分大致分爲兩種:惟一CSS選擇器和內聯樣式(Unique Selector VS Inline Styles)。接下來咱們會分別看一下對應於這兩種實現方式的兩個比較有表明性的實現:styled-componentsradium前端

Styled-components

Styled-components 應該是CSS-in-JS最熱門的一個庫了,到目前爲止github的star數已經超過了27k。經過styled-components,你可使用ES6的標籤模板字符串語法(Tagged Templates)爲須要styled的Component定義一系列CSS屬性,當該組件的JS代碼被解析執行的時候,styled-components會動態生成一個CSS選擇器,並把對應的CSS樣式經過style標籤的形式插入到head標籤裏面。動態生成的CSS選擇器會有一小段哈希值來保證全局惟一性來避免樣式發生衝突。java

CSS-in-JS Playground是一個能夠快速嘗試不一樣CSS-in-JS實現的網站,上面有一個簡單的用styled-components實現表單的例子:react

從上面的例子能夠看出,styled-components不須要你爲須要設置樣式的DOM節點設置一個樣式名,使用完標籤模板字符串定義後你會獲得一個styled好的Component,直接在JSX中使用這個Component就能夠了。接着讓咱們打開DevTools查看一下生成的CSS:git

從上面DevTools能夠看出styled的Component樣式存在於style標籤內,並且選擇器名字是一串隨機的哈希字符串,這樣其實實現了局部CSS做用域的效果(scoping styles),各個組件的樣式不會發生衝突。除了styled-components,採用惟一CSS選擇器作法的實現還有:jssemotionglamorous等。github

Radium

Radium是由FormidableLabs建立的在github上有超過7.2k star的CSS-in-JS庫。Radium和styled-components的最大區別是它生成的是標籤內聯樣式(inline styles)。因爲標籤內聯樣式在處理諸如media query以及:hover:focus:active等和瀏覽器狀態相關的樣式的時候很是不方便,因此radium爲這些樣式封裝了一些標準的接口以及抽象。web

再來看一下radium在CSS-in-JS Playground的例子:npm

從上面的例子能夠看出radium定義樣式的語法和styled-components有很大的區別,它要求你使用style屬性爲DOM添加相應的樣式。打開DevTools查看一下radium生成的CSS:

從DevTools上面inspect的結果能夠看出,radium會直接在標籤內生成內聯樣式。內聯樣式相比於CSS選擇器的方法有如下的優勢:

  • 自帶局部樣式做用域的效果,無需額外的操做
  • 內聯樣式的權重(specificity)是最高的,能夠避免權重衝突的煩惱
  • 因爲樣式直接寫在HTML中,十分方便開發者調試

其餘區別

不一樣的CSS-in-JS實現除了生成的CSS樣式和編寫語法有所區別外,它們實現的功能也不盡相同,除了一些最基本的諸如CSS局部做用域的功能,下面這些功能有的實現會包含而有的卻不支持:

  • 自動生成瀏覽器引擎前綴 - built-in vendor prefix
  • 支持抽取獨立的CSS樣式表 - extract css file
  • 自帶支持動畫 - built-in support for animations
  • 僞類 - pseudo classes
  • 媒體查詢 - media query
  • 其餘

想了解更多關於不一樣CSS-in-JS的對比,能夠看一下Michele Bertoli整理的不一樣實現的對比圖

好處

看完了一些不一樣的實現,你們應該對CSS-in-JS一些基本的概念和用法有了大概的理解,接着咱們能夠來聊一下CSS-in-JS都有什麼好處和壞處了。

局部樣式 - Scoping Styles

CSS有一個被你們詬病的問題就是沒有本地做用域,全部聲明的樣式都是全局的(global styles)。換句話來講頁面上任意元素只要匹配上某個選擇器的規則,這個規則就會被應用上,並且規則和規則之間能夠疊加做用(cascading)。SPA應用流行了以後這個問題變得更加突出了,由於對於SPA應用來講全部頁面的樣式代碼都會加載到同一個環境中,樣式衝突的機率會大大加大。因爲這個問題的存在,咱們在平常開發中會遇到如下這些問題:

  • 很難爲選擇器起名字。爲了不和頁面上其餘元素的樣式發生衝突,咱們在起選擇器名的時候必定要深思熟慮,起的名字必定不能太普通。舉個例子,假如你爲頁面上某個做爲標題的DOM節點定義一個叫作.title的樣式名,這個類名很大機率已經或者將會和頁面上的其餘選擇器發生衝突,因此你不得不手動爲這個類名添加一些前綴,例如.home-page-title來避免這個問題。
  • 團隊多人合做困難。當多我的一塊兒開發同一個項目的時候,特別是多個分支同時開發的時候,你們各自取的選擇器名字可能有會衝突,但是在本地獨立開發的時候這個問題幾乎發現不了。當你們的代碼合併到同一個分支的時候,一些樣式的問題就會隨之出現。

CSS-in-JS會提供自動局部CSS做用域的功能,你爲組件定義的樣式會被限制在這個組件,而不會對其餘組件的樣式產生影響。不一樣的CSS-in-JS庫實現局部做用域的方法可能有所不同,通常來講它們會經過爲組件的樣式生成惟一的選擇器來限制CSS樣式的做用域。如下是一個簡化了的CSS-in-JS庫生成惟一選擇器的示例代碼:

const css = styleBlock => {
  const className = someHash(styleBlock);
  const styleEl = document.createElement('style');
  styleEl.textContent = `
    .${className} {
      ${styleBlock}
    }
  `;
  document.head.appendChild(styleEl);
  return className;
};
const className = css(`
  color: red;
  padding: 20px;
`); // 'c23j4'

從上面的代碼能夠看出,CSS-in-JS的實現會根據定義的樣式字符串生成一個惟一的CSS選擇器,而後把對應的樣式插入到頁面頭部的style標籤中,styled-components使用的就是相似的方法。

避免無用的CSS樣式堆積 - Dead Code Elimination

進行過大型Web項目開發的同窗應該都有經歷過這個情景:在開發新的功能或者進行代碼重構的時候,因爲HTML代碼和CSS樣式之間沒有顯式的一一對應關係,咱們很難辨認出項目中哪些CSS樣式代碼是有用的哪些是無用的,這就致使了咱們不敢輕易刪除代碼中多是無用的樣式。這樣隨着時間的推移,項目中的CSS樣式只會增長而不會減小(append-only stylesheets)。無用的樣式代碼堆積會致使如下這些問題:

  • 項目變得愈來愈重量級,加載到瀏覽器的CSS樣式會愈來愈多,會形成必定的性能影響。
  • 開發者發現他們很難理解項目中的樣式代碼,甚至可能被大量的樣式代碼嚇到,這就致使了開發效率的下降以及一些奇奇怪怪的樣式問題的出現。

CSS-in-JS的思路就能夠很好地解決這個問題。咱們先來看一段styled-components的做者Max Stoiber說過的話:

「For three years, I have styled my web apps without any .css files. Instead, I have written all the CSS in JavaScript. ... I can add, change and delete CSS without any unexpected consequences. My changes to the styling of a component will not affect anything else. If I delete a component, I delete its CSS too. No more append-only stylesheets!」 – Max Stoiber

Max Stoiber大致就是說因爲CSS-in-JS會把樣式和組件綁定在一塊兒,當這個組件要被刪除掉的時候,直接把這些代碼刪除掉就行了,不用擔憂刪掉的樣式代碼會對項目的其餘組件樣式產生影響。並且因爲CSS是寫在JavaScript裏面的,咱們還能夠利用JS顯式的變量定義,模塊引用等語言特性來追蹤樣式的使用狀況,這大大方便了咱們對樣式代碼的更改或者重構。

Critical CSS

瀏覽器在將咱們的頁面呈現給用戶以前必定要先完成頁面引用到的CSS文件的下載和解析(download and parse),因此link標籤連接的CSS資源是渲染阻塞的(render-blocking)。若是CSS文件很是大或者網絡的情況不好,渲染阻塞的CSS會嚴重影響用戶體驗。針對這個問題,社區有一種優化方案就是將一些重要的CSS代碼(Critical CSS)直接放在頭部的style標籤內,其他的CSS代碼再進行異步加載,這樣瀏覽器在解析完HTML後就能夠直接渲染頁面了。具體作法相似於如下代碼:

<html>
  <head>
    <style>
      /* critical CSS */
    </style>
    <script>asyncLoadCSS("non-critical.css")</script>
  </head>
  <body>
    ...body goes here
  </body>
</html>

那麼如何定義Critical CSS呢?放在head標籤內的CSS固然是越少越好,由於太多的內容會加大html的體積,因此咱們通常把用戶須要在首屏看到的(above the fold)頁面要用到的最少CSS提取爲Critical CSS。如下是示意圖:

上圖中above the fold的CSS就是Critical CSS,由於它們須要當即展現在用戶面前。因爲頁面在不一樣的設備上展現的效果不一樣,對應着的Critical CSS內容也會有所差異,所以Critical CSS的提取是一個十分複雜的過程,雖然社區有不少對應的工具但是效果都差強人意。CSS-in-JS卻能夠很好地支持Critical CSS的生成。在CSS-in-JS中,因爲CSS是和組件綁定在一塊兒的,只有當組件掛載到頁面上的時候,它們的CSS樣式纔會被插入到頁面的style標籤內,因此很容易就能夠知道哪些CSS樣式須要在首屏渲染的時候發送給客戶端,再配合打包工具的Code Splitting功能,能夠將加載到頁面的代碼最小化,從而達到Critical CSS的效果。換句話來講,CSS-in-JS經過增長一點加載的JS體積就能夠避免另外發一次請求來獲取其它的CSS文件。並且一些CSS-in-JS的實現(例如styled-components)對Critical CSS是自動支持的

基於狀態的樣式定義 - State-based styling

CSS-in-JS最吸引個人地方就是它能夠根據組件的狀態動態地生成樣式。對於SPA應用來講,特別是一些交互複雜的頁面,頁面的樣式一般要根據組件的狀態變化而發生變化。若是不使用CSS-in-JS,處理這些邏輯複雜的狀況會比較麻煩。舉個例子,假如你如今頁面有一個圓點,它根據不一樣的狀態展現不一樣的顏色,running的時候是綠色,stop的時候是紅色,ready的時候是黃色。若是使用的是CSS modules方案你可能會寫下面的代碼:

style.css文件

.circle {
  ... circle base styles
}

.healthy {
  composes: circle;
  background-color: green;
}

.stop {
  composes: circle;
  background-color: red;
}

.ready {
  composes: circle;
  background-color: 
}

index.js文件

import React from 'react'
import styles from './style.css'

const styleLookup = {
  healthy: styles.healthy,
  stop: styles.stop,
  ready: styles.ready
}

export default ({ status }) => (
  <div
    className={styleLookup[status]}
  />
)

在style.css中咱們使用了CSS modules的繼承寫法來在不一樣狀態的CSS類中共用circle基類的樣式,代碼看起來十分冗餘和繁瑣。因爲CSS-in-JS會直接將CSS樣式寫在JS文件裏面,因此樣式複用以及邏輯判斷都十分方便,若是上面的例子用styled-components來寫是這樣的:

import styled from 'styled-components'

const circleColorLookup = {
  healthy: 'green',
  stop: 'red',
  ready: 'yellow'
}

export default styled.div`
  ... circle base styles
  background-color: ${({ status }) => circleColorLookup[status]};
`

對比起來,styled-components的邏輯更加清晰和簡潔,若是後面須要增長一個狀態,只須要爲circleColorLookup添加一個鍵值對就好,而CSS modules的寫法須要同時改動style.css和index.js文件,代碼很差維護和擴展。

封裝得更好的組件庫

你們在平常開發的過程當中可能會封裝一些組件在不一樣的項目中使用,若是你的組件的樣式使用的CSS預處理方案和另一個項目的預處理方案不同,例如組件使用的是less,項目使用的是css modules,組件複用會變得很麻煩。但是若是CSS是寫在JS裏面的,項目想要使用封裝的組件庫只須要進行簡單的npm install就能夠了,很是方便。

壞處

任何事物都有好的地方和壞的地方,只有對好處和壞處都瞭解清楚咱們才能更好地作出判斷。接着咱們就來講一下CSS-in-JS很差的地方吧。

陡峭的學習曲線 - Steep learning curve

這其實能夠從兩方面來講明。首先CSS-in-JS是針對component-based的框架的,這就意味着要學習CSS-in-JS你必須得學習:component-based框架(例如React),JavaScript和CSS這三樣技能。其次,即便你已經會用React,JavaScript和CSS來構建SPA應用,你還要學習某個CSS-in-JS實現(例如styled-components),以及學習一種全新的基於組件定義樣式的思考問題方式。咱們團隊在剛開始使用styled-components的時候,適應了好一段時間才學會如何用好這個庫。由於學習成本比較高,在項目中引入CSS-in-JS可能會下降大家的開發效率。

運行時消耗 - Runtime cost

因爲大多數的CSS-in-JS的庫都是在動態生成CSS的。這會有兩方面的影響。首先你發送到客戶端的代碼會包括使用到的CSS-in-JS運行時(runtime)代碼,這些代碼通常都不是很小,例如styled-components的runtime大小是12.42kB min + gzip,若是你但願你首屏加載的代碼很小,你得考慮這個問題。其次大多數CSS-in-JS實現都是在客戶端動態生成CSS的,這就意味着會有必定的性能代價。不一樣的CSS-in-JS實現因爲具體的實現細節不同,因此它們的性能也會有很大的區別,你能夠經過這個工具來查看和衡量各個實現的性能差別。

代碼可讀性差 - Unreadable class names

大多數CSS-in-JS實現會經過生成惟一的CSS選擇器來達到CSS局部做用域的效果。這些自動生成的選擇器會大大下降代碼的可讀性,給開發人員debug形成必定的影響。

沒有統一的業界標準 - No interoperability

因爲CSS-in-JS只是一種技術思路而沒有一個社區統一遵循的標準和規範,因此不一樣實現的語法和功能可能有很大的差別。這就意味着你不能從一個實現快速地切換到另一個實現。舉個例子,假如你先在項目使用radium,但是隨着項目規模的變大,你發現radium可能不適合你如今的業務,更好的解決方案應該是styled-components。但是因爲寫法差別巨大,這時候你要對代碼進行脫胎換骨的改動才能將代碼遷移到styled-components。不過使人欣慰的是,如今已經有人在制定相關的標準了,有興趣的同窗能夠看一下Interoperable Style Transfer Format

我的思考與總結

CSS-in-JS有好處也有壞處,咱們必定要根據本身的實際狀況進行衡量和取捨來肯定是否是要在本身的項目中使用它。永遠不要爲了使用一個技術而用一個技術。例如在下面幾種狀況下你就不須要它:

  • 你是前端開發的初學者: 因爲CSS-in-JS的學習坡度很陡,剛開始學習Web開發的同窗不必學習,可能會有挫敗感。
  • 你只想製做一些功能簡單的靜態頁面:邏輯交互不復雜的網站沒有必要使用CSS-in-JS。
  • 你很注重樣式名的可讀性以及調試體驗: CSS-in-JS動態生成的選擇器很影響代碼的可讀性,可能會下降你的調試效率。

相反若是你的應用交互邏輯複雜的話,CSS-in-JS可能會給你帶來很大的開發便利,沒有使用過的人十分值得一試。

參考文獻

我的技術動態

文章首發於個人我的博客

歡迎關注公衆號進擊的大蔥一塊兒學習成長

相關文章
相關標籤/搜索