技術(shù)天地 | CSS-in-JS:一個充滿爭議的技術(shù)方案
掃描二維碼
隨時隨地手機看文章
導(dǎo)讀
為了解決傳統(tǒng)CSS在現(xiàn)代前端應(yīng)用開發(fā)中遇到的痛點,F(xiàn)reeWheel評估了大量新一代的CSS框架/工具/方案。在本文中,作者以評估過程為線索,介紹了CSS-in-JS的背景、現(xiàn)狀、開發(fā)特點和趨勢。
HTML、JS、CSS 是 Web 開發(fā)的三大核心技術(shù)。Web 開發(fā)早期,開發(fā)人員的工作內(nèi)容以編寫可在瀏覽器渲染的頁面文檔為主,此時的最佳實踐推崇 “關(guān)注點分離“ 原則,使得開發(fā)者可以在一個時間點只關(guān)注單一技術(shù)。通過聲明式的語法,CSS 可以脫離 HTML 上下文進行獨立維護,同時依賴于選擇器、偽選擇器、媒體查詢等方式與 HTML 松耦合,最終將樣式應(yīng)用于 DOM 元素上。
隨著以 React 為首的現(xiàn)代前端開發(fā)框架的興起,在 JS 中維護 CSS 的方案(也就是 CSS-in-JS)成為了當(dāng)代前端社區(qū)的新趨勢,以解決在現(xiàn)代 Web 應(yīng)用開發(fā)中使用 CSS 時出現(xiàn)的一些痛點。

圖片來源:https://medium.com/@ChahanaTyagi/write-css-in-js-react-emotion-f828ddc65d3a
為了解決這些痛點,F(xiàn)reeWheel評估了大量新一代的CSS框架/工具/方案,并基于自身需求對CSS-in-JS方案進行了細致的選型。本文以我們的評估過程為線索,介紹了CSS-in-JS的背景、現(xiàn)狀、開發(fā)特點和趨勢。
傳統(tǒng) CSS 在 FreeWheel 轉(zhuǎn)型 React 過程中的痛點
FreeWheel的前端從十年前的巨型單體Rails應(yīng)用,發(fā)展到如今的前后端分離、基于React組件化的前端單頁應(yīng)用,在CSS的重構(gòu)和開發(fā)方面先后遇到過不少痛點。其中最主要的還是CSS的組件化封裝問題。
CSS 樣式規(guī)則一旦生效,就會應(yīng)用于全局,這就導(dǎo)致分發(fā)缺少樣式封裝的 React 組件時有一定選擇器沖突的風(fēng)險。雖然 React 本身組件提供 style 屬性,可以讓用戶以對象、內(nèi)聯(lián)樣式的方式,將樣式應(yīng)用于渲染后的 DOM 元素上,在一定程度上實現(xiàn)了樣式的組件化封裝。但是,由于內(nèi)聯(lián)樣式缺少 CSS 所能提供的許多特性,比如偽選擇器、動畫與漸變、媒體選擇器等,同時因為不支持預(yù)處理器,其瀏覽器兼容性也受到了限制。
舉例來說,F(xiàn)reeWheel的Rails應(yīng)用曾大量使用了jQuery和Bootstrap框架,將前端逐步遷移到React時,迫于開發(fā)周期等因素需要保留一部分老代碼,簡單封裝成React組件并與其他新編寫的組件混用,這就導(dǎo)致其他組件的樣式被Bootstrap CSS污染。
為了解決這個問題,當(dāng)時我們利用SCSS將全局樣式鑲嵌到bootstrap-scope類中,再用<div class=“bootstrap-scope”></div>將會產(chǎn)生CSS污染的老代碼隔離起來。類似的例子還有不少,然而這類方案卻并不具有普適性,引入了額外的維護成本。
相關(guān)替代方案
對于 Angular 和 Vue 來說,這兩個都有框架原生提供的 CSS 封裝方案,比如 Vue 文件的scoped style 標(biāo)簽和 Angular 組件的viewEncapsulation 屬性。React 本身的設(shè)計原則決定了其不會提供原生的 CSS 封裝方案,或者說CSS封裝并不是React框架本身的關(guān)注點【1】。因此 ,React 社區(qū)從很早的時候就開始尋找相關(guān)替代辦法。其中包含以下幾種技術(shù)路線:
CSS 模塊化 (CSS Modules):這種做法非常類似 Angular 與 Vue 對樣式的封裝方案,其核心是以 CSS 文件模塊為單元,將模塊內(nèi)的選擇器附上特殊的哈希字符串,以實現(xiàn)樣式的局部作用域。對于大多數(shù) React 項目來說,這種方案已經(jīng)足夠用了。
基于共識的人工維護的方法論,如 BEM。這種方法的缺點是會為團隊帶來很大的挑戰(zhàn),對于全局和局部規(guī)劃選擇器的命名,團隊對于這種方法需要有共識,即使熟練使用的情況下,在使用中依然有著較高的思維負擔(dān)和維護成本。
Shadow DOM:借助direflow.io【2】等工具,我們可以將 React 組件輸出為 Web Component,借助 Shadow DOM 實現(xiàn)組件的 CSS 樣式封裝。這是一種解決辦法,不過基本很少有項目選擇這樣做。
CSS-in-JS,也就是本文的重點,接下來我們會圍繞著它展開討論。
CSS-in-JS 的出現(xiàn)與爭議
CSS-in-JS (后文簡稱為 CIJ)在 2014 年由 Facebook 的員工Vjeux 在 NationJS 會議【3】上提出:可以借用 JS 解決許多 CSS 本身的一些“缺陷”,比如全局作用域、死代碼移除、生效順序依賴于樣式加載順序、常量共享等等問題。
CIJ 的一大特點是它的方案眾多【4】,這種看似混亂的狀態(tài)很符合前端社區(qū)喜歡重復(fù)造輪子的特征。發(fā)展初期,社區(qū)在各個方向上探索著用 JS 開發(fā)和維護 CSS 的可能性。每隔一段時間,都會有新的語法方案或?qū)崿F(xiàn),嘗試補充、增強或是修復(fù)已有實現(xiàn)。
隨著時間流逝,他們中的大多數(shù)不是被官方宣布廢棄,就是長時間不再維護。如:
glam【5】/glamor【6】: 由 React 的前項目經(jīng)理 Sunil Pai 維護,首先提出了 CSS 屬性接口方案
glamorous【7】 by PayPal
aphrodite【8】 by Khan
radium【9】by FormidableLabs
從 CIJ 概念的誕生到 6 年后的今天,社區(qū)對于它的看法依然充滿了爭議,并且熱度不減。甚至 Chrome 在新版中為了 CIJ 的需求修復(fù)了一個問題【10】,這也可以從側(cè)面看出來 CIJ 已經(jīng)得到了瀏覽器廠商的重視。
爭議主要集中在以下幾點:
使用 CIJ 是一種偽需求。假如開發(fā)者足夠理解 CSS 的概念,如 specificity (特異性)、cascading (級聯(lián))等,同時利用預(yù)、后處理工具(如 scss/postcss)和方法論(如 BEM),只靠 CSS 就足以完成任務(wù)
CIJ 方案和工具過多,缺乏標(biāo)準,許多處于不成熟的狀態(tài),使用起來有較大風(fēng)險。假如使用了一個方案,就需要承擔(dān)起這種實現(xiàn)可能會被遺棄的風(fēng)險
CIJ 有運行時性能損耗
趨于融合的事實標(biāo)準
雖然 CIJ 還沒有形成真正的標(biāo)準,但在接口 API 設(shè)計、功能或是使用體驗上,不同的實現(xiàn)方案越來越接近,其中最受歡迎的兩個解決方案是Emotion【11】 和styled-components【12】。通過幾年間的競爭,為了滿足開發(fā)者的需求,同時結(jié)合社區(qū)的使用反饋,在不斷的更新過程中,它們漸漸具有了幾乎相同的 API,只是在內(nèi)部實現(xiàn)上有所不同。

這種狀態(tài)形成了 CIJ 在 API 接口上的事實標(biāo)準。不管是現(xiàn)有的主流方案還是新出現(xiàn)的方案,幾乎在接口上使用同樣的(或是一部分的)接口設(shè)計:CSS prop 與樣式組件(styled components,與 styled-components 庫名稱相同)。以 Emotion 為例:
css prop
export function MyContainer({ color, children }) {
return (
<div
css={css`
padding: 32px;
background-color: hotpink;
font-size: 24px;
&:hover {
color: ${color};
}
`}
>
{children}
</div>
);
}
樣式組件
import styled from '@emotion/styled';
export const MyContainer = styled.div`
padding: 32px;
background-color: hotpink;
font-size: 24px;
&:hover {
color: ${(props) => props.color};
}
`;
同時,這兩種方案都支持模板字符串或是對象樣式。
import styled from '@emotion/styled';
export function MyContainer({ color, children }) {
return (
<div
css={{
padding: '32px',
backgroundColor: 'hotpink',
fontSize: '24px',
'&:hover': {
color,
},
}}
>
{children}
</div>
);
}
export const MyContainer = styled.div((props) => ({
padding: '32px',
backgroundColor: 'hotpink',
fontSize: '24px',
'&:hover': {
color: props.color,
},
}));
兩種方案在內(nèi)部實現(xiàn)中都會享受當(dāng)代前端工程化的福利,如語法檢查、自動增加瀏覽器屬性前綴、幫助開發(fā)者增強樣式的瀏覽器兼容性等等。同時利用 vscode-styled-components【13】、stylelint【14】 等代碼編輯器插件,我們可以在 JS 代碼中增加對于 CSS 的語法高亮支持。
"css prop" vs "樣式組件"
這兩種 CIJ 的 API 接口模式代表著兩種組件化樣式風(fēng)格。
css prop 可以算是內(nèi)聯(lián)樣式的升級版,用戶定義的內(nèi)聯(lián)樣式以 JSX 標(biāo)簽屬性的方式與組件緊密結(jié)合,可以幫助用戶快速迭代開發(fā),讓用戶可以更快速的定位問題。不過由于樣式直接內(nèi)嵌在JSX中,勢必在一定程度上會影響組件代碼的可讀性。
樣式組件更像是 CSS 的組件化封裝,將樣式抽象為語義化的標(biāo)簽,把樣式從組件實現(xiàn)中分離出來,讓 JSX 結(jié)構(gòu)更“干凈整潔”。相對而言,樣式組件定義的樣式不如內(nèi)聯(lián)樣式更方便直接,而且需要給額外多出來的樣式組件定義新的標(biāo)簽名,會在一定程度上影響開發(fā)效率;但從另外一個角度來說,樣式組件以更規(guī)范的接口提供給團隊復(fù)用,適合有成熟確定的設(shè)計語言的組件庫或是產(chǎn)品。
選擇用哪一種方案并沒有決定性方法論,可根據(jù)項目需要進行取舍。
新趨勢
雖說由于馬太效應(yīng),CIJ 的市場份額被 styled-components 和 Emotion 吃掉了一大部分,但社區(qū)依然有新的實現(xiàn)不斷涌現(xiàn),探索新的 CIJ 方向,或是解決先前技術(shù)的不足。
移除運行時性能損耗
在框架內(nèi)部,Emotion和styled-components在瀏覽器中都有一個運行時,這不光增加了最終構(gòu)建產(chǎn)物大小,更嚴重的問題是還帶來了運行時成本。舉例來說,CSS 屬性的實現(xiàn)思路是這樣的:
解析用戶樣式,在需要時添加前綴,并將其放入CSS類中
生成哈希類名
利用CSSOM【15】,創(chuàng)建或更新樣式
生成新樣式時更新css節(jié)點/規(guī)則
對于大型前端項目來說,CIJ 的運行時損耗有時是可以感知到的,這會對用戶體驗造成一些影響。有些新方案選擇將 CSS 在構(gòu)建時輸出為靜態(tài) CSS 文件,如Linaria【16】。不過這種方案有一些語法上的限制,比如不支持內(nèi)聯(lián)CSS樣式【17】。
值得一提的是@compiled/css-in-js【18】,這個庫會用類似于 Angular 的預(yù)先(AoT)編譯器,將組件樣式預(yù)先編譯為 CSS 字符串,嵌入轉(zhuǎn)譯的 JS 代碼中。這種方式顯著減少了因變量引起的 CSS 冗余問題。

原子化
以Tailwind CSS【19】 為代表,CSS 原子化是使用純 CSS 的一種流行方案。這種方案中,用戶使用庫提供的功能性CSS 類修飾DOM結(jié)構(gòu)。下面是一個使用 Tailwind 的例子:
<button class="bg-blue-500 hover:bg-blue-700 rounded">
Button
</button>
其中bg-blue-500 hover:bg-blue-700 rounded 是 Tailwind 預(yù)定義的原子 CSS 類,每個類里面只有一條唯一的樣式規(guī)則。使用原子化 CSS 有一些好處,比如:減少CSS規(guī)則沖突可能性(Specificity);CSS 的大小恒定,不會跟隨項目的增長而增長;用戶可以直接修改 HTML 屬性而不用修改 CSS,改變最終渲染的效果 。
不過選擇使用原子化 CSS,用戶要么需要自己生成一系列原子化的功能性類(工程化成本),要么需要引入 Tailwind 方案(學(xué)習(xí)成本)。而CIJ 給 CSS 原子化帶來了一些新的可能性,社區(qū)正在探索利用 CIJ 完成自動化的原子化 CSS 的可能性,比如Styletron【20】、Fela【21】、Otion【22】 等。
原子化 CSS 可能會給 CIJ 帶來不少好處,比如CSS規(guī)則去重。CIJ 在運行時會產(chǎn)生許多新的CSS類,增加瀏覽器的負擔(dān),遺憾的是這需要框架本身支持把CSS抽離為靜態(tài)文件的需求。目前流行的CSS-in-JS框架,比如Emotion,暫時還無法支持這樣的特性。
結(jié)語
為解決傳統(tǒng) CSS 在現(xiàn)代前端應(yīng)用開發(fā)中遇到的痛點,經(jīng)過了一段時間的探索與實踐,F(xiàn)reeWheel 最終確定使用Emotion 作為目前的 CIJ 方案,將其應(yīng)用于部分前端項目。Emotion 社區(qū)活躍度很高,在可以預(yù)見的未來之中,它依然會保持相當(dāng)長時間的流行度。并且,現(xiàn)在多數(shù) CIJ 方案出現(xiàn)了接口方案收斂融合的趨勢,假如將來我們需要切換方案的時候,我們有很大把握可以比較順滑的切換到新的方案上。除此之外,F(xiàn)reeWheel 依然會持續(xù)關(guān)注社區(qū)動態(tài),在必要的時候進行調(diào)整。
跟所有技術(shù)方案一樣,CIJ 同樣不是一顆能完美解決樣式維護難題的銀彈。但通過借助一定最佳實踐后,Emotion 足以應(yīng)對 FreeWheel 的大多數(shù)前端需求,比如消費設(shè)計令牌、主題切換、組件樣式封裝、用戶端樣式覆蓋等等,并顯著提升前端團隊在維護樣式時的幸福感。
希望此文會對你有所幫助!
參考文章鏈接:
【1】CSS封裝并不是React框架本身的關(guān)注點
https://reactjs.org/docs/faq-styling.html
【2】direflow.io
https://direflow.io/
【3】Vjeux 在 NationJS 會議
https://blog.vjeux.com/2014/javascript/react-css-in-js-nationjs.html
【4】方案眾多
https://github.com/MicheleBertoli/css-in-js
【5】glam
https://github.com/threepointone/glam
【6】glamor
https://github.com/threepointone/glamor
【7】glamorous
https://glamorous.rocks/
【8】aphrodite
https://github.com/Khan/aphrodite
【9】radium
https://github.com/FormidableLabs/radium
【10】一個問題
https://developers.google.com/web/updates/2020/06/devtools
【11】Emotion
https://emotion.sh/docs/introduction
【12】styled-components
https://styled-components.com/
【13】vscode-styled-components
https://marketplace.visualstudio.com/items?itemName=jpoissonnier.vscode-styled-components
【14】stylelint
https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint
【15】CSSOM
https://developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model
【16】Linaria
https://github.com/callstack/linaria
【17】不支持內(nèi)聯(lián)CSS樣式
https://github.com/callstack/linaria/blob/master/docs/DYNAMIC_STYLES.md
【18】@compiled/css-in-js
https://github.com/atlassian-labs/compiled-css-in-js
【19】Tailwind CSS
https://tailwindcss.com/
【20】Styletron
https://www.styletron.org/
【21】Fela
https://github.com/robinweser/fela
【22】Otion
https://github.com/kripod/otion
作者簡介
肖鵬
FreeWheel應(yīng)用平臺技術(shù)團隊高級工程師
特別推薦一個分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒關(guān)注的小伙伴,可以長按關(guān)注一下:
長按訂閱更多精彩▼
如有收獲,點個在看,誠摯感謝
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!