技術(shù)天地 | CSS-in-JS:一個(gè)充滿爭議的技術(shù)方案
掃描二維碼
隨時(shí)隨地手機(jī)看文章
導(dǎo)讀
為了解決傳統(tǒng)CSS在現(xiàn)代前端應(yīng)用開發(fā)中遇到的痛點(diǎn),F(xiàn)reeWheel評(píng)估了大量新一代的CSS框架/工具/方案。在本文中,作者以評(píng)估過程為線索,介紹了CSS-in-JS的背景、現(xiàn)狀、開發(fā)特點(diǎn)和趨勢(shì)。
HTML、JS、CSS 是 Web 開發(fā)的三大核心技術(shù)。Web 開發(fā)早期,開發(fā)人員的工作內(nèi)容以編寫可在瀏覽器渲染的頁面文檔為主,此時(shí)的最佳實(shí)踐推崇 “關(guān)注點(diǎn)分離“ 原則,使得開發(fā)者可以在一個(gè)時(shí)間點(diǎn)只關(guān)注單一技術(shù)。通過聲明式的語法,CSS 可以脫離 HTML 上下文進(jìn)行獨(dú)立維護(hù),同時(shí)依賴于選擇器、偽選擇器、媒體查詢等方式與 HTML 松耦合,最終將樣式應(yīng)用于 DOM 元素上。
隨著以 React 為首的現(xiàn)代前端開發(fā)框架的興起,在 JS 中維護(hù) CSS 的方案(也就是 CSS-in-JS)成為了當(dāng)代前端社區(qū)的新趨勢(shì),以解決在現(xiàn)代 Web 應(yīng)用開發(fā)中使用 CSS 時(shí)出現(xiàn)的一些痛點(diǎn)。
圖片來源:https://medium.com/@ChahanaTyagi/write-css-in-js-react-emotion-f828ddc65d3a
為了解決這些痛點(diǎn),F(xiàn)reeWheel評(píng)估了大量新一代的CSS框架/工具/方案,并基于自身需求對(duì)CSS-in-JS方案進(jìn)行了細(xì)致的選型。本文以我們的評(píng)估過程為線索,介紹了CSS-in-JS的背景、現(xiàn)狀、開發(fā)特點(diǎn)和趨勢(shì)。
傳統(tǒng) CSS 在 FreeWheel 轉(zhuǎn)型 React 過程中的痛點(diǎn)
FreeWheel的前端從十年前的巨型單體Rails應(yīng)用,發(fā)展到如今的前后端分離、基于React組件化的前端單頁應(yīng)用,在CSS的重構(gòu)和開發(fā)方面先后遇到過不少痛點(diǎn)。其中最主要的還是CSS的組件化封裝問題。
CSS 樣式規(guī)則一旦生效,就會(huì)應(yīng)用于全局,這就導(dǎo)致分發(fā)缺少樣式封裝的 React 組件時(shí)有一定選擇器沖突的風(fēng)險(xiǎn)。雖然 React 本身組件提供 style 屬性,可以讓用戶以對(duì)象、內(nèi)聯(lián)樣式的方式,將樣式應(yīng)用于渲染后的 DOM 元素上,在一定程度上實(shí)現(xiàn)了樣式的組件化封裝。但是,由于內(nèi)聯(lián)樣式缺少 CSS 所能提供的許多特性,比如偽選擇器、動(dòng)畫與漸變、媒體選擇器等,同時(shí)因?yàn)椴恢С诸A(yù)處理器,其瀏覽器兼容性也受到了限制。
舉例來說,F(xiàn)reeWheel的Rails應(yīng)用曾大量使用了jQuery和Bootstrap框架,將前端逐步遷移到React時(shí),迫于開發(fā)周期等因素需要保留一部分老代碼,簡單封裝成React組件并與其他新編寫的組件混用,這就導(dǎo)致其他組件的樣式被Bootstrap CSS污染。
為了解決這個(gè)問題,當(dāng)時(shí)我們利用SCSS將全局樣式鑲嵌到bootstrap-scope類中,再用<div class=“bootstrap-scope”></div>將會(huì)產(chǎn)生CSS污染的老代碼隔離起來。類似的例子還有不少,然而這類方案卻并不具有普適性,引入了額外的維護(hù)成本。
相關(guān)替代方案
對(duì)于 Angular 和 Vue 來說,這兩個(gè)都有框架原生提供的 CSS 封裝方案,比如 Vue 文件的scoped style 標(biāo)簽和 Angular 組件的viewEncapsulation 屬性。React 本身的設(shè)計(jì)原則決定了其不會(huì)提供原生的 CSS 封裝方案,或者說CSS封裝并不是React框架本身的關(guān)注點(diǎn)【1】。因此 ,React 社區(qū)從很早的時(shí)候就開始尋找相關(guān)替代辦法。其中包含以下幾種技術(shù)路線:
CSS 模塊化 (CSS Modules):這種做法非常類似 Angular 與 Vue 對(duì)樣式的封裝方案,其核心是以 CSS 文件模塊為單元,將模塊內(nèi)的選擇器附上特殊的哈希字符串,以實(shí)現(xiàn)樣式的局部作用域。對(duì)于大多數(shù) React 項(xiàng)目來說,這種方案已經(jīng)足夠用了。
基于共識(shí)的人工維護(hù)的方法論,如 BEM。這種方法的缺點(diǎn)是會(huì)為團(tuán)隊(duì)帶來很大的挑戰(zhàn),對(duì)于全局和局部規(guī)劃選擇器的命名,團(tuán)隊(duì)對(duì)于這種方法需要有共識(shí),即使熟練使用的情況下,在使用中依然有著較高的思維負(fù)擔(dān)和維護(hù)成本。
Shadow DOM:借助direflow.io【2】等工具,我們可以將 React 組件輸出為 Web Component,借助 Shadow DOM 實(shí)現(xiàn)組件的 CSS 樣式封裝。這是一種解決辦法,不過基本很少有項(xiàng)目選擇這樣做。
CSS-in-JS,也就是本文的重點(diǎn),接下來我們會(huì)圍繞著它展開討論。
CSS-in-JS 的出現(xiàn)與爭議
CSS-in-JS (后文簡稱為 CIJ)在 2014 年由 Facebook 的員工Vjeux 在 NationJS 會(huì)議【3】上提出:可以借用 JS 解決許多 CSS 本身的一些“缺陷”,比如全局作用域、死代碼移除、生效順序依賴于樣式加載順序、常量共享等等問題。
CIJ 的一大特點(diǎn)是它的方案眾多【4】,這種看似混亂的狀態(tài)很符合前端社區(qū)喜歡重復(fù)造輪子的特征。發(fā)展初期,社區(qū)在各個(gè)方向上探索著用 JS 開發(fā)和維護(hù) CSS 的可能性。每隔一段時(shí)間,都會(huì)有新的語法方案或?qū)崿F(xiàn),嘗試補(bǔ)充、增強(qiáng)或是修復(fù)已有實(shí)現(xiàn)。
隨著時(shí)間流逝,他們中的大多數(shù)不是被官方宣布廢棄,就是長時(shí)間不再維護(hù)。如:
glam【5】/glamor【6】: 由 React 的前項(xiàng)目經(jīng)理 Sunil Pai 維護(hù),首先提出了 CSS 屬性接口方案
glamorous【7】 by PayPal
aphrodite【8】 by Khan
radium【9】by FormidableLabs
從 CIJ 概念的誕生到 6 年后的今天,社區(qū)對(duì)于它的看法依然充滿了爭議,并且熱度不減。甚至 Chrome 在新版中為了 CIJ 的需求修復(fù)了一個(gè)問題【10】,這也可以從側(cè)面看出來 CIJ 已經(jīng)得到了瀏覽器廠商的重視。
爭議主要集中在以下幾點(diǎn):
使用 CIJ 是一種偽需求。假如開發(fā)者足夠理解 CSS 的概念,如 specificity (特異性)、cascading (級(jí)聯(lián))等,同時(shí)利用預(yù)、后處理工具(如 scss/postcss)和方法論(如 BEM),只靠 CSS 就足以完成任務(wù)
CIJ 方案和工具過多,缺乏標(biāo)準(zhǔn),許多處于不成熟的狀態(tài),使用起來有較大風(fēng)險(xiǎn)。假如使用了一個(gè)方案,就需要承擔(dān)起這種實(shí)現(xiàn)可能會(huì)被遺棄的風(fēng)險(xiǎn)
CIJ 有運(yùn)行時(shí)性能損耗
趨于融合的事實(shí)標(biāo)準(zhǔn)
雖然 CIJ 還沒有形成真正的標(biāo)準(zhǔn),但在接口 API 設(shè)計(jì)、功能或是使用體驗(yàn)上,不同的實(shí)現(xiàn)方案越來越接近,其中最受歡迎的兩個(gè)解決方案是Emotion【11】 和styled-components【12】。通過幾年間的競爭,為了滿足開發(fā)者的需求,同時(shí)結(jié)合社區(qū)的使用反饋,在不斷的更新過程中,它們漸漸具有了幾乎相同的 API,只是在內(nèi)部實(shí)現(xiàn)上有所不同。
這種狀態(tài)形成了 CIJ 在 API 接口上的事實(shí)標(biāo)準(zhǔn)。不管是現(xiàn)有的主流方案還是新出現(xiàn)的方案,幾乎在接口上使用同樣的(或是一部分的)接口設(shè)計(jì):CSS prop 與樣式組件(styled components,與 styled-components 庫名稱相同)。以 Emotion 為例:
css prop
export function MyContainer({ color, children }) {return (<divcss={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};}`;
同時(shí),這兩種方案都支持模板字符串或是對(duì)象樣式。
import styled from '@emotion/styled';export function MyContainer({ color, children }) {return (<divcss={{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)部實(shí)現(xiàn)中都會(huì)享受當(dāng)代前端工程化的福利,如語法檢查、自動(dòng)增加瀏覽器屬性前綴、幫助開發(fā)者增強(qiáng)樣式的瀏覽器兼容性等等。同時(shí)利用 vscode-styled-components【13】、stylelint【14】 等代碼編輯器插件,我們可以在 JS 代碼中增加對(duì)于 CSS 的語法高亮支持。
"css prop" vs "樣式組件"
這兩種 CIJ 的 API 接口模式代表著兩種組件化樣式風(fēng)格。
css prop 可以算是內(nèi)聯(lián)樣式的升級(jí)版,用戶定義的內(nèi)聯(lián)樣式以 JSX 標(biāo)簽屬性的方式與組件緊密結(jié)合,可以幫助用戶快速迭代開發(fā),讓用戶可以更快速的定位問題。不過由于樣式直接內(nèi)嵌在JSX中,勢(shì)必在一定程度上會(huì)影響組件代碼的可讀性。
樣式組件更像是 CSS 的組件化封裝,將樣式抽象為語義化的標(biāo)簽,把樣式從組件實(shí)現(xiàn)中分離出來,讓 JSX 結(jié)構(gòu)更“干凈整潔”。相對(duì)而言,樣式組件定義的樣式不如內(nèi)聯(lián)樣式更方便直接,而且需要給額外多出來的樣式組件定義新的標(biāo)簽名,會(huì)在一定程度上影響開發(fā)效率;但從另外一個(gè)角度來說,樣式組件以更規(guī)范的接口提供給團(tuán)隊(duì)復(fù)用,適合有成熟確定的設(shè)計(jì)語言的組件庫或是產(chǎn)品。
選擇用哪一種方案并沒有決定性方法論,可根據(jù)項(xiàng)目需要進(jìn)行取舍。
新趨勢(shì)
雖說由于馬太效應(yīng),CIJ 的市場(chǎng)份額被 styled-components 和 Emotion 吃掉了一大部分,但社區(qū)依然有新的實(shí)現(xiàn)不斷涌現(xiàn),探索新的 CIJ 方向,或是解決先前技術(shù)的不足。
移除運(yùn)行時(shí)性能損耗
在框架內(nèi)部,Emotion和styled-components在瀏覽器中都有一個(gè)運(yùn)行時(shí),這不光增加了最終構(gòu)建產(chǎn)物大小,更嚴(yán)重的問題是還帶來了運(yùn)行時(shí)成本。舉例來說,CSS 屬性的實(shí)現(xiàn)思路是這樣的:
解析用戶樣式,在需要時(shí)添加前綴,并將其放入CSS類中
生成哈希類名
利用CSSOM【15】,創(chuàng)建或更新樣式
生成新樣式時(shí)更新css節(jié)點(diǎn)/規(guī)則
對(duì)于大型前端項(xiàng)目來說,CIJ 的運(yùn)行時(shí)損耗有時(shí)是可以感知到的,這會(huì)對(duì)用戶體驗(yàn)造成一些影響。有些新方案選擇將 CSS 在構(gòu)建時(shí)輸出為靜態(tài) CSS 文件,如Linaria【16】。不過這種方案有一些語法上的限制,比如不支持內(nèi)聯(lián)CSS樣式【17】。
值得一提的是@compiled/css-in-js【18】,這個(gè)庫會(huì)用類似于 Angular 的預(yù)先(AoT)編譯器,將組件樣式預(yù)先編譯為 CSS 字符串,嵌入轉(zhuǎn)譯的 JS 代碼中。這種方式顯著減少了因變量引起的 CSS 冗余問題。
原子化
以Tailwind CSS【19】 為代表,CSS 原子化是使用純 CSS 的一種流行方案。這種方案中,用戶使用庫提供的功能性CSS 類修飾DOM結(jié)構(gòu)。下面是一個(gè)使用 Tailwind 的例子:
<button class="bg-blue-500 hover:bg-blue-700 rounded">Button</button>
其中bg-blue-500 hover:bg-blue-700 rounded 是 Tailwind 預(yù)定義的原子 CSS 類,每個(gè)類里面只有一條唯一的樣式規(guī)則。使用原子化 CSS 有一些好處,比如:減少CSS規(guī)則沖突可能性(Specificity);CSS 的大小恒定,不會(huì)跟隨項(xiàng)目的增長而增長;用戶可以直接修改 HTML 屬性而不用修改 CSS,改變最終渲染的效果 。
不過選擇使用原子化 CSS,用戶要么需要自己生成一系列原子化的功能性類(工程化成本),要么需要引入 Tailwind 方案(學(xué)習(xí)成本)。而CIJ 給 CSS 原子化帶來了一些新的可能性,社區(qū)正在探索利用 CIJ 完成自動(dòng)化的原子化 CSS 的可能性,比如Styletron【20】、Fela【21】、Otion【22】 等。
原子化 CSS 可能會(huì)給 CIJ 帶來不少好處,比如CSS規(guī)則去重。CIJ 在運(yùn)行時(shí)會(huì)產(chǎn)生許多新的CSS類,增加瀏覽器的負(fù)擔(dān),遺憾的是這需要框架本身支持把CSS抽離為靜態(tài)文件的需求。目前流行的CSS-in-JS框架,比如Emotion,暫時(shí)還無法支持這樣的特性。
結(jié)語
為解決傳統(tǒng) CSS 在現(xiàn)代前端應(yīng)用開發(fā)中遇到的痛點(diǎn),經(jīng)過了一段時(shí)間的探索與實(shí)踐,F(xiàn)reeWheel 最終確定使用Emotion 作為目前的 CIJ 方案,將其應(yīng)用于部分前端項(xiàng)目。Emotion 社區(qū)活躍度很高,在可以預(yù)見的未來之中,它依然會(huì)保持相當(dāng)長時(shí)間的流行度。并且,現(xiàn)在多數(shù) CIJ 方案出現(xiàn)了接口方案收斂融合的趨勢(shì),假如將來我們需要切換方案的時(shí)候,我們有很大把握可以比較順滑的切換到新的方案上。除此之外,F(xiàn)reeWheel 依然會(huì)持續(xù)關(guān)注社區(qū)動(dòng)態(tài),在必要的時(shí)候進(jìn)行調(diào)整。
跟所有技術(shù)方案一樣,CIJ 同樣不是一顆能完美解決樣式維護(hù)難題的銀彈。但通過借助一定最佳實(shí)踐后,Emotion 足以應(yīng)對(duì) FreeWheel 的大多數(shù)前端需求,比如消費(fèi)設(shè)計(jì)令牌、主題切換、組件樣式封裝、用戶端樣式覆蓋等等,并顯著提升前端團(tuán)隊(duì)在維護(hù)樣式時(shí)的幸福感。
希望此文會(huì)對(duì)你有所幫助!
參考文章鏈接:
【1】CSS封裝并不是React框架本身的關(guān)注點(diǎn)
https://reactjs.org/docs/faq-styling.html
【2】direflow.io
https://direflow.io/
【3】Vjeux 在 NationJS 會(huì)議
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】一個(gè)問題
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)用平臺(tái)技術(shù)團(tuán)隊(duì)高級(jí)工程師
特別推薦一個(gè)分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒關(guān)注的小伙伴,可以長按關(guān)注一下:

長按訂閱更多精彩▼
如有收獲,點(diǎn)個(gè)在看,誠摯感謝
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問題,請(qǐng)聯(lián)系我們,謝謝!





