React組件設計實踐總結03 - 樣式的管理

CSS 是前端開發的重要組成部分,可是它並不完美,本文主要探討 React 樣式管理方面的一些解決方案,目的是實現樣式的高度可定製化, 讓大型項目的樣式代碼更容易維護.javascript

系列目錄css


目錄html




1. 認識 CSS 的侷限性

vjeux-speak

2014 年vjeux一個 speak 深入揭示的原生 CSS 的一些侷限性. 雖然它有些爭議, 對於開發者來講更多的是啓發. 至從那以後出現了不少 CSS-in-js 解決方案.前端

1️⃣ 全局性

CSS 的選擇器是沒有隔離性的, 無論是使用命名空間仍是 BEM 模式組織, 最終都會污染全局命名空間. 尤爲是大型團隊合做的項目, 很難肯定某個特定的類或者元素是否已經賦過樣式. 因此在大部分狀況下咱們都會絞盡腦汁新建立一個類名, 而不是複用已有的類型.java

解決的方向: 生成惟一的類名; shadow dom; 內聯樣式; Vue-scoped 方案react


2️⃣ 依賴

因爲 CSS 的'全局性', 因此就產生了依賴問題:webpack

一方面咱們須要在組件渲染前就須要先將 CSS 加載完畢, 可是很難清晰地定義某個特定組件依賴於某段特定的 CSS 代碼; 另外一方面, 全局性致使你的樣式可能被別的組件依賴(某種程度的細節耦合), 你不能隨便修改你的樣式, 以避免破壞其餘頁面或組件的樣式. 若是團隊沒有制定合適的 CSS 規範(例如 BEM, 不直接使用標籤選擇器, 減小選擇器嵌套等等), 代碼很快就會失控git

解決的方向: 以前文章提到組件是一個內聚單元, 樣式應該是和組件綁定的. 最基本的解決辦法是使用相似 BEM 命名規範來避免組件之間的命名衝突, 再經過建立優於複用, 組合優於繼承的原則, 來避免組件間樣式耦合;github


3️⃣ 無用代碼的移除

因爲上述'依賴'問題, 組件樣式之間並無明確的邊界, 很難判斷哪些樣式屬於那個組件; 在加上 CSS 的'疊層特性', 更沒法肯定刪除樣式會帶來什麼影響.web

現代瀏覽器已支持 CSS 無用代碼檢查. 但對於無組織的 CSS 效果不會太大

解決的方向: 若是樣式的依賴比較明確,則能夠安全地移除無用代碼


4️⃣ 壓縮

選擇器和類名的壓縮能夠減小文件的體積, 提升加載的性能. 由於原生 CSS 通常有開發者由配置類名(在 html 或 js 動態指定), 因此工具很難對類名進行控制.

壓縮類名也會減低代碼的可讀性, 變得難以調試.

解決的方向: 由工具來轉換或建立類名


5️⃣ 常量共享

常規的 CSS 很難作到在樣式和 JS 之間共享變量, 例如自定義主題色, 一般經過內聯樣式來部分實現這種需求

解決的方向: CSS-in-js


6️⃣ CSS 解析方式的不肯定性

CSS 規則的加載順序是很重要的, 他會影響屬性應用的優先級, 若是按需加載 CSS, 則沒法確保他們的解析順序, 進而致使錯誤的樣式應用到元素上. 有些開發者爲了解決這個問題, 使用!important 聲明屬性, 這無疑是進入了另外一個坑.

解決方向:避免使用全局樣式,組件樣式隔離;樣式加載和組件生命週期綁定




2. 組件的樣式管理

1️⃣ 組件的樣式應該高度可定製化

組件的樣式應該是能夠自由定製的, 開發者應該考慮組件的各類使用場景. 因此一個好的組件必須暴露相關的樣式定製接口. 至少須要支持爲頂層元素配置classNamestyle屬性:

interface ButtonProps {
  className?: string;
  style?: React.CSSProperties;
}
複製代碼

這兩個屬性應該是每一個展現型組件應該暴露的 props, 其餘嵌套元素也要考慮支持配置樣式, 例如 footerClassName, footerStyle.




2️⃣ 避免使用內聯 CSS

  1. style props 添加的屬性不能自動增長廠商前綴, 這可能會致使兼容性問題. 若是添加廠商前綴又會讓代碼變得囉嗦.
  2. 內聯 CSS 不支持複雜的樣式配置, 例如僞元素, 僞類, 動畫定義, 媒體查詢和媒體回退(對象不容許同名屬性, 例如display: -webkit-flex; display: flex;)
  3. 內聯樣式經過 object 傳入組件, 內聯的 object 每次渲染會從新生成, 會致使組件從新渲染. 固然經過某些工具能夠將靜態的 object 提取出去
  4. 不方便調試和閱讀 ...

因此 內聯 CSS 適合用於設置動態且比較簡單的樣式屬性

社區上有許多 CSS-in-js 方案是基於內聯 CSS 的, 例如 Radium, 它使用 JS 添加事件處理器來模擬僞類, 另外也媒體查詢和動畫. 不過不是全部東西均可以經過 JS 模擬, 好比僞元素. 因此這類解決方案用得比較少




3️⃣ 使用 CSS-in-js

社區有不少 CSS 解決方案, 有個項目(MicheleBertoli/css-in-js)專門羅列和對比了這些方案. 讀者也能夠讀這篇文章(What to use for React styling?)學習對 CSS 相關技術進行選型決策

社區上最流行的, 也是筆者以爲使用起來最舒服的是styled-components, styled-components 有下列特性:

  • 自動生成類名, 解決 CSS 的全局性和樣式衝突. 經過組件名來標誌樣式, 自動生成惟一的類名, 開發者不須要爲元素定義類名.
  • 綁定組件. 隔離了 CSS 的依賴問題, 讓組件 JSX 更加簡潔, 反過來開發者須要考慮更多組件的語義
  • 天生支持'關鍵 CSS'. 樣式和組件綁定, 能夠和組件一塊兒進行代碼分割和異步加載
  • 自動添加廠商前綴
  • 靈活的動態樣式. 經過 props 和全局 theme 來動態控制樣式
  • 提供了一些 CSS 預處理器的語法
  • 主題機制
  • 支持 react-native. 這個用起來比較爽
  • 支持 stylint, 編輯器高亮和智能提示
  • 支持服務端渲染
  • 符合分離展現組件和行爲組件原則

推薦這篇文章: Stop using css-in-javascript for web development, styled-components 能夠基本覆蓋全部 CSS 的使用場景:


0. 基本用法

// 定義組件props
const Title = styled.h1<{ active?: boolean }>` color: ${props => (props.active ? 'red' : 'gray')}; `;

// 固定或計算組件props
const Input = styled.input.attrs({
  type: 'text',
  size: props => (props.small ? 5 : undefined),
})``;
複製代碼

1. 樣式擴展

const Button = styled.button` color: palevioletred; font-size: 1em; margin: 1em; padding: 0.25em 1em; border: 2px solid palevioletred; border-radius: 3px; `;

// 覆蓋和擴展已有的組件, 包含styled生成的組件仍是自定義組件(經過className傳入)
const TomatoButton = styled(Button)` color: tomato; border-color: tomato; `;
複製代碼

2. mixin 機制

在 SCSS 中, mixin 是重要的 CSS 複用機制, styled-components 也能夠實現:

定義:

import { css } from 'styled-components';

// utils/styled-mixins.ts
export function truncate(width) {
  return css` width: ${width}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; `;
}
複製代碼

使用:

import { truncate } from '~/utils/styled-mixins';

const Box = styled.div` // 混入 ${truncate('250px')} background: papayawhip; `;
複製代碼

3. 類 SCSS 的語法

const Example = styled(Component)` // 自動廠商前綴 padding: 2em 1em; background: papayawhip; // 僞類 &:hover { background: palevioletred; } // 提供樣式優先級技巧 &&& { color: palevioletred; font-weight: bold; } // 覆蓋內聯css樣式 &[style] { font-size: 12px !important; color: blue !important; } // 支持媒體查詢 @media (max-width: 600px) { background: tomato; // 嵌套規則 &:hover { background: yellow; } } > p { /* descendant-selectors work as well, but are more of an escape hatch */ text-decoration: underline; } /* Contextual selectors work as well */ html.test & { display: none; } `;
複製代碼

引用其餘組件

因爲 styled-components 的類名是自動生成的, 因此不能直接在選擇器中聲明他們, 但能夠在模板字符串中引用其餘組件:

const Icon = styled.svg` flex: none; transition: fill 0.25s; width: 48px; height: 48px; // 引用其餘組件的類名. 這個組件必須是styled-components生成或者包裝的組件 ${Link}:hover & { fill: rebeccapurple; } `;
複製代碼

5. JS 帶來的動態性

媒體查詢幫助方法:

// utils/styled.ts
const sizes = {
  giant: 1170,
  desktop: 992,
  tablet: 768,
  phone: 376,
};

export const media = Object.keys(sizes).reduce((accumulator, label) => {
  const emSize = sizes[label] / 16;
  accumulator[label] = (...args) => css` @media (max-width: ${emSize}em) { ${css(...args)} } `;
  return accumulator;
}, {});
複製代碼

使用:

const Container = styled.div` color: #333; ${media.desktop`padding: 0 20px;`} ${media.tablet`padding: 0 10px;`} ${media.phone`padding: 0 5px;`} `;
複製代碼

SCSS 也提供了不少內置工具方法, 好比顏色的處理, 尺寸的計算. styled-components 提供了一個相似的 js 庫: polished來知足這部分需求, 另外還集成了經常使用的 mixin, 如 clearfix. 經過 babel 插件能夠在編譯時轉換爲靜態代碼, 不須要運行時.


6. 綁定組件的全局樣式

全局樣式和組件生命週期綁定, 當組件卸載時也會刪除全局樣式. 全局樣式一般用於覆蓋一些第三方組件樣式

const GlobalStyle = createGlobalStyle`
  body {
    color: ${props => (props.whiteColor ? 'white' : 'black')};
  }
`

// Test
<React.Fragment>
  <GlobalStyle whiteColor />
  <Navigation /> {/* example of other top-level stuff */}
</React.Fragment>
複製代碼

7. Theme 機制及 Theme 對象的設計

styled-components 的 ThemeProvider 能夠用於取代 SCSS 的變量機制, 只不過它更加靈活, 能夠被全部下級組件共享, 並動態變化.

關於 Theme 對象的設計我以爲能夠參考傳統的 UI 框架, 例如Foundation或者Bootstrap, 通過多年的迭代它們代碼組織很是好, 很是值得學習. 以 Bootstrap 的項目結構爲例:

.
├── _alert.scss
├── ...                # 定義各類組件的樣式
├── _print.scss        # 打印樣式適配
├── _root.scss         # 🔴根樣式, 即全局樣式
├── _transitions.scss  # 過渡效果
├── _type.scss         # 🔴基本排版樣式
├── _reboot.scss       # 🔴瀏覽器重置樣式, 相似於normalize.css
├── _functions.scss
├── _mixins.scss
├── _utilities.scss
├── _variables.scss    # 🔴變量配置, 包含全局配置和全部組件配置
├── bootstrap-grid.scss
├── bootstrap-reboot.scss
├── bootstrap.scss
├── mixins             # 各類mixin, 可複用的css代碼
├── utilities          # 各類工具方法
└── vendor
    └── _rfs.scss
複製代碼

_variables.scss包含了如下配置:

  • 顏色系統: 調色盤配置

    • 灰階顏色: 提供白色到黑色之間多個級別的灰階顏色. 例如

    • 語義顏色: 根據 UI 上面的語義, 定義各類顏色. 這個也是 CSS 開發的常見模式

  • 尺寸系統: 多個級別的間距, 尺寸大小配置

  • 配置開關: 全局性的配置開關, 例如是否支持圓角, 陰影

  • 連接樣式配置: 如顏色, 激活狀態, decoration

  • 排版: 字體, 字體大小, font-weight, 行高, 邊框, 標題等基本排版配置

  • 網格系統斷點配置

bootstrap 將這些配置項有很高的參考意義. 組件能夠認爲是 UI 設計師 的產出, 若是你的應用有統一和規範的設計語言(參考antd), 這些配置會頗有意義。樣式可配置化可讓你的代碼更靈活, 更穩定, 可複用性和可維護性更高. 無論對於 UI 設計仍是客戶端開發, 設計規範能夠提升團隊工做效率, 減小溝通成本.

styled-components 的 Theme 使用的是React Context API, 官方文檔有詳盡的描述, 這裏就不展開了. 點擊這裏瞭解更多, 另外在這裏瞭解如何在 Typescript 中聲明 theme 類型


8. 提高開發體驗

可使用babel-plugin-styled-componentsbabel macro來支持服務端渲染、 樣式壓縮和提高 debug 體驗. 推薦使用 macro 形式, 無需安裝和配置 babel 插件. 在 create-react-app 中已內置支持:

import styled, { createGlobalStyle } from 'styled-components/macro';

const Thing = styled.div` color: red; `;
複製代碼

詳見Tooling


9. 瞭解 styled-components 的侷限性

比較能想到的侷限性是性能問題:

  1. css-in-js: 須要一個 JS 運行時, 會增長 js 包體積(大約 15KB)
  2. 相比原生 CSS 會有更多節點嵌套(例如 ThemeConsumer)和計算消耗. 這個對於複雜的組件樹的渲染影響尤其明顯
  3. 不能抽取爲 CSS 文件, 這一般不算問題

官方benchmark

下面是基於 v4.0 基準測試對比圖, 在衆多 CSS-in-js 方案中, styled-components 處於中上水平:

styled-components benchmark


10. 一些開發規範

  • 避免無心義的組件名. 避免相似Div, Span這類直接照搬元素名的無心義的組件命名

  • 在一個文件中定義 styled-components 組件. 對於比較簡單的組件, 通常會在同一個文件中定義 styled-components 組件就好了. 下面是典型組件的文件結構:

    import React, { FC } from 'react';
    import styled from 'styled-components/macro';
    
    // 在頂部定義全部styled-components組件
    const Header = styled.header``;
    const Title = styled.div``;
    const StepName = styled.div``;
    const StepBars = styled.div``;
    const StepBar = styled.div<{ active?: boolean }>``;
    const FormContainer = styled.div``;
    
    // 使用組件
    export const Steps: FC<StepsProps> = props => {
      return <>...</>;
    };
    
    export default Steps;
    複製代碼

    然而對於比較複雜的頁面組件來講, 會讓文件變得很臃腫, 擾亂組件的主體, 因此筆者通常會像抽取到單獨的styled.tsx文件中:

    import React, { FC } from 'react';
    import { Header, Title, StepName, StepBars, StepBar, FormContainer } from './styled';
    
    export const Steps: FC<StepsProps> = props => {
      return <>...</>;
    };
    
    export default Steps;
    複製代碼

  • 考慮導出 styled-components 組件, 方便上級組件設置樣式

    // ---Foo/index.ts---
    import * as Styled from './styled';
    
    export { Styled };
    // ...
    
    // ---Bar/index.ts----
    import { Styled } from '../Foo';
    
    const MyComponent = styled.div` & ${Styled.SomeComponent} { color: red; } `;
    複製代碼

11. 其餘 CSS-in-js 方案

  • CSS-module
  • JSS
  • emotion
  • glamorous

這裏值得一提的是CSS-module, 這也是社區比較流行的解決方案. 嚴格來講, 這不是 CSS-in-js. 有興趣的讀者能夠看這篇文章CSS Modules 詳解及 React 中實踐.

特性:

  • 比較輕量, 不須要 JS 運行時, 由於他在編譯階段進行計算
  • 全部樣式默認都是 local, 經過導入模塊方式能夠導入這些生成的類名
  • 能夠和 CSS proprocessor 配合
  • 採用非標準的語法, 例如:global, :local, :export, compose:

CSS module 一樣也有外部樣式覆蓋問題, 因此須要經過其餘手段對關鍵節點添加其餘屬性(如 data-name).

若是使用 css-module, 建議使用*.module.css來命名 css 文件, 和普通 CSS 區分開來.

擴展:




4️⃣ 通用的組件庫不該該耦合 CSS-in-js/CSS-module 的方案

若是是做爲第三方組件庫形式開發, 我的以爲不該該耦合各類 CSS-in-js/CSS-module. 不能強求你的組件庫使用者耦合這些技術棧, 並且部分技術是須要構建工具支持的. 建議使用原生 CSS 或者將 SCSS/Less 這些預處理工具做爲加強方案




5️⃣ 優先使用原生 CSS

筆者的項目大部分都是使用styled-components, 但對於部分極致要求性能的組件, 通常我會回退使用原生 CSS, 再配合 BEM 命名規範. 這種最簡單方式, 可以知足大部分需求.




6️⃣ 選擇合適本身團隊的技術棧

每一個團隊的狀況和偏好不同, 選擇合適本身的纔是最好的. 關於 CSS 方面的技術棧搭配也很是多樣:

css determination

  • 選擇 CSS-in-js 方案: 優勢: 這個方案解決了大部分 CSS 的缺陷, 靈活, 動態性強, 學習成本比較低, 很是適合組件化的場景. 缺點: 性能相比靜態 CSS 要弱, 不過這點已經慢慢在改善. 能夠考慮在部分組件使用原生 CSS
  • 選擇 CSS 方案:
    • 選擇原生 CSS 方案: 這種方案最簡單
    • 選擇 Preprocessor: 添加 CSS 預處理器, 能夠加強 CSS 的可編程性: 例如模塊化, 變量, 函數, mixin. 優勢: 預處理器能夠減小代碼重複, 讓 CSS 更好維護. 適合組織性要求很高的大型項目. 缺點: 就是須要學習成本, 因此這裏筆者建議使用標準的 cssnext 來代替 SCSS/Less 這些方案
    • 方法論: CSS 的各類方法論旨在提升 CSS 的組織性, 提供一些架構建議, 讓 CSS 更好維護.
    • postcss: 對 CSS 進行優化加強, 例如添加廠商前綴
    • css-module: 隔離 CSS, 支持暴露變量給 JS, 解決 CSS 的一些缺陷, 讓 CSS 適合組件化場景. 可選, 經過合適的命名和組織實際上是能夠規避 CSS 的缺陷

綜上所述, CSS-in-js 和 CSS 方案各有適用場景. 好比對於組件庫, 如 antd 則選擇了 Preprocessor 方案; 對於通常應用筆者建議使用 CSS-in-js 方案, 它學習成本很低, 而且There's Only One Way To Do It 沒有太多心智負擔, 不須要學習冗雜的方法論, 代碼相對比較可控; 另外它還支持跨平臺, 在 ReactNative 下, styled-components 是更好的選擇. 而 CSS 方案, 對於大型應用要作到有組織有紀律和規劃化, 須要花費較大的精力, 尤爲是團隊成員能力不均狀況下, 很容易失控




7️⃣ 使用 svgr 轉換 svg 圖標

現在 CSS-Image-Sprite 早已被 SVG-Sprite 取代. 而在 React 生態中使用svgr更加方便, 它能夠將 svg 文件轉換爲 React 組件, 也就是一個普通的 JS 模塊, 它有如下優點:

  • 轉換爲普通 JS 文件, 方便代碼分割和異步加載
  • 相比 svg-sprite 和 iconfont 方案更容易管理
  • svg 能夠經過 CSS/JS 配置, 可操做性更強; 相比 iconfont 支持多色
  • 支持 svgo 壓縮

基本用法:

import starUrl, { ReactComponent as Star } from './star.svg';
const App = () => (
  <div>
    <img src={starUrl} alt="star" />
    <Star />
  </div>
);
複製代碼

瞭解更多

antd 3.9 以後使用 svg 圖標代替了 font 圖標
對比SVG vs Image, SVG vs Iconfont




8️⃣ 結合使用 rem 和 em 等相對單位, 建立更有彈性的組件

Bootstrap v4 全面使用 rem 做爲基本單位, 這使得全部組件均可以響應瀏覽器字體的調整:

bootstrap

rem 可讓整個文檔能夠響應 html 字體的變化, 常常用於移動端等比例還原設計稿, 詳見Rem 佈局的原理解析. 我我的對於以爲彈性組件來講更重要的是 em 單位, 尤爲是那些比例固定組件, 例如 Button, Switch, Icon. 好比我會這樣定義 svg Icon 的樣式:

.svg-icon {
  width: 1em;
  height: 1em;
  fill: currentColor;
}
複製代碼

像 iconfont 同樣, 外部只須要設置font-size就能夠配置 icon 到合適的尺寸, 默認則繼承當前上下文的字體大小:

<MyIcon style={{ fontSize: 17 }} />
複製代碼

em 可讓Switch這類固定比例的組件的樣式能夠更容易的被配置, 能夠配合函數將px轉換爲em:

Edit redux hooks

擴展:




3. 規範

1️⃣ 促進創建統一的 UI 設計規範

上文已經闡述了 UI 設計規範的重要性, 有興趣的讀者能夠看看這篇文章開發和設計溝通有多難? - 你只差一個設計規範. 簡單總結一下:

  • 提供團隊協做效率
  • 提升組件的複用率. 統一的組件規範可讓組件更好管理
  • 保持產品迭代過程當中品牌一致性

2️⃣ CSS 編寫規範

能夠參考如下規範:

3️⃣ 使用stylint進行樣式規範檢查




擴展

相關文章
相關標籤/搜索