CSS-in-JS:一個充滿爭議的技術方案

HTML、JS、CSS 是 Web 開發的三大核心技術。Web 開發早期,開發人員的工作內容以編寫可在瀏覽器渲染的頁面文檔爲主,此時的最佳實踐推崇 “關注點分離“ 原則,使得開發者可以在一個時間點只關注單一技術。通過聲明式的語法,CSS 可以脫離 HTML 上下文進行獨立維護,同時依賴於選擇器、僞選擇器、媒體查詢等方式與 HTML 松耦合,最終將樣式應用於 DOM 元素上。

隨着以 React 爲首的現代前端開發框架的興起,在 JS 中維護 CSS 的方案(也就是 CSS-in-JS)成爲了當代前端社區的新趨勢,以解決在現代 Web 應用開發中使用 CSS 時出現的一些痛點。

爲了解決這些痛點,FreeWheel評估了大量新一代的CSS框架/工具/方案,並基於自身需求對CSS-in-JS方案進行了細緻的選型。本文以我們的評估過程爲線索,介紹了CSS-in-JS的背景、現狀、開發特點和趨勢。

傳統 CSS 在 FreeWheel 轉型 React 過程中的痛點

FreeWheel的前端從十年前的巨型單體Rails應用,發展到如今的前後端分離、基於React組件化的前端單頁應用,在CSS的重構和開發方面先後遇到過不少痛點。其中最主要的還是CSS的組件化封裝問題。

CSS 樣式規則一旦生效,就會應用於全局,這就導致分發缺少樣式封裝的 React 組件時有一定選擇器衝突的風險。雖然 React 本身組件提供 style 屬性,可以讓用戶以對象、內聯樣式的方式,將樣式應用於渲染後的 DOM 元素上,在一定程度上實現了樣式的組件化封裝。但是,由於內聯樣式缺少 CSS 所能提供的許多特性,比如僞選擇器、動畫與漸變、媒體選擇器等,同時因爲不支持預處理器,其瀏覽器兼容性也受到了限制。

舉例來說,FreeWheel的Rails應用曾大量使用了jQuery和Bootstrap框架,將前端逐步遷移到React時,迫於開發週期等因素需要保留一部分老代碼,簡單封裝成React組件並與其他新編寫的組件混用,這就導致其他組件的樣式被Bootstrap CSS污染。

爲了解決這個問題,當時我們利用SCSS將全局樣式鑲嵌到bootstrap-scope類中,再用<div class=“bootstrap-scope”></div>將會產生CSS污染的老代碼隔離起來。類似的例子還有不少,然而這類方案卻並不具有普適性,引入了額外的維護成本。

相關替代方案

對於 Angular 和 Vue 來說,這兩個都有框架原生提供的 CSS 封裝方案,比如 Vue 文件的scoped style 標籤和 Angular 組件的viewEncapsulation 屬性。React 本身的設計原則決定了其不會提供原生的 CSS 封裝方案,或者說CSS封裝並不是React框架本身的關注點。因此 ,React 社區從很早的時候就開始尋找相關替代辦法。其中包含以下幾種技術路線:

  • CSS 模塊化 (CSS Modules):這種做法非常類似 Angular 與 Vue 對樣式的封裝方案,其核心是以 CSS 文件模塊爲單元,將模塊內的選擇器附上特殊的哈希字符串,以實現樣式的局部作用域。對於大多數 React 項目來說,這種方案已經足夠用了。

  • 基於共識的人工維護的方法論,如 BEM。這種方法的缺點是會爲團隊帶來很大的挑戰,對於全局和局部規劃選擇器的命名,團隊對於這種方法需要有共識,即使熟練使用的情況下,在使用中依然有着較高的思維負擔和維護成本。

  • Shadow DOM:藉助direflow.io等工具,我們可以將 React 組件輸出爲 Web Component,藉助 Shadow DOM 實現組件的 CSS 樣式封裝。這是一種解決辦法,不過基本很少有項目選擇這樣做。

  • CSS-in-JS,也就是本文的重點,接下來我們會圍繞着它展開討論。

CSS-in-JS 的出現與爭議

CSS-in-JS (後文簡稱爲 CIJ)在 2014 年由 Facebook 的員工Vjeux 在 NationJS 會議上提出:可以借用 JS 解決許多 CSS 本身的一些“缺陷”,比如全局作用域、死代碼移除、生效順序依賴於樣式加載順序、常量共享等等問題。

CIJ 的一大特點是它的方案衆多,這種看似混亂的狀態很符合前端社區喜歡重複造輪子的特徵。發展初期,社區在各個方向上探索着用 JS 開發和維護 CSS 的可能性。每隔一段時間,都會有新的語法方案或實現,嘗試補充、增強或是修復已有實現。

隨着時間流逝,他們中的大多數不是被官方宣佈廢棄,就是長時間不再維護。如:

  • glam/glamor: 由 React 的前項目經理 Sunil Pai 維護,首先提出了 CSS 屬性接口方案

  • glamorous by PayPal

  • aphrodite by Khan

  • radium by FormidableLabs

從 CIJ 概念的誕生到 6 年後的今天,社區對於它的看法依然充滿了爭議,並且熱度不減。甚至 Chrome 在新版中爲了 CIJ 的需求修復了一個問題,這也可以從側面看出來 CIJ 已經得到了瀏覽器廠商的重視。

爭議主要集中在以下幾點:

  • 使用 CIJ 是一種僞需求。假如開發者足夠理解 CSS 的概念,如 specificity (特異性)、cascading (級聯)等,同時利用預、後處理工具(如 scss/postcss)和方法論(如 BEM),只靠 CSS 就足以完成任務

  • CIJ 方案和工具過多,缺乏標準,許多處於不成熟的狀態,使用起來有較大風險。假如使用了一個方案,就需要承擔起這種實現可能會被遺棄的風險

  • CIJ 有運行時性能損耗

趨於融合的事實標準

雖然 CIJ 還沒有形成真正的標準,但在接口 API 設計、功能或是使用體驗上,不同的實現方案越來越接近,其中最受歡迎的兩個解決方案是Emotion和styled-components。通過幾年間的競爭,爲了滿足開發者的需求,同時結合社區的使用反饋,在不斷的更新過程中,它們漸漸具有了幾乎相同的 API,只是在內部實現上有所不同。

這種狀態形成了 CIJ 在 API 接口上的事實標準。不管是現有的主流方案還是新出現的方案,幾乎在接口上使用同樣的(或是一部分的)接口設計: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,
  },
}));

兩種方案在內部實現中都會享受當代前端工程化的福利,如語法檢查、自動增加瀏覽器屬性前綴、幫助開發者增強樣式的瀏覽器兼容性等等。同時利用 vscode-styled-components、stylelint等代碼編輯器插件,我們可以在 JS 代碼中增加對於 CSS 的語法高亮支持。

"css prop" vs "樣式組件"

這兩種 CIJ 的 API 接口模式代表着兩種組件化樣式風格。

css prop 可以算是內聯樣式的升級版,用戶定義的內聯樣式以 JSX 標籤屬性的方式與組件緊密結合,可以幫助用戶快速迭代開發,讓用戶可以更快速的定位問題。不過由於樣式直接內嵌在JSX中,勢必在一定程度上會影響組件代碼的可讀性。

樣式組件更像是 CSS 的組件化封裝,將樣式抽象爲語義化的標籤,把樣式從組件實現中分離出來,讓 JSX 結構更“乾淨整潔”。相對而言,樣式組件定義的樣式不如內聯樣式更方便直接,而且需要給額外多出來的樣式組件定義新的標籤名,會在一定程度上影響開發效率;但從另外一個角度來說,樣式組件以更規範的接口提供給團隊複用,適合有成熟確定的設計語言的組件庫或是產品。

選擇用哪一種方案並沒有決定性方法論,可根據項目需要進行取捨。

新趨勢

雖說由於馬太效應,CIJ 的市場份額被 styled-components 和 Emotion 喫掉了一大部分,但社區依然有新的實現不斷湧現,探索新的 CIJ 方向,或是解決先前技術的不足。

移除運行時性能損耗

在框架內部,Emotion和styled-components在瀏覽器中都有一個運行時,這不光增加了最終構建產物大小,更嚴重的問題是還帶來了運行時成本。舉例來說,CSS 屬性的實現思路是這樣的:

  • 解析用戶樣式,在需要時添加前綴,並將其放入CSS類中

  • 生成哈希類名

  • 利用CSSOM,創建或更新樣式

  • 生成新樣式時更新css節點/規則

對於大型前端項目來說,CIJ 的運行時損耗有時是可以感知到的,這會對用戶體驗造成一些影響。有些新方案選擇將 CSS 在構建時輸出爲靜態 CSS 文件,如Linaria。不過這種方案有一些語法上的限制,比如不支持內聯CSS樣式。

值得一提的是@compiled/css-in-js,這個庫會用類似於 Angular 的預先(AoT)編譯器,將組件樣式預先編譯爲 CSS 字符串,嵌入轉譯的 JS 代碼中。這種方式顯著減少了因變量引起的 CSS 冗餘問題。

原子化

以Tailwind CSS爲代表,CSS 原子化是使用純 CSS 的一種流行方案。這種方案中,用戶使用庫提供的功能性CSS 類修飾DOM結構。下面是一個使用 Tailwind 的例子:

<button class="bg-blue-500 hover:bg-blue-700 rounded">
  Button
</button>

其中bg-blue-500 hover:bg-blue-700 rounded 是 Tailwind 預定義的原子 CSS 類,每個類裏面只有一條唯一的樣式規則。使用原子化 CSS 有一些好處,比如:減少CSS規則衝突可能性(Specificity);CSS 的大小恆定,不會跟隨項目的增長而增長;用戶可以直接修改 HTML 屬性而不用修改 CSS,改變最終渲染的效果 。

不過選擇使用原子化 CSS,用戶要麼需要自己生成一系列原子化的功能性類(工程化成本),要麼需要引入 Tailwind 方案(學習成本)。而CIJ 給 CSS 原子化帶來了一些新的可能性,社區正在探索利用 CIJ 完成自動化的原子化 CSS 的可能性,比如Styletron、Fela、Otion 等。

原子化 CSS 可能會給 CIJ 帶來不少好處,比如CSS規則去重。CIJ 在運行時會產生許多新的CSS類,增加瀏覽器的負擔,遺憾的是這需要框架本身支持把CSS抽離爲靜態文件的需求。目前流行的CSS-in-JS框架,比如Emotion,暫時還無法支持這樣的特性。

結語

爲解決傳統 CSS 在現代前端應用開發中遇到的痛點,經過了一段時間的探索與實踐,FreeWheel 最終確定使用Emotion 作爲目前的 CIJ 方案,將其應用於部分前端項目。Emotion 社區活躍度很高,在可以預見的未來之中,它依然會保持相當長時間的流行度。並且,現在多數 CIJ 方案出現了接口方案收斂融合的趨勢,假如將來我們需要切換方案的時候,我們有很大把握可以比較順滑的切換到新的方案上。除此之外,FreeWheel 依然會持續關注社區動態,在必要的時候進行調整。

跟所有技術方案一樣,CIJ 同樣不是一顆能完美解決樣式維護難題的銀彈。但通過藉助一定最佳實踐後,Emotion 足以應對 FreeWheel 的大多數前端需求,比如消費設計令牌、主題切換、組件樣式封裝、用戶端樣式覆蓋等等,並顯著提升前端團隊在維護樣式時的幸福感。

希望此文會對你有所幫助!

作者介紹

肖鵬,FreeWheel應用平臺技術團隊高級工程師。

原文鏈接

技術天地 | CSS-in-JS:一個充滿爭議的技術方案

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章