CSS 是前端開發的重要組成部分,可是它並不完美,本文主要探討 React 樣式管理方面的一些解決方案,目的是實現樣式的高度可定製化, 讓大型項目的樣式代碼更容易維護.javascript
系列目錄css
目錄html
2014 年vjeux一個 speak 深入揭示的原生 CSS 的一些侷限性. 雖然它有些爭議, 對於開發者來講更多的是啓發. 至從那以後出現了不少 CSS-in-js
解決方案.前端
CSS 的選擇器是沒有隔離性的, 無論是使用命名空間仍是 BEM 模式組織, 最終都會污染全局命名空間. 尤爲是大型團隊合做的項目, 很難肯定某個特定的類或者元素是否已經賦過樣式. 因此在大部分狀況下咱們都會絞盡腦汁新建立一個類名, 而不是複用已有的類型.java
解決的方向: 生成惟一的類名; shadow dom; 內聯樣式; Vue-scoped 方案react
因爲 CSS 的'全局性', 因此就產生了依賴問題:webpack
一方面咱們須要在組件渲染前就須要先將 CSS 加載完畢, 可是很難清晰地定義某個特定組件依賴於某段特定的 CSS 代碼; 另外一方面, 全局性致使你的樣式可能被別的組件依賴(某種程度的細節耦合), 你不能隨便修改你的樣式, 以避免破壞其餘頁面或組件的樣式. 若是團隊沒有制定合適的 CSS 規範(例如 BEM, 不直接使用標籤選擇器, 減小選擇器嵌套等等), 代碼很快就會失控git
解決的方向: 以前文章提到組件是一個內聚單元, 樣式應該是和組件綁定的. 最基本的解決辦法是使用相似 BEM 命名規範來避免組件之間的命名衝突, 再經過建立優於複用, 組合優於繼承的原則, 來避免組件間樣式耦合;github
因爲上述'依賴'問題, 組件樣式之間並無明確的邊界, 很難判斷哪些樣式屬於那個組件; 在加上 CSS 的'疊層特性', 更沒法肯定刪除樣式會帶來什麼影響.web
現代瀏覽器已支持 CSS 無用代碼檢查. 但對於無組織的 CSS 效果不會太大
解決的方向: 若是樣式的依賴比較明確,則能夠安全地移除無用代碼
選擇器和類名的壓縮能夠減小文件的體積, 提升加載的性能. 由於原生 CSS 通常有開發者由配置類名(在 html 或 js 動態指定), 因此工具很難對類名進行控制.
壓縮類名也會減低代碼的可讀性, 變得難以調試.
解決的方向: 由工具來轉換或建立類名
常規的 CSS 很難作到在樣式和 JS 之間共享變量, 例如自定義主題色, 一般經過內聯樣式來部分實現這種需求
解決的方向: CSS-in-js
CSS 規則的加載順序是很重要的, 他會影響屬性應用的優先級, 若是按需加載 CSS, 則沒法確保他們的解析順序, 進而致使錯誤的樣式應用到元素上. 有些開發者爲了解決這個問題, 使用!important 聲明屬性, 這無疑是進入了另外一個坑.
解決方向:避免使用全局樣式,組件樣式隔離;樣式加載和組件生命週期綁定
組件的樣式應該是能夠自由定製的, 開發者應該考慮組件的各類使用場景. 因此一個好的組件必須暴露相關的樣式定製接口. 至少須要支持爲頂層元素配置className
和style
屬性:
interface ButtonProps {
className?: string;
style?: React.CSSProperties;
}
複製代碼
這兩個屬性應該是每一個展現型組件應該暴露的 props, 其餘嵌套元素也要考慮支持配置樣式, 例如 footerClassName, footerStyle.
display: -webkit-flex; display: flex;
)因此 內聯 CSS 適合用於設置動態且比較簡單的樣式屬性
社區上有許多 CSS-in-js 方案是基於內聯 CSS 的, 例如 Radium, 它使用 JS 添加事件處理器來模擬僞類, 另外也媒體查詢和動畫. 不過不是全部東西均可以經過 JS 模擬, 好比僞元素. 因此這類解決方案用得比較少
社區有不少 CSS 解決方案, 有個項目(MicheleBertoli/css-in-js)專門羅列和對比了這些方案. 讀者也能夠讀這篇文章(What to use for React styling?)學習對 CSS 相關技術進行選型決策
社區上最流行的, 也是筆者以爲使用起來最舒服的是styled-components
, styled-components 有下列特性:
推薦這篇文章: Stop using css-in-javascript for web development, styled-components 能夠基本覆蓋全部 CSS 的使用場景:
// 定義組件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),
})``;
複製代碼
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; `;
複製代碼
在 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; `;
複製代碼
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; } `;
複製代碼
媒體查詢幫助方法:
// 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 插件能夠在編譯時轉換爲靜態代碼, 不須要運行時.
全局樣式
全局樣式和組件生命週期綁定, 當組件卸載時也會刪除全局樣式. 全局樣式一般用於覆蓋一些第三方組件樣式
const GlobalStyle = createGlobalStyle`
body {
color: ${props => (props.whiteColor ? 'white' : 'black')};
}
`
// Test
<React.Fragment>
<GlobalStyle whiteColor />
<Navigation /> {/* example of other top-level stuff */}
</React.Fragment>
複製代碼
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 類型
可使用babel-plugin-styled-components
或babel macro
來支持服務端渲染、 樣式壓縮和提高 debug 體驗. 推薦使用 macro 形式, 無需安裝和配置 babel 插件. 在 create-react-app 中已內置支持:
import styled, { createGlobalStyle } from 'styled-components/macro';
const Thing = styled.div` color: red; `;
複製代碼
詳見Tooling
比較能想到的侷限性是性能問題:
下面是基於 v4.0 基準測試對比圖, 在衆多 CSS-in-js 方案中, styled-components 處於中上水平:
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; } `;
複製代碼
這裏值得一提的是CSS-module, 這也是社區比較流行的解決方案. 嚴格來講, 這不是 CSS-in-js. 有興趣的讀者能夠看這篇文章CSS Modules 詳解及 React 中實踐.
特性:
CSS module 一樣也有外部樣式覆蓋問題, 因此須要經過其餘手段對關鍵節點添加其餘屬性(如 data-name).
若是使用 css-module, 建議使用
*.module.css
來命名 css 文件, 和普通 CSS 區分開來.
擴展:
若是是做爲第三方組件庫形式開發, 我的以爲不該該耦合各類 CSS-in-js/CSS-module. 不能強求你的組件庫使用者耦合這些技術棧, 並且部分技術是須要構建工具支持的. 建議使用原生 CSS 或者將 SCSS/Less 這些預處理工具做爲加強方案
筆者的項目大部分都是使用styled-components
, 但對於部分極致要求性能的組件, 通常我會回退使用原生 CSS, 再配合 BEM 命名規範. 這種最簡單方式, 可以知足大部分需求.
每一個團隊的狀況和偏好不同, 選擇合適本身的纔是最好的. 關於 CSS 方面的技術棧搭配也很是多樣:
綜上所述, CSS-in-js 和 CSS 方案各有適用場景. 好比對於組件庫, 如 antd 則選擇了 Preprocessor 方案; 對於通常應用筆者建議使用 CSS-in-js 方案, 它學習成本很低, 而且There's Only One Way To Do It
沒有太多心智負擔, 不須要學習冗雜的方法論, 代碼相對比較可控; 另外它還支持跨平臺, 在 ReactNative 下, styled-components 是更好的選擇. 而 CSS 方案, 對於大型應用要作到有組織有紀律和規劃化, 須要花費較大的精力, 尤爲是團隊成員能力不均狀況下, 很容易失控
現在 CSS-Image-Sprite 早已被 SVG-Sprite 取代. 而在 React 生態中使用svgr
更加方便, 它能夠將 svg 文件轉換爲 React 組件, 也就是一個普通的 JS 模塊, 它有如下優點:
基本用法:
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
Bootstrap v4 全面使用 rem 做爲基本單位, 這使得全部組件均可以響應瀏覽器字體的調整:
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:
擴展:
上文已經闡述了 UI 設計規範的重要性, 有興趣的讀者能夠看看這篇文章開發和設計溝通有多難? - 你只差一個設計規範. 簡單總結一下:
能夠參考如下規範: