Styled-Components:現代 Web 的 CSS-in-JS 庫

已發表: 2022-03-11

CSS 是為文檔設計的,“舊網絡”應該包含的內容。 像 Sass 或 Less 這樣的預處理器的出現表明,社區需要的比 CSS 提供的要多。 隨著網絡應用程序隨著時間的推移變得越來越複雜,CSS 的局限性變得越來越明顯並且難以緩解。

Styled-components利用完整的編程語言(JavaScript)的強大功能及其範圍功能來幫助將代碼結構化為組件。 這有助於避免為大型項目編寫和維護 CSS 的常見陷阱。 開發人員可以在沒有副作用風險的情況下描述組件的樣式。

有什麼問題?

使用 CSS 的一個優點是樣式與代碼完全分離。 這意味著開發人員和設計人員可以並行工作而不會相互干擾。

另一方面, styled-components更容易陷入風格和邏輯強耦合的陷阱。 Max Stoiber 解釋瞭如何避免這種情況。 雖然分離邏輯和表示的想法絕對不是新的,但在開發 React 組件時可能會想走捷徑。 例如,很容易為一個驗證按鈕創建一個組件來處理點擊動作以及按鈕的樣式。 將它分成兩個組件需要更多的努力。

容器/展示架構

這是一個非常簡單的原則。 組件要么定義事物的外觀,要么管理數據和邏輯。 表示組件的一個非常重要的方面是它們不應該有任何依賴關係。 他們接收道具並相應地渲染 DOM(或孩子)。 另一方面,容器知道數據架構(狀態、redux、flux 等),但絕不應該負責顯示。 Dan Abramov 的文章對這種架構進行了非常好的和詳細的解釋。

記住 SMACSS

儘管 CSS 的 Scalable and Modular Architecture 是組織 CSS 的樣式指南,但基本概念是樣式組件自動遵循的,大部分情況下是自動遵循的。 這個想法是將 CSS 分為五類:

  • Base包含所有一般規則。
  • Layout的目的是定義結構屬性以及內容的各個部分(例如頁眉、頁腳、側邊欄、內容)。
  • 模塊包含 UI 的各種邏輯塊的子類別。
  • 狀態定義修飾符類來指示元素的狀態,例如錯誤的字段,禁用的按鈕。
  • 主題包含可以修改或取決於用戶偏好的顏色、字體和其他外觀方面。

在使用樣式組件時保持這種分離很容易。 項目通常包括某種 CSS 規範化或重置。 這通常屬於基本類別。 您還可以定義一般的字體大小、線條大小等。這可以通過普通的 CSS(或 Sass/Less)或通過styled-components提供的injectGlobal函數來完成。

對於佈局規則,如果您使用 UI 框架,那麼它可能會定義容器類或網格系統。 您可以輕鬆地將這些類與您自己編寫的佈局組件中的規則結合使用。

模塊自動跟隨styled-components的架構,因為樣式直接附加到組件,而不是在外部文件中描述。 基本上,您編寫的每個樣式組件都是其自己的模塊。 您可以編寫樣式代碼而不必擔心副作用。

狀態將是您在組件中定義為變量規則的規則。 您只需定義一個函數來插入 CSS 屬性的值。 如果使用 UI 框架,您可能還會將有用的類添加到您的組件中。 您可能還會有 CSS 偽選擇器規則(懸停、焦點等)

主題可以簡單地插入到您的組件中。 將您的主題定義為一組要在整個應用程序中使用的變量是一個好主意。 您甚至可以通過編程方式(使用庫或手動)獲取顏色,例如處理對比度和高光。 請記住,您擁有編程語言的全部功能!

將他們聚集在一起尋求解決方案

為了更輕鬆的導航體驗,將它們放在一起很重要; 我們不想按類型(表示與邏輯)來組織它們,而是按功能來組織它們。

因此,我們將為所有通用組件(按鈕等)創建一個文件夾。 其他應根據項目及其功能進行組織。 例如,如果我們有用戶管理功能,我們應該對特定於該功能的所有組件進行分組。

要將styled-components 的容器/表示架構應用於 SMACSS 方法,我們需要一種額外的組件類型:結構。 我們最終得到三種組件; 樣式、結構和容器。 由於styled-components裝飾了一個標籤(或組件),我們需要第三種類型的組件來構建 DOM。 在某些情況下,可能允許容器組件處理子組件的結構,但是當 DOM 結構變得複雜並且出於視覺目的需要時,最好將它們分開。 一個很好的例子是一個表格,其中 DOM 通常會變得非常冗長。

示例項目

讓我們構建一個顯示食譜的小應用程序來說明這些原則。 我們可以開始構建一個食譜組件。 父組件將是一個控制器。 它將處理狀態——在本例中是配方列表。 它還將調用 API 函數來獲取數據。

 class Recipes extends Component{ constructor (props) { super(props); this.state = { recipes: [] }; } componentDidMount () { this.loadData() } loadData () { getRecipes().then(recipes => { this.setState({recipes}) }) } render() { let {recipes} = this.state return ( <RecipesContainer recipes={recipes} /> ) } }

它將呈現食譜列表,但它不需要(也不應該)知道如何。 所以我們渲染另一個組件,它獲取配方列表並輸出 DOM:

 class RecipesContainer extends Component{ render() { let {recipes} = this.props return ( <TilesContainer> {recipes.map(recipe => (<Recipe key={recipe.id} {...recipe}/>))} </TilesContainer> ) } }

在這裡,實際上,我們想要製作一個平舖網格。 將實際的 tile 佈局設為通用組件可能是個好主意。 因此,如果我們提取它,我們會得到一個如下所示的新組件:

 class TilesContainer extends Component { render () { let {children} = this.props return ( <Tiles> { React.Children.map(children, (child, i) => ( <Tile key={i}> {child} </Tile> )) } </Tiles> ) } }

TilesStyles.js:

 export const Tiles = styled.div` padding: 20px 10px; display: flex; flex-direction: row; flex-wrap: wrap; ` export const Tile = styled.div` flex: 1 1 auto; ... display: flex; & > div { flex: 1 0 auto; } `

請注意,此組件純粹是展示性的。 它定義了它的樣式並將它接收到的任何子元素包裝在另一個樣式化的 DOM 元素中,該元素定義了瓦片的外觀。 這是一個很好的例子,說明您的通用表示組件在架構上的外觀。

然後,我們需要定義配方的外觀。 我們需要一個容器組件來描述相對複雜的 DOM,並在必要時定義樣式。 我們最終得到了這個:

 class RecipeContainer extends Component { onChangeServings (e) { let {changeServings} = this.props changeServings(e.target.value) } render () { let {title, ingredients, instructions, time, servings} = this.props return ( <Recipe> <Title>{title}</Title> <div>{time}</div> <div>Serving <input type="number" min="1" max="1000" value={servings} onChange={this.onChangeServings.bind(this)}/> </div> <Ingredients> {ingredients.map((ingredient, i) => ( <Ingredient key={i} servings={servings}> <span className="name">{ingredient.name}</span> <span className="quantity">{ingredient.quantity * servings} {ingredient.unit}</span> </Ingredient> ))} </Ingredients> <div> {instructions.map((instruction, i) => (<p key={i}>{instruction}</p>))} </div> </Recipe> ) } }

請注意,容器會生成一些 DOM,但它是它包含的唯一邏輯。 請記住,您可以定義嵌套樣式,因此您不需要為每個需要樣式的標籤創建樣式元素。 這就是我們在這里為成分項目的名稱和數量所做的。 當然,我們可以進一步拆分它並為一種成分創建一個新組件。 這取決於您(取決於項目的複雜性)來確定粒度。 在這種情況下,它只是與 RecipeStyles 文件中的其餘部分一起定義的樣式組件:

 export const Recipe = styled.div` background-color: ${theme('colors.background-highlight')}; `; export const Title = styled.div` font-weight: bold; ` export const Ingredients = styled.ul` margin: 5px 0; ` export const Ingredient = styled.li` & .name { ... } & .quantity { ... } `

出於本練習的目的,我使用了 ThemeProvider。 它將主題註入到樣式化組件的 props 中。 您可以簡單地將其用作color: ${props => props.theme.core_color} ,我只是使用一個小包裝器來防止主題中缺少屬性:

 const theme = (key) => (prop) => _.get(prop.theme, key) || console.warn('missing key', key)

您還可以在模塊中定義自己的常量並使用這些常量。 例如: color: ${styleConstants.core_color}

優點

使用styled-components的一個好處是您可以盡可能少地使用它。 您可以使用自己喜歡的 UI 框架並在其上添加樣式組件。 這也意味著您可以輕鬆地逐個遷移現有項目組件。 您可以選擇使用標準 CSS 為大部分佈局設置樣式,並且僅將styled-components用於可重用組件。

缺點

設計師/風格整合者需要學習非常基本的 JavaScript 來處理變量並使用它們來代替 Sass/Less。

他們還必須學習導航項目結構,儘管我認為在該組件的文件夾中查找組件的樣式比必須找到包含您需要修改的規則的正確 CSS/Sass/Less 文件更容易。

如果他們想要語法高亮、linting 等,他們還需要稍微改變他們的工具。一個很好的起點是這個 Atom 插件和這個 babel 插件。