CSS in js 的好與壞!前端學者需要理清思緒,才好構思畫面

CSS in js 的好與壞!前端學者需要理清思緒,才好構思畫面

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都有屬於框架自己的一套定義樣式的方案。

本文將通過分析CSS-in-JS這項技術帶來的好處以及它存在的一些問題來幫助大家判斷自己是不是要在項目中使用CSS-in-JS。

不同的實現

實現了CSS-in-JS的庫有很多,據統計現在已經超過了61種。雖然每個庫解決的問題都差不多,可是它們的實現方法和語法卻大相徑庭。從實現方法上區分大體分為兩種:唯一CSS選擇器和內聯樣式(Unique Selector VS Inline Styles)。接下來我們會分別看一下對應於這兩種實現方式的兩個比較有代表性的實現:styled-components和radium。

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選擇器會有一小段哈希值來保證全局唯一性來避免樣式發生衝突。

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


CSS in js 的好與壞!前端學者需要理清思緒,才好構思畫面

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


CSS in js 的好與壞!前端學者需要理清思緒,才好構思畫面

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

Radium

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

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

CSS in js 的好與壞!前端學者需要理清思緒,才好構思畫面

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

CSS in js 的好與壞!前端學者需要理清思緒,才好構思畫面

從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庫生成唯一選擇器的示例代碼:

<code>

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; `

); /<code>

從上面的代碼可以看出,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後就可以直接渲染頁面了。具體做法類似於以下代碼:

<code>

<

html

>

<

head

>

<

style

>

style

>

<

script

>

asyncLoadCSS(

"non-critical.css"

)

script

>

head

>

<

body

>

...body goes here

body

>

html

>

/<code>

那麼如何定義Critical CSS呢?放在head標籤內的CSS當然是越少越好,因為太多的內容會加大html的體積,所以我們一般把用戶需要在首屏看到的(above the fold)頁面要用到的最少CSS提取為Critical CSS。以下是示意圖:

CSS in js 的好與壞!前端學者需要理清思緒,才好構思畫面

上圖中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文件

<code>/<code>

index.js文件

<code>

import

React

from

'react'

import

styles

from

'./style.css'

const

styleLookup

=

{

5

healthy:

styles.healthy,

6

stop:

styles.stop,

7

ready:

styles.ready

8

}

9

10

export

default

({

status

})

=>

(

11

12

className={styleLookup[status]}

13

/>

14

)

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

<code>

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]}; `

/<code>

對比起來,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可能會給你帶來很大的開發便利,沒有使用過的人十分值得一試。

喜歡小編的可以點個贊關注小編哦,小編每天都會給大家分享文章。

我自己是一名從事了多年的前端老程序員,小編為大家準備了新出的前端編程學習資料,免費分享給大家!

如果你也想學習前端,可以觀看【置頂】文章。也可以私信【1】 領取最新前端練手實戰項目

收藏

舉報

前端明澈/<code>


分享到:


相關文章: